diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala index a2c637c..bfa063a 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala @@ -1,13 +1,11 @@ package works.iterative.ui.components.laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorDef import com.raquo.laminar.tags.HtmlTag case class Bin[Source, +Value]( label: String, description: Option[String | HtmlElement], - color: ColorDef, valueOf: Source => Value ): def map[NewValue]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala index a2c637c..bfa063a 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala @@ -1,13 +1,11 @@ package works.iterative.ui.components.laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorDef import com.raquo.laminar.tags.HtmlTag case class Bin[Source, +Value]( label: String, description: Option[String | HtmlElement], - color: ColorDef, valueOf: Source => Value ): def map[NewValue]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala deleted file mode 100644 index 3aac0bf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala +++ /dev/null @@ -1,34 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ButtonComponentsModule: - def buttons: ButtonComponents - - trait ButtonComponents: - def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala index a2c637c..bfa063a 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala @@ -1,13 +1,11 @@ package works.iterative.ui.components.laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorDef import com.raquo.laminar.tags.HtmlTag case class Bin[Source, +Value]( label: String, description: Option[String | HtmlElement], - color: ColorDef, valueOf: Source => Value ): def map[NewValue]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala deleted file mode 100644 index 3aac0bf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala +++ /dev/null @@ -1,34 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ButtonComponentsModule: - def buttons: ButtonComponents - - trait ButtonComponents: - def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala deleted file mode 100644 index 646e73d..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala +++ /dev/null @@ -1,191 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.color.ColorDef -import com.raquo.laminar.tags.HtmlTag -import org.scalajs.dom.html.UList -import com.raquo.laminar.nodes.ReactiveHtmlElement -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind - -trait DashboardComponentsModule: - def dashboard: DashboardComponents - - // Only methods that create HtmlElements - trait DashboardComponents(using ComponentContext): - def section(label: String, children: Modifier[HtmlElement]): Div - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI - def cardInitials( - initials: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div - def count(label: String, value: Int, color: ColorDef): Span - def counts(children: HtmlElement*): Span - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - -trait DefaultDashboardComponentsModule(using ComponentContext) - extends DashboardComponentsModule: - override val dashboard: DashboardComponents = new DashboardComponents: - def section(label: String, children: Modifier[HtmlElement]): Div = - div( - cls("mt-3"), - h2( - cls("text-gray-500 text-xs font-medium uppercase tracking-wide"), - label - ), - children - ) - - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] = - ul( - cls( - "mt-3 grid gap-5 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" - ), - children - ) - - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI = - li( - div( - cls("col-span-1 flex shadow-sm rounded-md"), - initials, - div( - cls( - "flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate" - ), - div( - cls("flex-1 px-4 py-2 text-sm truncate"), - div( - cls("text-gray-900 font-medium hover:text-gray-600 truncate"), - label - ), - p(cls("text-gray-500"), children) - ) - ) - ) - ) - - def cardInitials( - id: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div = - div( - cls( - "flex-shrink-0 flex flex-col items-center justify-center w-16 text-white text-sm font-medium rounded-l-md" - ), - color.map(_(600).bg), - div(id), - children - ) - - def count(label: String, value: Int, color: ColorDef): Span = - span(color.text, title(label), s"${value}") - - def counts(children: HtmlElement*): Span = - span(interleave(children, span(" / "))) - - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div = - div( - cls("flex flex-col mt-2"), - div( - cls( - "align-middle min-w-full shadow sm:rounded-lg" - ), - L.table( - cls("min-w-full border-separate border-spacing-0"), - styleAttr("border-spacing: 0"), // Tailwind somehow doesn't work - thead( - tr( - cls("border-t border-gray-200"), - headers.map(h => - th( - cls( - "sticky top-0 z-10 px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - h - ) - ) - ) - ), - tbody(cls("bg-white divide-y divide-gray-100"), sections) - ) - ) - ) - - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - td( - cls("bg-gray-50 border-gray-200 border-t border-b"), - cls( - "sticky top-14 z-10 bg-gray-50 px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - colSpan(cs), - label - ), - mods - ) - - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - mods, - td( - cls( - "px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900 truncate" - ), - label - ), - children.map(e => - td( - cls( - "whitespace-nowrap px-2 py-2 text-sm text-gray-500" - ), - e - ) - ) - ) - - private def interleave( - a: Seq[HtmlElement], - separator: => HtmlElement - ): Seq[HtmlElement] = - if a.size < 2 then a - else a.init.flatMap(n => Seq(n, separator)) :+ a.last diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala index a2c637c..bfa063a 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala @@ -1,13 +1,11 @@ package works.iterative.ui.components.laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorDef import com.raquo.laminar.tags.HtmlTag case class Bin[Source, +Value]( label: String, description: Option[String | HtmlElement], - color: ColorDef, valueOf: Source => Value ): def map[NewValue]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala deleted file mode 100644 index 3aac0bf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala +++ /dev/null @@ -1,34 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ButtonComponentsModule: - def buttons: ButtonComponents - - trait ButtonComponents: - def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala deleted file mode 100644 index 646e73d..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala +++ /dev/null @@ -1,191 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.color.ColorDef -import com.raquo.laminar.tags.HtmlTag -import org.scalajs.dom.html.UList -import com.raquo.laminar.nodes.ReactiveHtmlElement -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind - -trait DashboardComponentsModule: - def dashboard: DashboardComponents - - // Only methods that create HtmlElements - trait DashboardComponents(using ComponentContext): - def section(label: String, children: Modifier[HtmlElement]): Div - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI - def cardInitials( - initials: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div - def count(label: String, value: Int, color: ColorDef): Span - def counts(children: HtmlElement*): Span - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - -trait DefaultDashboardComponentsModule(using ComponentContext) - extends DashboardComponentsModule: - override val dashboard: DashboardComponents = new DashboardComponents: - def section(label: String, children: Modifier[HtmlElement]): Div = - div( - cls("mt-3"), - h2( - cls("text-gray-500 text-xs font-medium uppercase tracking-wide"), - label - ), - children - ) - - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] = - ul( - cls( - "mt-3 grid gap-5 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" - ), - children - ) - - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI = - li( - div( - cls("col-span-1 flex shadow-sm rounded-md"), - initials, - div( - cls( - "flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate" - ), - div( - cls("flex-1 px-4 py-2 text-sm truncate"), - div( - cls("text-gray-900 font-medium hover:text-gray-600 truncate"), - label - ), - p(cls("text-gray-500"), children) - ) - ) - ) - ) - - def cardInitials( - id: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div = - div( - cls( - "flex-shrink-0 flex flex-col items-center justify-center w-16 text-white text-sm font-medium rounded-l-md" - ), - color.map(_(600).bg), - div(id), - children - ) - - def count(label: String, value: Int, color: ColorDef): Span = - span(color.text, title(label), s"${value}") - - def counts(children: HtmlElement*): Span = - span(interleave(children, span(" / "))) - - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div = - div( - cls("flex flex-col mt-2"), - div( - cls( - "align-middle min-w-full shadow sm:rounded-lg" - ), - L.table( - cls("min-w-full border-separate border-spacing-0"), - styleAttr("border-spacing: 0"), // Tailwind somehow doesn't work - thead( - tr( - cls("border-t border-gray-200"), - headers.map(h => - th( - cls( - "sticky top-0 z-10 px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - h - ) - ) - ) - ), - tbody(cls("bg-white divide-y divide-gray-100"), sections) - ) - ) - ) - - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - td( - cls("bg-gray-50 border-gray-200 border-t border-b"), - cls( - "sticky top-14 z-10 bg-gray-50 px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - colSpan(cs), - label - ), - mods - ) - - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - mods, - td( - cls( - "px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900 truncate" - ), - label - ), - children.map(e => - td( - cls( - "whitespace-nowrap px-2 py-2 text-sm text-gray-500" - ), - e - ) - ) - ) - - private def interleave( - a: Seq[HtmlElement], - separator: => HtmlElement - ): Seq[HtmlElement] = - if a.size < 2 then a - else a.init.flatMap(n => Seq(n, separator)) :+ a.last diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala deleted file mode 100644 index a45beaf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.FileRef - -trait DetailComponentsModule: - - def details: DetailComponents - - trait DetailComponents: - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement - - def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement - - def files(fs: List[FileRef]): HtmlElement - - def file(f: FileRef): HtmlElement - -trait DefaultDetailComponentsModule(using ctx: ComponentContext) - extends DetailComponentsModule: - self: IconsModule => - - override val details: DetailComponents = new DetailComponents: - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement = - div( - div( - cls := "px-4 sm:px-0", - h3( - cls := "text-base font-semibold leading-7 text-gray-900", - title - ), - subtitle.map(st => - p( - cls := "mt-1 max-w-2xl text-sm leading-6 text-gray-500", - st - ) - ) - // TODO: actions - ), - div( - cls := "mt-6 border-t border-gray-100", - dl( - cls := "divide-y divide-gray-100", - fields - ) - ) - ) - - override def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0", - dt( - cls := "text-sm font-medium leading-6 text-gray-900", - title - ), - dd( - cls := "mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0", - content - ) - ) - - override def files(fs: List[FileRef]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-100 rounded-md border border-gray-200", - fs.map(file) - ) - - override def file(f: FileRef): HtmlElement = - li( - cls := "flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6", - div( - cls := "flex w-0 flex-1 items-center", - icons.`paper-clip-solid`(), - div( - cls := "ml-4 flex min-w-0 flex-1 gap-2", - span( - cls := "truncate font-medium", - f.name - ), - f.size.map(size => span(cls := "flex-shrink-0 text-gray-400", size)) - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - "Uložit" - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala index a2c637c..bfa063a 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala @@ -1,13 +1,11 @@ package works.iterative.ui.components.laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorDef import com.raquo.laminar.tags.HtmlTag case class Bin[Source, +Value]( label: String, description: Option[String | HtmlElement], - color: ColorDef, valueOf: Source => Value ): def map[NewValue]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala deleted file mode 100644 index 3aac0bf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala +++ /dev/null @@ -1,34 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ButtonComponentsModule: - def buttons: ButtonComponents - - trait ButtonComponents: - def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala deleted file mode 100644 index 646e73d..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala +++ /dev/null @@ -1,191 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.color.ColorDef -import com.raquo.laminar.tags.HtmlTag -import org.scalajs.dom.html.UList -import com.raquo.laminar.nodes.ReactiveHtmlElement -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind - -trait DashboardComponentsModule: - def dashboard: DashboardComponents - - // Only methods that create HtmlElements - trait DashboardComponents(using ComponentContext): - def section(label: String, children: Modifier[HtmlElement]): Div - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI - def cardInitials( - initials: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div - def count(label: String, value: Int, color: ColorDef): Span - def counts(children: HtmlElement*): Span - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - -trait DefaultDashboardComponentsModule(using ComponentContext) - extends DashboardComponentsModule: - override val dashboard: DashboardComponents = new DashboardComponents: - def section(label: String, children: Modifier[HtmlElement]): Div = - div( - cls("mt-3"), - h2( - cls("text-gray-500 text-xs font-medium uppercase tracking-wide"), - label - ), - children - ) - - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] = - ul( - cls( - "mt-3 grid gap-5 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" - ), - children - ) - - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI = - li( - div( - cls("col-span-1 flex shadow-sm rounded-md"), - initials, - div( - cls( - "flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate" - ), - div( - cls("flex-1 px-4 py-2 text-sm truncate"), - div( - cls("text-gray-900 font-medium hover:text-gray-600 truncate"), - label - ), - p(cls("text-gray-500"), children) - ) - ) - ) - ) - - def cardInitials( - id: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div = - div( - cls( - "flex-shrink-0 flex flex-col items-center justify-center w-16 text-white text-sm font-medium rounded-l-md" - ), - color.map(_(600).bg), - div(id), - children - ) - - def count(label: String, value: Int, color: ColorDef): Span = - span(color.text, title(label), s"${value}") - - def counts(children: HtmlElement*): Span = - span(interleave(children, span(" / "))) - - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div = - div( - cls("flex flex-col mt-2"), - div( - cls( - "align-middle min-w-full shadow sm:rounded-lg" - ), - L.table( - cls("min-w-full border-separate border-spacing-0"), - styleAttr("border-spacing: 0"), // Tailwind somehow doesn't work - thead( - tr( - cls("border-t border-gray-200"), - headers.map(h => - th( - cls( - "sticky top-0 z-10 px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - h - ) - ) - ) - ), - tbody(cls("bg-white divide-y divide-gray-100"), sections) - ) - ) - ) - - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - td( - cls("bg-gray-50 border-gray-200 border-t border-b"), - cls( - "sticky top-14 z-10 bg-gray-50 px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - colSpan(cs), - label - ), - mods - ) - - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - mods, - td( - cls( - "px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900 truncate" - ), - label - ), - children.map(e => - td( - cls( - "whitespace-nowrap px-2 py-2 text-sm text-gray-500" - ), - e - ) - ) - ) - - private def interleave( - a: Seq[HtmlElement], - separator: => HtmlElement - ): Seq[HtmlElement] = - if a.size < 2 then a - else a.init.flatMap(n => Seq(n, separator)) :+ a.last diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala deleted file mode 100644 index a45beaf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.FileRef - -trait DetailComponentsModule: - - def details: DetailComponents - - trait DetailComponents: - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement - - def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement - - def files(fs: List[FileRef]): HtmlElement - - def file(f: FileRef): HtmlElement - -trait DefaultDetailComponentsModule(using ctx: ComponentContext) - extends DetailComponentsModule: - self: IconsModule => - - override val details: DetailComponents = new DetailComponents: - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement = - div( - div( - cls := "px-4 sm:px-0", - h3( - cls := "text-base font-semibold leading-7 text-gray-900", - title - ), - subtitle.map(st => - p( - cls := "mt-1 max-w-2xl text-sm leading-6 text-gray-500", - st - ) - ) - // TODO: actions - ), - div( - cls := "mt-6 border-t border-gray-100", - dl( - cls := "divide-y divide-gray-100", - fields - ) - ) - ) - - override def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0", - dt( - cls := "text-sm font-medium leading-6 text-gray-900", - title - ), - dd( - cls := "mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0", - content - ) - ) - - override def files(fs: List[FileRef]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-100 rounded-md border border-gray-200", - fs.map(file) - ) - - override def file(f: FileRef): HtmlElement = - li( - cls := "flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6", - div( - cls := "flex w-0 flex-1 items-center", - icons.`paper-clip-solid`(), - div( - cls := "ml-4 flex min-w-0 flex-1 gap-2", - span( - cls := "truncate font-medium", - f.name - ), - f.size.map(size => span(cls := "flex-shrink-0 text-gray-400", size)) - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - "Uložit" - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala new file mode 100644 index 0000000..f6c583f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait FormComponents(using ctx: ComponentContext) extends LocalDateSelectModule: + def searchIcon: SvgElement + + def renderLocalDateSelect( + id: String, + labelText: Option[String], + placeholderText: Option[String], + mods: LocalDateSelect => Modifier[HtmlElement] + ): HtmlElement = + div( + labelText, + input( + idAttr(id), + nameAttr(id), + autoComplete("date"), + tpe("date"), + cls( + "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ), + placeholderText.map(placeholder(_)), + mods(localDateSelect) + ) + ) + + def searchField( + id: String, + placeholderText: Option[String] = None + )( + mods: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls := "flex-1 min-w-0", + label(ctx.messages("forms.search.label"), forId(id), cls("sr-only")), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + searchIcon + ), + input( + tpe := "search", + nameAttr := "search", + idAttr := id, + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholderText + .orElse( + ctx.messages + .opt( + s"forms.search.${id}.placeholder", + s"form.search.placeholder" + ) + ) + .map(placeholder(_)), + mods + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala index a2c637c..bfa063a 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala @@ -1,13 +1,11 @@ package works.iterative.ui.components.laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorDef import com.raquo.laminar.tags.HtmlTag case class Bin[Source, +Value]( label: String, description: Option[String | HtmlElement], - color: ColorDef, valueOf: Source => Value ): def map[NewValue]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala deleted file mode 100644 index 3aac0bf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala +++ /dev/null @@ -1,34 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ButtonComponentsModule: - def buttons: ButtonComponents - - trait ButtonComponents: - def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala deleted file mode 100644 index 646e73d..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala +++ /dev/null @@ -1,191 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.color.ColorDef -import com.raquo.laminar.tags.HtmlTag -import org.scalajs.dom.html.UList -import com.raquo.laminar.nodes.ReactiveHtmlElement -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind - -trait DashboardComponentsModule: - def dashboard: DashboardComponents - - // Only methods that create HtmlElements - trait DashboardComponents(using ComponentContext): - def section(label: String, children: Modifier[HtmlElement]): Div - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI - def cardInitials( - initials: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div - def count(label: String, value: Int, color: ColorDef): Span - def counts(children: HtmlElement*): Span - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - -trait DefaultDashboardComponentsModule(using ComponentContext) - extends DashboardComponentsModule: - override val dashboard: DashboardComponents = new DashboardComponents: - def section(label: String, children: Modifier[HtmlElement]): Div = - div( - cls("mt-3"), - h2( - cls("text-gray-500 text-xs font-medium uppercase tracking-wide"), - label - ), - children - ) - - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] = - ul( - cls( - "mt-3 grid gap-5 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" - ), - children - ) - - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI = - li( - div( - cls("col-span-1 flex shadow-sm rounded-md"), - initials, - div( - cls( - "flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate" - ), - div( - cls("flex-1 px-4 py-2 text-sm truncate"), - div( - cls("text-gray-900 font-medium hover:text-gray-600 truncate"), - label - ), - p(cls("text-gray-500"), children) - ) - ) - ) - ) - - def cardInitials( - id: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div = - div( - cls( - "flex-shrink-0 flex flex-col items-center justify-center w-16 text-white text-sm font-medium rounded-l-md" - ), - color.map(_(600).bg), - div(id), - children - ) - - def count(label: String, value: Int, color: ColorDef): Span = - span(color.text, title(label), s"${value}") - - def counts(children: HtmlElement*): Span = - span(interleave(children, span(" / "))) - - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div = - div( - cls("flex flex-col mt-2"), - div( - cls( - "align-middle min-w-full shadow sm:rounded-lg" - ), - L.table( - cls("min-w-full border-separate border-spacing-0"), - styleAttr("border-spacing: 0"), // Tailwind somehow doesn't work - thead( - tr( - cls("border-t border-gray-200"), - headers.map(h => - th( - cls( - "sticky top-0 z-10 px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - h - ) - ) - ) - ), - tbody(cls("bg-white divide-y divide-gray-100"), sections) - ) - ) - ) - - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - td( - cls("bg-gray-50 border-gray-200 border-t border-b"), - cls( - "sticky top-14 z-10 bg-gray-50 px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - colSpan(cs), - label - ), - mods - ) - - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - mods, - td( - cls( - "px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900 truncate" - ), - label - ), - children.map(e => - td( - cls( - "whitespace-nowrap px-2 py-2 text-sm text-gray-500" - ), - e - ) - ) - ) - - private def interleave( - a: Seq[HtmlElement], - separator: => HtmlElement - ): Seq[HtmlElement] = - if a.size < 2 then a - else a.init.flatMap(n => Seq(n, separator)) :+ a.last diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala deleted file mode 100644 index a45beaf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.FileRef - -trait DetailComponentsModule: - - def details: DetailComponents - - trait DetailComponents: - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement - - def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement - - def files(fs: List[FileRef]): HtmlElement - - def file(f: FileRef): HtmlElement - -trait DefaultDetailComponentsModule(using ctx: ComponentContext) - extends DetailComponentsModule: - self: IconsModule => - - override val details: DetailComponents = new DetailComponents: - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement = - div( - div( - cls := "px-4 sm:px-0", - h3( - cls := "text-base font-semibold leading-7 text-gray-900", - title - ), - subtitle.map(st => - p( - cls := "mt-1 max-w-2xl text-sm leading-6 text-gray-500", - st - ) - ) - // TODO: actions - ), - div( - cls := "mt-6 border-t border-gray-100", - dl( - cls := "divide-y divide-gray-100", - fields - ) - ) - ) - - override def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0", - dt( - cls := "text-sm font-medium leading-6 text-gray-900", - title - ), - dd( - cls := "mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0", - content - ) - ) - - override def files(fs: List[FileRef]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-100 rounded-md border border-gray-200", - fs.map(file) - ) - - override def file(f: FileRef): HtmlElement = - li( - cls := "flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6", - div( - cls := "flex w-0 flex-1 items-center", - icons.`paper-clip-solid`(), - div( - cls := "ml-4 flex min-w-0 flex-1 gap-2", - span( - cls := "truncate font-medium", - f.name - ), - f.size.map(size => span(cls := "flex-shrink-0 text-gray-400", size)) - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - "Uložit" - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala new file mode 100644 index 0000000..f6c583f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait FormComponents(using ctx: ComponentContext) extends LocalDateSelectModule: + def searchIcon: SvgElement + + def renderLocalDateSelect( + id: String, + labelText: Option[String], + placeholderText: Option[String], + mods: LocalDateSelect => Modifier[HtmlElement] + ): HtmlElement = + div( + labelText, + input( + idAttr(id), + nameAttr(id), + autoComplete("date"), + tpe("date"), + cls( + "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ), + placeholderText.map(placeholder(_)), + mods(localDateSelect) + ) + ) + + def searchField( + id: String, + placeholderText: Option[String] = None + )( + mods: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls := "flex-1 min-w-0", + label(ctx.messages("forms.search.label"), forId(id), cls("sr-only")), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + searchIcon + ), + input( + tpe := "search", + nameAttr := "search", + idAttr := id, + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholderText + .orElse( + ctx.messages + .opt( + s"forms.search.${id}.placeholder", + s"form.search.placeholder" + ) + ) + .map(placeholder(_)), + mods + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala deleted file mode 100644 index 7714490..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala +++ /dev/null @@ -1,82 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -/** Form components - * - * A form is a collection of sections, each of which contains a collection of - * fields. - * - * Form -> * Section -> * Field - * -> * Action - * - * Each Field has an `id`, `label` and `input` element. The `id` is used to - * link the label to the input. - * - * The `input` element can be a simple text input, or a more complex component - * such as a date picker. The caller is expected to make the input work. - */ -trait FormComponentsModule extends LocalDateSelectModule: - def forms: FormComponents - - trait FormComponents: - - /** Layout the sections and actions of a form */ - def form(mods: Modifier[HtmlElement]*)(sections: Modifier[HtmlElement]*)( - actions: Modifier[HtmlElement]* - ): HtmlElement - - /** Section layout, with a title, optional subtitle and any content. */ - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label and any content */ - def field(label: Modifier[HtmlElement])( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label, input and optional help text */ - def field( - id: String, - label: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement - - def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout an inline form element */ - def inlineForm(mods: Modifier[HtmlElement]*): HtmlElement - - /** An input field with a search icon */ - def searchField(id: String, placeholderText: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement - - /** LocalDate input */ - def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala index a2c637c..bfa063a 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala @@ -1,13 +1,11 @@ package works.iterative.ui.components.laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorDef import com.raquo.laminar.tags.HtmlTag case class Bin[Source, +Value]( label: String, description: Option[String | HtmlElement], - color: ColorDef, valueOf: Source => Value ): def map[NewValue]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala deleted file mode 100644 index 3aac0bf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala +++ /dev/null @@ -1,34 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ButtonComponentsModule: - def buttons: ButtonComponents - - trait ButtonComponents: - def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala deleted file mode 100644 index 646e73d..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala +++ /dev/null @@ -1,191 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.color.ColorDef -import com.raquo.laminar.tags.HtmlTag -import org.scalajs.dom.html.UList -import com.raquo.laminar.nodes.ReactiveHtmlElement -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind - -trait DashboardComponentsModule: - def dashboard: DashboardComponents - - // Only methods that create HtmlElements - trait DashboardComponents(using ComponentContext): - def section(label: String, children: Modifier[HtmlElement]): Div - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI - def cardInitials( - initials: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div - def count(label: String, value: Int, color: ColorDef): Span - def counts(children: HtmlElement*): Span - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - -trait DefaultDashboardComponentsModule(using ComponentContext) - extends DashboardComponentsModule: - override val dashboard: DashboardComponents = new DashboardComponents: - def section(label: String, children: Modifier[HtmlElement]): Div = - div( - cls("mt-3"), - h2( - cls("text-gray-500 text-xs font-medium uppercase tracking-wide"), - label - ), - children - ) - - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] = - ul( - cls( - "mt-3 grid gap-5 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" - ), - children - ) - - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI = - li( - div( - cls("col-span-1 flex shadow-sm rounded-md"), - initials, - div( - cls( - "flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate" - ), - div( - cls("flex-1 px-4 py-2 text-sm truncate"), - div( - cls("text-gray-900 font-medium hover:text-gray-600 truncate"), - label - ), - p(cls("text-gray-500"), children) - ) - ) - ) - ) - - def cardInitials( - id: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div = - div( - cls( - "flex-shrink-0 flex flex-col items-center justify-center w-16 text-white text-sm font-medium rounded-l-md" - ), - color.map(_(600).bg), - div(id), - children - ) - - def count(label: String, value: Int, color: ColorDef): Span = - span(color.text, title(label), s"${value}") - - def counts(children: HtmlElement*): Span = - span(interleave(children, span(" / "))) - - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div = - div( - cls("flex flex-col mt-2"), - div( - cls( - "align-middle min-w-full shadow sm:rounded-lg" - ), - L.table( - cls("min-w-full border-separate border-spacing-0"), - styleAttr("border-spacing: 0"), // Tailwind somehow doesn't work - thead( - tr( - cls("border-t border-gray-200"), - headers.map(h => - th( - cls( - "sticky top-0 z-10 px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - h - ) - ) - ) - ), - tbody(cls("bg-white divide-y divide-gray-100"), sections) - ) - ) - ) - - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - td( - cls("bg-gray-50 border-gray-200 border-t border-b"), - cls( - "sticky top-14 z-10 bg-gray-50 px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - colSpan(cs), - label - ), - mods - ) - - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - mods, - td( - cls( - "px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900 truncate" - ), - label - ), - children.map(e => - td( - cls( - "whitespace-nowrap px-2 py-2 text-sm text-gray-500" - ), - e - ) - ) - ) - - private def interleave( - a: Seq[HtmlElement], - separator: => HtmlElement - ): Seq[HtmlElement] = - if a.size < 2 then a - else a.init.flatMap(n => Seq(n, separator)) :+ a.last diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala deleted file mode 100644 index a45beaf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.FileRef - -trait DetailComponentsModule: - - def details: DetailComponents - - trait DetailComponents: - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement - - def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement - - def files(fs: List[FileRef]): HtmlElement - - def file(f: FileRef): HtmlElement - -trait DefaultDetailComponentsModule(using ctx: ComponentContext) - extends DetailComponentsModule: - self: IconsModule => - - override val details: DetailComponents = new DetailComponents: - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement = - div( - div( - cls := "px-4 sm:px-0", - h3( - cls := "text-base font-semibold leading-7 text-gray-900", - title - ), - subtitle.map(st => - p( - cls := "mt-1 max-w-2xl text-sm leading-6 text-gray-500", - st - ) - ) - // TODO: actions - ), - div( - cls := "mt-6 border-t border-gray-100", - dl( - cls := "divide-y divide-gray-100", - fields - ) - ) - ) - - override def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0", - dt( - cls := "text-sm font-medium leading-6 text-gray-900", - title - ), - dd( - cls := "mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0", - content - ) - ) - - override def files(fs: List[FileRef]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-100 rounded-md border border-gray-200", - fs.map(file) - ) - - override def file(f: FileRef): HtmlElement = - li( - cls := "flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6", - div( - cls := "flex w-0 flex-1 items-center", - icons.`paper-clip-solid`(), - div( - cls := "ml-4 flex min-w-0 flex-1 gap-2", - span( - cls := "truncate font-medium", - f.name - ), - f.size.map(size => span(cls := "flex-shrink-0 text-gray-400", size)) - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - "Uložit" - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala new file mode 100644 index 0000000..f6c583f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait FormComponents(using ctx: ComponentContext) extends LocalDateSelectModule: + def searchIcon: SvgElement + + def renderLocalDateSelect( + id: String, + labelText: Option[String], + placeholderText: Option[String], + mods: LocalDateSelect => Modifier[HtmlElement] + ): HtmlElement = + div( + labelText, + input( + idAttr(id), + nameAttr(id), + autoComplete("date"), + tpe("date"), + cls( + "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ), + placeholderText.map(placeholder(_)), + mods(localDateSelect) + ) + ) + + def searchField( + id: String, + placeholderText: Option[String] = None + )( + mods: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls := "flex-1 min-w-0", + label(ctx.messages("forms.search.label"), forId(id), cls("sr-only")), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + searchIcon + ), + input( + tpe := "search", + nameAttr := "search", + idAttr := id, + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholderText + .orElse( + ctx.messages + .opt( + s"forms.search.${id}.placeholder", + s"form.search.placeholder" + ) + ) + .map(placeholder(_)), + mods + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala deleted file mode 100644 index 7714490..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala +++ /dev/null @@ -1,82 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -/** Form components - * - * A form is a collection of sections, each of which contains a collection of - * fields. - * - * Form -> * Section -> * Field - * -> * Action - * - * Each Field has an `id`, `label` and `input` element. The `id` is used to - * link the label to the input. - * - * The `input` element can be a simple text input, or a more complex component - * such as a date picker. The caller is expected to make the input work. - */ -trait FormComponentsModule extends LocalDateSelectModule: - def forms: FormComponents - - trait FormComponents: - - /** Layout the sections and actions of a form */ - def form(mods: Modifier[HtmlElement]*)(sections: Modifier[HtmlElement]*)( - actions: Modifier[HtmlElement]* - ): HtmlElement - - /** Section layout, with a title, optional subtitle and any content. */ - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label and any content */ - def field(label: Modifier[HtmlElement])( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label, input and optional help text */ - def field( - id: String, - label: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement - - def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout an inline form element */ - def inlineForm(mods: Modifier[HtmlElement]*): HtmlElement - - /** An input field with a search icon */ - def searchField(id: String, placeholderText: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement - - /** LocalDate input */ - def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala deleted file mode 100644 index 8acbe7f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala +++ /dev/null @@ -1,28 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.Tag - -trait GenericComponentsModule: - - def generic: GenericComponents - - trait GenericComponents: - def tag(name: String, color: ColorKind): HtmlElement - def tag(t: Tag): HtmlElement = tag(t.value, t.color) - -trait DefaultGenericComponentsModule(using ComponentContext) - extends GenericComponentsModule: - override val generic: GenericComponents = new GenericComponents: - override def tag(name: String, color: ColorKind): HtmlElement = - p( - cls( - "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" - ), - color(800).text, - color(100).bg, - name - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala index a2c637c..bfa063a 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala @@ -1,13 +1,11 @@ package works.iterative.ui.components.laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorDef import com.raquo.laminar.tags.HtmlTag case class Bin[Source, +Value]( label: String, description: Option[String | HtmlElement], - color: ColorDef, valueOf: Source => Value ): def map[NewValue]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala deleted file mode 100644 index 3aac0bf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala +++ /dev/null @@ -1,34 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ButtonComponentsModule: - def buttons: ButtonComponents - - trait ButtonComponents: - def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala deleted file mode 100644 index 646e73d..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala +++ /dev/null @@ -1,191 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.color.ColorDef -import com.raquo.laminar.tags.HtmlTag -import org.scalajs.dom.html.UList -import com.raquo.laminar.nodes.ReactiveHtmlElement -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind - -trait DashboardComponentsModule: - def dashboard: DashboardComponents - - // Only methods that create HtmlElements - trait DashboardComponents(using ComponentContext): - def section(label: String, children: Modifier[HtmlElement]): Div - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI - def cardInitials( - initials: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div - def count(label: String, value: Int, color: ColorDef): Span - def counts(children: HtmlElement*): Span - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - -trait DefaultDashboardComponentsModule(using ComponentContext) - extends DashboardComponentsModule: - override val dashboard: DashboardComponents = new DashboardComponents: - def section(label: String, children: Modifier[HtmlElement]): Div = - div( - cls("mt-3"), - h2( - cls("text-gray-500 text-xs font-medium uppercase tracking-wide"), - label - ), - children - ) - - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] = - ul( - cls( - "mt-3 grid gap-5 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" - ), - children - ) - - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI = - li( - div( - cls("col-span-1 flex shadow-sm rounded-md"), - initials, - div( - cls( - "flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate" - ), - div( - cls("flex-1 px-4 py-2 text-sm truncate"), - div( - cls("text-gray-900 font-medium hover:text-gray-600 truncate"), - label - ), - p(cls("text-gray-500"), children) - ) - ) - ) - ) - - def cardInitials( - id: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div = - div( - cls( - "flex-shrink-0 flex flex-col items-center justify-center w-16 text-white text-sm font-medium rounded-l-md" - ), - color.map(_(600).bg), - div(id), - children - ) - - def count(label: String, value: Int, color: ColorDef): Span = - span(color.text, title(label), s"${value}") - - def counts(children: HtmlElement*): Span = - span(interleave(children, span(" / "))) - - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div = - div( - cls("flex flex-col mt-2"), - div( - cls( - "align-middle min-w-full shadow sm:rounded-lg" - ), - L.table( - cls("min-w-full border-separate border-spacing-0"), - styleAttr("border-spacing: 0"), // Tailwind somehow doesn't work - thead( - tr( - cls("border-t border-gray-200"), - headers.map(h => - th( - cls( - "sticky top-0 z-10 px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - h - ) - ) - ) - ), - tbody(cls("bg-white divide-y divide-gray-100"), sections) - ) - ) - ) - - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - td( - cls("bg-gray-50 border-gray-200 border-t border-b"), - cls( - "sticky top-14 z-10 bg-gray-50 px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - colSpan(cs), - label - ), - mods - ) - - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - mods, - td( - cls( - "px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900 truncate" - ), - label - ), - children.map(e => - td( - cls( - "whitespace-nowrap px-2 py-2 text-sm text-gray-500" - ), - e - ) - ) - ) - - private def interleave( - a: Seq[HtmlElement], - separator: => HtmlElement - ): Seq[HtmlElement] = - if a.size < 2 then a - else a.init.flatMap(n => Seq(n, separator)) :+ a.last diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala deleted file mode 100644 index a45beaf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.FileRef - -trait DetailComponentsModule: - - def details: DetailComponents - - trait DetailComponents: - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement - - def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement - - def files(fs: List[FileRef]): HtmlElement - - def file(f: FileRef): HtmlElement - -trait DefaultDetailComponentsModule(using ctx: ComponentContext) - extends DetailComponentsModule: - self: IconsModule => - - override val details: DetailComponents = new DetailComponents: - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement = - div( - div( - cls := "px-4 sm:px-0", - h3( - cls := "text-base font-semibold leading-7 text-gray-900", - title - ), - subtitle.map(st => - p( - cls := "mt-1 max-w-2xl text-sm leading-6 text-gray-500", - st - ) - ) - // TODO: actions - ), - div( - cls := "mt-6 border-t border-gray-100", - dl( - cls := "divide-y divide-gray-100", - fields - ) - ) - ) - - override def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0", - dt( - cls := "text-sm font-medium leading-6 text-gray-900", - title - ), - dd( - cls := "mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0", - content - ) - ) - - override def files(fs: List[FileRef]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-100 rounded-md border border-gray-200", - fs.map(file) - ) - - override def file(f: FileRef): HtmlElement = - li( - cls := "flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6", - div( - cls := "flex w-0 flex-1 items-center", - icons.`paper-clip-solid`(), - div( - cls := "ml-4 flex min-w-0 flex-1 gap-2", - span( - cls := "truncate font-medium", - f.name - ), - f.size.map(size => span(cls := "flex-shrink-0 text-gray-400", size)) - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - "Uložit" - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala new file mode 100644 index 0000000..f6c583f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait FormComponents(using ctx: ComponentContext) extends LocalDateSelectModule: + def searchIcon: SvgElement + + def renderLocalDateSelect( + id: String, + labelText: Option[String], + placeholderText: Option[String], + mods: LocalDateSelect => Modifier[HtmlElement] + ): HtmlElement = + div( + labelText, + input( + idAttr(id), + nameAttr(id), + autoComplete("date"), + tpe("date"), + cls( + "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ), + placeholderText.map(placeholder(_)), + mods(localDateSelect) + ) + ) + + def searchField( + id: String, + placeholderText: Option[String] = None + )( + mods: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls := "flex-1 min-w-0", + label(ctx.messages("forms.search.label"), forId(id), cls("sr-only")), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + searchIcon + ), + input( + tpe := "search", + nameAttr := "search", + idAttr := id, + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholderText + .orElse( + ctx.messages + .opt( + s"forms.search.${id}.placeholder", + s"form.search.placeholder" + ) + ) + .map(placeholder(_)), + mods + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala deleted file mode 100644 index 7714490..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala +++ /dev/null @@ -1,82 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -/** Form components - * - * A form is a collection of sections, each of which contains a collection of - * fields. - * - * Form -> * Section -> * Field - * -> * Action - * - * Each Field has an `id`, `label` and `input` element. The `id` is used to - * link the label to the input. - * - * The `input` element can be a simple text input, or a more complex component - * such as a date picker. The caller is expected to make the input work. - */ -trait FormComponentsModule extends LocalDateSelectModule: - def forms: FormComponents - - trait FormComponents: - - /** Layout the sections and actions of a form */ - def form(mods: Modifier[HtmlElement]*)(sections: Modifier[HtmlElement]*)( - actions: Modifier[HtmlElement]* - ): HtmlElement - - /** Section layout, with a title, optional subtitle and any content. */ - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label and any content */ - def field(label: Modifier[HtmlElement])( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label, input and optional help text */ - def field( - id: String, - label: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement - - def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout an inline form element */ - def inlineForm(mods: Modifier[HtmlElement]*): HtmlElement - - /** An input field with a search icon */ - def searchField(id: String, placeholderText: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement - - /** LocalDate input */ - def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala deleted file mode 100644 index 8acbe7f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala +++ /dev/null @@ -1,28 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.Tag - -trait GenericComponentsModule: - - def generic: GenericComponents - - trait GenericComponents: - def tag(name: String, color: ColorKind): HtmlElement - def tag(t: Tag): HtmlElement = tag(t.value, t.color) - -trait DefaultGenericComponentsModule(using ComponentContext) - extends GenericComponentsModule: - override val generic: GenericComponents = new GenericComponents: - override def tag(name: String, color: ColorKind): HtmlElement = - p( - cls( - "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" - ), - color(800).text, - color(100).bg, - name - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala deleted file mode 100644 index 8cfc1d9..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala +++ /dev/null @@ -1,123 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.CustomAttrs - -trait IconsModule: - def icons: Icons - - trait Icons: - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement - def close(mods: Modifier[SvgElement]*): SvgElement - def `search-solid`(mods: Modifier[SvgElement]*): SvgElement - def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement - def `document-chart-bar-outline`(mods: Modifier[SvgElement]*): SvgElement - def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement - -trait DefaultIconsModule(using ComponentContext) extends IconsModule: - override val icons: Icons = new Icons: - import svg.* - - object aria: - val hidden = CustomAttrs.svg.ariaHidden - - private def withDefault( - mods: Seq[Modifier[SvgElement]], - default: Modifier[SvgElement] - ): Modifier[SvgElement] = - if mods.isEmpty then default else mods - - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-8 w-8"), - 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" - ) - ) - ) - - override def close(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-2 w-2"), - stroke := "currentColor", - fill := "none", - viewBox := "0 0 8 8", - path( - strokeLineCap := "round", - strokeWidth := "1.5", - d := "M1 1l6 6m0-6L1 7" - ) - ) - - override def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 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 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") - ) - ) - - override def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 text-gray-400")), - 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") - ) - ) - - override def `document-chart-bar-outline`( - mods: Modifier[SvgElement]* - ): SvgElement = - svg( - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - strokeWidth := "1.5", - stroke := "currentColor", - withDefault(mods, cls := "h-6 w-6"), - path( - strokeLineCap := "round", - strokeLineJoin := "round", - d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" - ) - ) - - override def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-5 w-5"), - cls := "flex-shrink-0 text-gray-400", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", - clipRule := "evenodd" - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala index a2c637c..bfa063a 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala @@ -1,13 +1,11 @@ package works.iterative.ui.components.laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorDef import com.raquo.laminar.tags.HtmlTag case class Bin[Source, +Value]( label: String, description: Option[String | HtmlElement], - color: ColorDef, valueOf: Source => Value ): def map[NewValue]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala deleted file mode 100644 index 3aac0bf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala +++ /dev/null @@ -1,34 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ButtonComponentsModule: - def buttons: ButtonComponents - - trait ButtonComponents: - def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala deleted file mode 100644 index 646e73d..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala +++ /dev/null @@ -1,191 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.color.ColorDef -import com.raquo.laminar.tags.HtmlTag -import org.scalajs.dom.html.UList -import com.raquo.laminar.nodes.ReactiveHtmlElement -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind - -trait DashboardComponentsModule: - def dashboard: DashboardComponents - - // Only methods that create HtmlElements - trait DashboardComponents(using ComponentContext): - def section(label: String, children: Modifier[HtmlElement]): Div - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI - def cardInitials( - initials: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div - def count(label: String, value: Int, color: ColorDef): Span - def counts(children: HtmlElement*): Span - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - -trait DefaultDashboardComponentsModule(using ComponentContext) - extends DashboardComponentsModule: - override val dashboard: DashboardComponents = new DashboardComponents: - def section(label: String, children: Modifier[HtmlElement]): Div = - div( - cls("mt-3"), - h2( - cls("text-gray-500 text-xs font-medium uppercase tracking-wide"), - label - ), - children - ) - - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] = - ul( - cls( - "mt-3 grid gap-5 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" - ), - children - ) - - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI = - li( - div( - cls("col-span-1 flex shadow-sm rounded-md"), - initials, - div( - cls( - "flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate" - ), - div( - cls("flex-1 px-4 py-2 text-sm truncate"), - div( - cls("text-gray-900 font-medium hover:text-gray-600 truncate"), - label - ), - p(cls("text-gray-500"), children) - ) - ) - ) - ) - - def cardInitials( - id: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div = - div( - cls( - "flex-shrink-0 flex flex-col items-center justify-center w-16 text-white text-sm font-medium rounded-l-md" - ), - color.map(_(600).bg), - div(id), - children - ) - - def count(label: String, value: Int, color: ColorDef): Span = - span(color.text, title(label), s"${value}") - - def counts(children: HtmlElement*): Span = - span(interleave(children, span(" / "))) - - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div = - div( - cls("flex flex-col mt-2"), - div( - cls( - "align-middle min-w-full shadow sm:rounded-lg" - ), - L.table( - cls("min-w-full border-separate border-spacing-0"), - styleAttr("border-spacing: 0"), // Tailwind somehow doesn't work - thead( - tr( - cls("border-t border-gray-200"), - headers.map(h => - th( - cls( - "sticky top-0 z-10 px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - h - ) - ) - ) - ), - tbody(cls("bg-white divide-y divide-gray-100"), sections) - ) - ) - ) - - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - td( - cls("bg-gray-50 border-gray-200 border-t border-b"), - cls( - "sticky top-14 z-10 bg-gray-50 px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - colSpan(cs), - label - ), - mods - ) - - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - mods, - td( - cls( - "px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900 truncate" - ), - label - ), - children.map(e => - td( - cls( - "whitespace-nowrap px-2 py-2 text-sm text-gray-500" - ), - e - ) - ) - ) - - private def interleave( - a: Seq[HtmlElement], - separator: => HtmlElement - ): Seq[HtmlElement] = - if a.size < 2 then a - else a.init.flatMap(n => Seq(n, separator)) :+ a.last diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala deleted file mode 100644 index a45beaf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.FileRef - -trait DetailComponentsModule: - - def details: DetailComponents - - trait DetailComponents: - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement - - def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement - - def files(fs: List[FileRef]): HtmlElement - - def file(f: FileRef): HtmlElement - -trait DefaultDetailComponentsModule(using ctx: ComponentContext) - extends DetailComponentsModule: - self: IconsModule => - - override val details: DetailComponents = new DetailComponents: - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement = - div( - div( - cls := "px-4 sm:px-0", - h3( - cls := "text-base font-semibold leading-7 text-gray-900", - title - ), - subtitle.map(st => - p( - cls := "mt-1 max-w-2xl text-sm leading-6 text-gray-500", - st - ) - ) - // TODO: actions - ), - div( - cls := "mt-6 border-t border-gray-100", - dl( - cls := "divide-y divide-gray-100", - fields - ) - ) - ) - - override def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0", - dt( - cls := "text-sm font-medium leading-6 text-gray-900", - title - ), - dd( - cls := "mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0", - content - ) - ) - - override def files(fs: List[FileRef]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-100 rounded-md border border-gray-200", - fs.map(file) - ) - - override def file(f: FileRef): HtmlElement = - li( - cls := "flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6", - div( - cls := "flex w-0 flex-1 items-center", - icons.`paper-clip-solid`(), - div( - cls := "ml-4 flex min-w-0 flex-1 gap-2", - span( - cls := "truncate font-medium", - f.name - ), - f.size.map(size => span(cls := "flex-shrink-0 text-gray-400", size)) - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - "Uložit" - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala new file mode 100644 index 0000000..f6c583f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait FormComponents(using ctx: ComponentContext) extends LocalDateSelectModule: + def searchIcon: SvgElement + + def renderLocalDateSelect( + id: String, + labelText: Option[String], + placeholderText: Option[String], + mods: LocalDateSelect => Modifier[HtmlElement] + ): HtmlElement = + div( + labelText, + input( + idAttr(id), + nameAttr(id), + autoComplete("date"), + tpe("date"), + cls( + "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ), + placeholderText.map(placeholder(_)), + mods(localDateSelect) + ) + ) + + def searchField( + id: String, + placeholderText: Option[String] = None + )( + mods: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls := "flex-1 min-w-0", + label(ctx.messages("forms.search.label"), forId(id), cls("sr-only")), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + searchIcon + ), + input( + tpe := "search", + nameAttr := "search", + idAttr := id, + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholderText + .orElse( + ctx.messages + .opt( + s"forms.search.${id}.placeholder", + s"form.search.placeholder" + ) + ) + .map(placeholder(_)), + mods + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala deleted file mode 100644 index 7714490..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala +++ /dev/null @@ -1,82 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -/** Form components - * - * A form is a collection of sections, each of which contains a collection of - * fields. - * - * Form -> * Section -> * Field - * -> * Action - * - * Each Field has an `id`, `label` and `input` element. The `id` is used to - * link the label to the input. - * - * The `input` element can be a simple text input, or a more complex component - * such as a date picker. The caller is expected to make the input work. - */ -trait FormComponentsModule extends LocalDateSelectModule: - def forms: FormComponents - - trait FormComponents: - - /** Layout the sections and actions of a form */ - def form(mods: Modifier[HtmlElement]*)(sections: Modifier[HtmlElement]*)( - actions: Modifier[HtmlElement]* - ): HtmlElement - - /** Section layout, with a title, optional subtitle and any content. */ - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label and any content */ - def field(label: Modifier[HtmlElement])( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label, input and optional help text */ - def field( - id: String, - label: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement - - def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout an inline form element */ - def inlineForm(mods: Modifier[HtmlElement]*): HtmlElement - - /** An input field with a search icon */ - def searchField(id: String, placeholderText: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement - - /** LocalDate input */ - def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala deleted file mode 100644 index 8acbe7f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala +++ /dev/null @@ -1,28 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.Tag - -trait GenericComponentsModule: - - def generic: GenericComponents - - trait GenericComponents: - def tag(name: String, color: ColorKind): HtmlElement - def tag(t: Tag): HtmlElement = tag(t.value, t.color) - -trait DefaultGenericComponentsModule(using ComponentContext) - extends GenericComponentsModule: - override val generic: GenericComponents = new GenericComponents: - override def tag(name: String, color: ColorKind): HtmlElement = - p( - cls( - "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" - ), - color(800).text, - color(100).bg, - name - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala deleted file mode 100644 index 8cfc1d9..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala +++ /dev/null @@ -1,123 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.CustomAttrs - -trait IconsModule: - def icons: Icons - - trait Icons: - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement - def close(mods: Modifier[SvgElement]*): SvgElement - def `search-solid`(mods: Modifier[SvgElement]*): SvgElement - def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement - def `document-chart-bar-outline`(mods: Modifier[SvgElement]*): SvgElement - def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement - -trait DefaultIconsModule(using ComponentContext) extends IconsModule: - override val icons: Icons = new Icons: - import svg.* - - object aria: - val hidden = CustomAttrs.svg.ariaHidden - - private def withDefault( - mods: Seq[Modifier[SvgElement]], - default: Modifier[SvgElement] - ): Modifier[SvgElement] = - if mods.isEmpty then default else mods - - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-8 w-8"), - 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" - ) - ) - ) - - override def close(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-2 w-2"), - stroke := "currentColor", - fill := "none", - viewBox := "0 0 8 8", - path( - strokeLineCap := "round", - strokeWidth := "1.5", - d := "M1 1l6 6m0-6L1 7" - ) - ) - - override def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 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 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") - ) - ) - - override def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 text-gray-400")), - 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") - ) - ) - - override def `document-chart-bar-outline`( - mods: Modifier[SvgElement]* - ): SvgElement = - svg( - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - strokeWidth := "1.5", - stroke := "currentColor", - withDefault(mods, cls := "h-6 w-6"), - path( - strokeLineCap := "round", - strokeLineJoin := "round", - d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" - ) - ) - - override def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-5 w-5"), - cls := "flex-shrink-0 text-gray-400", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", - clipRule := "evenodd" - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala deleted file mode 100644 index 95ab654..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait LayoutModule: - def layout: LayoutComponents - - trait LayoutComponents: - def card(content: Modifier[HtmlElement]*): HtmlElement - -trait DefaultLayoutModule(using ctx: ComponentContext) extends LayoutModule: - override val layout: LayoutComponents = new LayoutComponents: - override def card(content: Modifier[HtmlElement]*): HtmlElement = - div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala index a2c637c..bfa063a 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala @@ -1,13 +1,11 @@ package works.iterative.ui.components.laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorDef import com.raquo.laminar.tags.HtmlTag case class Bin[Source, +Value]( label: String, description: Option[String | HtmlElement], - color: ColorDef, valueOf: Source => Value ): def map[NewValue]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala deleted file mode 100644 index 3aac0bf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala +++ /dev/null @@ -1,34 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ButtonComponentsModule: - def buttons: ButtonComponents - - trait ButtonComponents: - def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala deleted file mode 100644 index 646e73d..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala +++ /dev/null @@ -1,191 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.color.ColorDef -import com.raquo.laminar.tags.HtmlTag -import org.scalajs.dom.html.UList -import com.raquo.laminar.nodes.ReactiveHtmlElement -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind - -trait DashboardComponentsModule: - def dashboard: DashboardComponents - - // Only methods that create HtmlElements - trait DashboardComponents(using ComponentContext): - def section(label: String, children: Modifier[HtmlElement]): Div - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI - def cardInitials( - initials: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div - def count(label: String, value: Int, color: ColorDef): Span - def counts(children: HtmlElement*): Span - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - -trait DefaultDashboardComponentsModule(using ComponentContext) - extends DashboardComponentsModule: - override val dashboard: DashboardComponents = new DashboardComponents: - def section(label: String, children: Modifier[HtmlElement]): Div = - div( - cls("mt-3"), - h2( - cls("text-gray-500 text-xs font-medium uppercase tracking-wide"), - label - ), - children - ) - - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] = - ul( - cls( - "mt-3 grid gap-5 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" - ), - children - ) - - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI = - li( - div( - cls("col-span-1 flex shadow-sm rounded-md"), - initials, - div( - cls( - "flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate" - ), - div( - cls("flex-1 px-4 py-2 text-sm truncate"), - div( - cls("text-gray-900 font-medium hover:text-gray-600 truncate"), - label - ), - p(cls("text-gray-500"), children) - ) - ) - ) - ) - - def cardInitials( - id: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div = - div( - cls( - "flex-shrink-0 flex flex-col items-center justify-center w-16 text-white text-sm font-medium rounded-l-md" - ), - color.map(_(600).bg), - div(id), - children - ) - - def count(label: String, value: Int, color: ColorDef): Span = - span(color.text, title(label), s"${value}") - - def counts(children: HtmlElement*): Span = - span(interleave(children, span(" / "))) - - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div = - div( - cls("flex flex-col mt-2"), - div( - cls( - "align-middle min-w-full shadow sm:rounded-lg" - ), - L.table( - cls("min-w-full border-separate border-spacing-0"), - styleAttr("border-spacing: 0"), // Tailwind somehow doesn't work - thead( - tr( - cls("border-t border-gray-200"), - headers.map(h => - th( - cls( - "sticky top-0 z-10 px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - h - ) - ) - ) - ), - tbody(cls("bg-white divide-y divide-gray-100"), sections) - ) - ) - ) - - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - td( - cls("bg-gray-50 border-gray-200 border-t border-b"), - cls( - "sticky top-14 z-10 bg-gray-50 px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - colSpan(cs), - label - ), - mods - ) - - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - mods, - td( - cls( - "px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900 truncate" - ), - label - ), - children.map(e => - td( - cls( - "whitespace-nowrap px-2 py-2 text-sm text-gray-500" - ), - e - ) - ) - ) - - private def interleave( - a: Seq[HtmlElement], - separator: => HtmlElement - ): Seq[HtmlElement] = - if a.size < 2 then a - else a.init.flatMap(n => Seq(n, separator)) :+ a.last diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala deleted file mode 100644 index a45beaf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.FileRef - -trait DetailComponentsModule: - - def details: DetailComponents - - trait DetailComponents: - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement - - def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement - - def files(fs: List[FileRef]): HtmlElement - - def file(f: FileRef): HtmlElement - -trait DefaultDetailComponentsModule(using ctx: ComponentContext) - extends DetailComponentsModule: - self: IconsModule => - - override val details: DetailComponents = new DetailComponents: - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement = - div( - div( - cls := "px-4 sm:px-0", - h3( - cls := "text-base font-semibold leading-7 text-gray-900", - title - ), - subtitle.map(st => - p( - cls := "mt-1 max-w-2xl text-sm leading-6 text-gray-500", - st - ) - ) - // TODO: actions - ), - div( - cls := "mt-6 border-t border-gray-100", - dl( - cls := "divide-y divide-gray-100", - fields - ) - ) - ) - - override def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0", - dt( - cls := "text-sm font-medium leading-6 text-gray-900", - title - ), - dd( - cls := "mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0", - content - ) - ) - - override def files(fs: List[FileRef]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-100 rounded-md border border-gray-200", - fs.map(file) - ) - - override def file(f: FileRef): HtmlElement = - li( - cls := "flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6", - div( - cls := "flex w-0 flex-1 items-center", - icons.`paper-clip-solid`(), - div( - cls := "ml-4 flex min-w-0 flex-1 gap-2", - span( - cls := "truncate font-medium", - f.name - ), - f.size.map(size => span(cls := "flex-shrink-0 text-gray-400", size)) - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - "Uložit" - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala new file mode 100644 index 0000000..f6c583f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait FormComponents(using ctx: ComponentContext) extends LocalDateSelectModule: + def searchIcon: SvgElement + + def renderLocalDateSelect( + id: String, + labelText: Option[String], + placeholderText: Option[String], + mods: LocalDateSelect => Modifier[HtmlElement] + ): HtmlElement = + div( + labelText, + input( + idAttr(id), + nameAttr(id), + autoComplete("date"), + tpe("date"), + cls( + "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ), + placeholderText.map(placeholder(_)), + mods(localDateSelect) + ) + ) + + def searchField( + id: String, + placeholderText: Option[String] = None + )( + mods: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls := "flex-1 min-w-0", + label(ctx.messages("forms.search.label"), forId(id), cls("sr-only")), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + searchIcon + ), + input( + tpe := "search", + nameAttr := "search", + idAttr := id, + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholderText + .orElse( + ctx.messages + .opt( + s"forms.search.${id}.placeholder", + s"form.search.placeholder" + ) + ) + .map(placeholder(_)), + mods + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala deleted file mode 100644 index 7714490..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala +++ /dev/null @@ -1,82 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -/** Form components - * - * A form is a collection of sections, each of which contains a collection of - * fields. - * - * Form -> * Section -> * Field - * -> * Action - * - * Each Field has an `id`, `label` and `input` element. The `id` is used to - * link the label to the input. - * - * The `input` element can be a simple text input, or a more complex component - * such as a date picker. The caller is expected to make the input work. - */ -trait FormComponentsModule extends LocalDateSelectModule: - def forms: FormComponents - - trait FormComponents: - - /** Layout the sections and actions of a form */ - def form(mods: Modifier[HtmlElement]*)(sections: Modifier[HtmlElement]*)( - actions: Modifier[HtmlElement]* - ): HtmlElement - - /** Section layout, with a title, optional subtitle and any content. */ - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label and any content */ - def field(label: Modifier[HtmlElement])( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label, input and optional help text */ - def field( - id: String, - label: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement - - def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout an inline form element */ - def inlineForm(mods: Modifier[HtmlElement]*): HtmlElement - - /** An input field with a search icon */ - def searchField(id: String, placeholderText: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement - - /** LocalDate input */ - def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala deleted file mode 100644 index 8acbe7f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala +++ /dev/null @@ -1,28 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.Tag - -trait GenericComponentsModule: - - def generic: GenericComponents - - trait GenericComponents: - def tag(name: String, color: ColorKind): HtmlElement - def tag(t: Tag): HtmlElement = tag(t.value, t.color) - -trait DefaultGenericComponentsModule(using ComponentContext) - extends GenericComponentsModule: - override val generic: GenericComponents = new GenericComponents: - override def tag(name: String, color: ColorKind): HtmlElement = - p( - cls( - "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" - ), - color(800).text, - color(100).bg, - name - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala deleted file mode 100644 index 8cfc1d9..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala +++ /dev/null @@ -1,123 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.CustomAttrs - -trait IconsModule: - def icons: Icons - - trait Icons: - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement - def close(mods: Modifier[SvgElement]*): SvgElement - def `search-solid`(mods: Modifier[SvgElement]*): SvgElement - def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement - def `document-chart-bar-outline`(mods: Modifier[SvgElement]*): SvgElement - def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement - -trait DefaultIconsModule(using ComponentContext) extends IconsModule: - override val icons: Icons = new Icons: - import svg.* - - object aria: - val hidden = CustomAttrs.svg.ariaHidden - - private def withDefault( - mods: Seq[Modifier[SvgElement]], - default: Modifier[SvgElement] - ): Modifier[SvgElement] = - if mods.isEmpty then default else mods - - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-8 w-8"), - 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" - ) - ) - ) - - override def close(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-2 w-2"), - stroke := "currentColor", - fill := "none", - viewBox := "0 0 8 8", - path( - strokeLineCap := "round", - strokeWidth := "1.5", - d := "M1 1l6 6m0-6L1 7" - ) - ) - - override def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 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 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") - ) - ) - - override def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 text-gray-400")), - 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") - ) - ) - - override def `document-chart-bar-outline`( - mods: Modifier[SvgElement]* - ): SvgElement = - svg( - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - strokeWidth := "1.5", - stroke := "currentColor", - withDefault(mods, cls := "h-6 w-6"), - path( - strokeLineCap := "round", - strokeLineJoin := "round", - d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" - ) - ) - - override def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-5 w-5"), - cls := "flex-shrink-0 text-gray-400", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", - clipRule := "evenodd" - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala deleted file mode 100644 index 95ab654..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait LayoutModule: - def layout: LayoutComponents - - trait LayoutComponents: - def card(content: Modifier[HtmlElement]*): HtmlElement - -trait DefaultLayoutModule(using ctx: ComponentContext) extends LayoutModule: - override val layout: LayoutComponents = new LayoutComponents: - override def card(content: Modifier[HtmlElement]*): HtmlElement = - div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala deleted file mode 100644 index 5946974..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom.html.Paragraph -import com.raquo.laminar.nodes.TextNode - -trait ListComponentsModule: - - def list: ListComponents - - trait ListComponents(using ComponentContext): - def label(text: String, color: ColorKind): HtmlElement - def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI - def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] - def listSection(header: String, list: HtmlElement): Div - def navigation(sections: Modifier[HtmlElement]): HtmlElement - -trait DefaultListComponentsModule(using ComponentContext) - extends ListComponentsModule: - self: GenericComponentsModule => - - override val list: ListComponents = new ListComponents: - override def label( - text: String, - color: ColorKind - ): HtmlElement = generic.tag(text, color) - - override def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI = - li( - cls("group"), - div( - cls( - "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" - ), - avatar.map(a => - div( - cls("flex-shrink-0"), - div( - cls( - "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" - ), - a - ) - ) - ), - div( - cls("flex-1 min-w-0"), - p( - cls("text-sm font-medium text-gray-900"), - title, - span(cls("float-right"), right) - ), - subtitle.map(st => - p( - cls("text-sm text-gray-500 truncate"), - st - ) - ) - ) - ) - ) - - override def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = - ul( - cls("relative z-0 divide-y divide-gray-200"), - role("list"), - children - ) - - override def listSection( - header: String, - list: HtmlElement - ): Div = - div( - cls("relative"), - div( - cls( - "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" - ), - header - ), - list - ) - - override def navigation(sections: Modifier[HtmlElement]): HtmlElement = - navTag( - cls("flex-1 min-h-0 overflow-y-auto"), - sections - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala index a2c637c..bfa063a 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala @@ -1,13 +1,11 @@ package works.iterative.ui.components.laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorDef import com.raquo.laminar.tags.HtmlTag case class Bin[Source, +Value]( label: String, description: Option[String | HtmlElement], - color: ColorDef, valueOf: Source => Value ): def map[NewValue]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala deleted file mode 100644 index 3aac0bf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala +++ /dev/null @@ -1,34 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ButtonComponentsModule: - def buttons: ButtonComponents - - trait ButtonComponents: - def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala deleted file mode 100644 index 646e73d..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala +++ /dev/null @@ -1,191 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.color.ColorDef -import com.raquo.laminar.tags.HtmlTag -import org.scalajs.dom.html.UList -import com.raquo.laminar.nodes.ReactiveHtmlElement -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind - -trait DashboardComponentsModule: - def dashboard: DashboardComponents - - // Only methods that create HtmlElements - trait DashboardComponents(using ComponentContext): - def section(label: String, children: Modifier[HtmlElement]): Div - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI - def cardInitials( - initials: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div - def count(label: String, value: Int, color: ColorDef): Span - def counts(children: HtmlElement*): Span - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - -trait DefaultDashboardComponentsModule(using ComponentContext) - extends DashboardComponentsModule: - override val dashboard: DashboardComponents = new DashboardComponents: - def section(label: String, children: Modifier[HtmlElement]): Div = - div( - cls("mt-3"), - h2( - cls("text-gray-500 text-xs font-medium uppercase tracking-wide"), - label - ), - children - ) - - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] = - ul( - cls( - "mt-3 grid gap-5 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" - ), - children - ) - - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI = - li( - div( - cls("col-span-1 flex shadow-sm rounded-md"), - initials, - div( - cls( - "flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate" - ), - div( - cls("flex-1 px-4 py-2 text-sm truncate"), - div( - cls("text-gray-900 font-medium hover:text-gray-600 truncate"), - label - ), - p(cls("text-gray-500"), children) - ) - ) - ) - ) - - def cardInitials( - id: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div = - div( - cls( - "flex-shrink-0 flex flex-col items-center justify-center w-16 text-white text-sm font-medium rounded-l-md" - ), - color.map(_(600).bg), - div(id), - children - ) - - def count(label: String, value: Int, color: ColorDef): Span = - span(color.text, title(label), s"${value}") - - def counts(children: HtmlElement*): Span = - span(interleave(children, span(" / "))) - - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div = - div( - cls("flex flex-col mt-2"), - div( - cls( - "align-middle min-w-full shadow sm:rounded-lg" - ), - L.table( - cls("min-w-full border-separate border-spacing-0"), - styleAttr("border-spacing: 0"), // Tailwind somehow doesn't work - thead( - tr( - cls("border-t border-gray-200"), - headers.map(h => - th( - cls( - "sticky top-0 z-10 px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - h - ) - ) - ) - ), - tbody(cls("bg-white divide-y divide-gray-100"), sections) - ) - ) - ) - - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - td( - cls("bg-gray-50 border-gray-200 border-t border-b"), - cls( - "sticky top-14 z-10 bg-gray-50 px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - colSpan(cs), - label - ), - mods - ) - - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - mods, - td( - cls( - "px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900 truncate" - ), - label - ), - children.map(e => - td( - cls( - "whitespace-nowrap px-2 py-2 text-sm text-gray-500" - ), - e - ) - ) - ) - - private def interleave( - a: Seq[HtmlElement], - separator: => HtmlElement - ): Seq[HtmlElement] = - if a.size < 2 then a - else a.init.flatMap(n => Seq(n, separator)) :+ a.last diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala deleted file mode 100644 index a45beaf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.FileRef - -trait DetailComponentsModule: - - def details: DetailComponents - - trait DetailComponents: - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement - - def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement - - def files(fs: List[FileRef]): HtmlElement - - def file(f: FileRef): HtmlElement - -trait DefaultDetailComponentsModule(using ctx: ComponentContext) - extends DetailComponentsModule: - self: IconsModule => - - override val details: DetailComponents = new DetailComponents: - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement = - div( - div( - cls := "px-4 sm:px-0", - h3( - cls := "text-base font-semibold leading-7 text-gray-900", - title - ), - subtitle.map(st => - p( - cls := "mt-1 max-w-2xl text-sm leading-6 text-gray-500", - st - ) - ) - // TODO: actions - ), - div( - cls := "mt-6 border-t border-gray-100", - dl( - cls := "divide-y divide-gray-100", - fields - ) - ) - ) - - override def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0", - dt( - cls := "text-sm font-medium leading-6 text-gray-900", - title - ), - dd( - cls := "mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0", - content - ) - ) - - override def files(fs: List[FileRef]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-100 rounded-md border border-gray-200", - fs.map(file) - ) - - override def file(f: FileRef): HtmlElement = - li( - cls := "flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6", - div( - cls := "flex w-0 flex-1 items-center", - icons.`paper-clip-solid`(), - div( - cls := "ml-4 flex min-w-0 flex-1 gap-2", - span( - cls := "truncate font-medium", - f.name - ), - f.size.map(size => span(cls := "flex-shrink-0 text-gray-400", size)) - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - "Uložit" - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala new file mode 100644 index 0000000..f6c583f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait FormComponents(using ctx: ComponentContext) extends LocalDateSelectModule: + def searchIcon: SvgElement + + def renderLocalDateSelect( + id: String, + labelText: Option[String], + placeholderText: Option[String], + mods: LocalDateSelect => Modifier[HtmlElement] + ): HtmlElement = + div( + labelText, + input( + idAttr(id), + nameAttr(id), + autoComplete("date"), + tpe("date"), + cls( + "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ), + placeholderText.map(placeholder(_)), + mods(localDateSelect) + ) + ) + + def searchField( + id: String, + placeholderText: Option[String] = None + )( + mods: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls := "flex-1 min-w-0", + label(ctx.messages("forms.search.label"), forId(id), cls("sr-only")), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + searchIcon + ), + input( + tpe := "search", + nameAttr := "search", + idAttr := id, + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholderText + .orElse( + ctx.messages + .opt( + s"forms.search.${id}.placeholder", + s"form.search.placeholder" + ) + ) + .map(placeholder(_)), + mods + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala deleted file mode 100644 index 7714490..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala +++ /dev/null @@ -1,82 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -/** Form components - * - * A form is a collection of sections, each of which contains a collection of - * fields. - * - * Form -> * Section -> * Field - * -> * Action - * - * Each Field has an `id`, `label` and `input` element. The `id` is used to - * link the label to the input. - * - * The `input` element can be a simple text input, or a more complex component - * such as a date picker. The caller is expected to make the input work. - */ -trait FormComponentsModule extends LocalDateSelectModule: - def forms: FormComponents - - trait FormComponents: - - /** Layout the sections and actions of a form */ - def form(mods: Modifier[HtmlElement]*)(sections: Modifier[HtmlElement]*)( - actions: Modifier[HtmlElement]* - ): HtmlElement - - /** Section layout, with a title, optional subtitle and any content. */ - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label and any content */ - def field(label: Modifier[HtmlElement])( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label, input and optional help text */ - def field( - id: String, - label: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement - - def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout an inline form element */ - def inlineForm(mods: Modifier[HtmlElement]*): HtmlElement - - /** An input field with a search icon */ - def searchField(id: String, placeholderText: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement - - /** LocalDate input */ - def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala deleted file mode 100644 index 8acbe7f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala +++ /dev/null @@ -1,28 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.Tag - -trait GenericComponentsModule: - - def generic: GenericComponents - - trait GenericComponents: - def tag(name: String, color: ColorKind): HtmlElement - def tag(t: Tag): HtmlElement = tag(t.value, t.color) - -trait DefaultGenericComponentsModule(using ComponentContext) - extends GenericComponentsModule: - override val generic: GenericComponents = new GenericComponents: - override def tag(name: String, color: ColorKind): HtmlElement = - p( - cls( - "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" - ), - color(800).text, - color(100).bg, - name - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala deleted file mode 100644 index 8cfc1d9..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala +++ /dev/null @@ -1,123 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.CustomAttrs - -trait IconsModule: - def icons: Icons - - trait Icons: - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement - def close(mods: Modifier[SvgElement]*): SvgElement - def `search-solid`(mods: Modifier[SvgElement]*): SvgElement - def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement - def `document-chart-bar-outline`(mods: Modifier[SvgElement]*): SvgElement - def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement - -trait DefaultIconsModule(using ComponentContext) extends IconsModule: - override val icons: Icons = new Icons: - import svg.* - - object aria: - val hidden = CustomAttrs.svg.ariaHidden - - private def withDefault( - mods: Seq[Modifier[SvgElement]], - default: Modifier[SvgElement] - ): Modifier[SvgElement] = - if mods.isEmpty then default else mods - - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-8 w-8"), - 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" - ) - ) - ) - - override def close(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-2 w-2"), - stroke := "currentColor", - fill := "none", - viewBox := "0 0 8 8", - path( - strokeLineCap := "round", - strokeWidth := "1.5", - d := "M1 1l6 6m0-6L1 7" - ) - ) - - override def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 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 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") - ) - ) - - override def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 text-gray-400")), - 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") - ) - ) - - override def `document-chart-bar-outline`( - mods: Modifier[SvgElement]* - ): SvgElement = - svg( - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - strokeWidth := "1.5", - stroke := "currentColor", - withDefault(mods, cls := "h-6 w-6"), - path( - strokeLineCap := "round", - strokeLineJoin := "round", - d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" - ) - ) - - override def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-5 w-5"), - cls := "flex-shrink-0 text-gray-400", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", - clipRule := "evenodd" - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala deleted file mode 100644 index 95ab654..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait LayoutModule: - def layout: LayoutComponents - - trait LayoutComponents: - def card(content: Modifier[HtmlElement]*): HtmlElement - -trait DefaultLayoutModule(using ctx: ComponentContext) extends LayoutModule: - override val layout: LayoutComponents = new LayoutComponents: - override def card(content: Modifier[HtmlElement]*): HtmlElement = - div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala deleted file mode 100644 index 5946974..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom.html.Paragraph -import com.raquo.laminar.nodes.TextNode - -trait ListComponentsModule: - - def list: ListComponents - - trait ListComponents(using ComponentContext): - def label(text: String, color: ColorKind): HtmlElement - def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI - def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] - def listSection(header: String, list: HtmlElement): Div - def navigation(sections: Modifier[HtmlElement]): HtmlElement - -trait DefaultListComponentsModule(using ComponentContext) - extends ListComponentsModule: - self: GenericComponentsModule => - - override val list: ListComponents = new ListComponents: - override def label( - text: String, - color: ColorKind - ): HtmlElement = generic.tag(text, color) - - override def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI = - li( - cls("group"), - div( - cls( - "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" - ), - avatar.map(a => - div( - cls("flex-shrink-0"), - div( - cls( - "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" - ), - a - ) - ) - ), - div( - cls("flex-1 min-w-0"), - p( - cls("text-sm font-medium text-gray-900"), - title, - span(cls("float-right"), right) - ), - subtitle.map(st => - p( - cls("text-sm text-gray-500 truncate"), - st - ) - ) - ) - ) - ) - - override def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = - ul( - cls("relative z-0 divide-y divide-gray-200"), - role("list"), - children - ) - - override def listSection( - header: String, - list: HtmlElement - ): Div = - div( - cls("relative"), - div( - cls( - "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" - ), - header - ), - list - ) - - override def navigation(sections: Modifier[HtmlElement]): HtmlElement = - navTag( - cls("flex-1 min-h-0 overflow-y-auto"), - sections - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala deleted file mode 100644 index 5b5490f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ModalComponentsModule: - - def modal: ModalComponents - - trait ModalComponents: - def modalDialog( - content: HtmlElement, - onClose: Modifier[HtmlElement] - ): HtmlElement - -trait DefaultModalComponentsModule(using ctx: ComponentContext) - extends ModalComponentsModule: - - override val modal: ModalComponents = new ModalComponents: - - override def modalDialog( - content: HtmlElement, - closeMod: Modifier[HtmlElement] - ): 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( - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")), - closeMod - ) - ) - - div( - cls("fixed inset-0 z-20 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - 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-7xl sm:align-middle" - ), - content - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala index a2c637c..bfa063a 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala @@ -1,13 +1,11 @@ package works.iterative.ui.components.laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorDef import com.raquo.laminar.tags.HtmlTag case class Bin[Source, +Value]( label: String, description: Option[String | HtmlElement], - color: ColorDef, valueOf: Source => Value ): def map[NewValue]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala deleted file mode 100644 index 3aac0bf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala +++ /dev/null @@ -1,34 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ButtonComponentsModule: - def buttons: ButtonComponents - - trait ButtonComponents: - def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala deleted file mode 100644 index 646e73d..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala +++ /dev/null @@ -1,191 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.color.ColorDef -import com.raquo.laminar.tags.HtmlTag -import org.scalajs.dom.html.UList -import com.raquo.laminar.nodes.ReactiveHtmlElement -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind - -trait DashboardComponentsModule: - def dashboard: DashboardComponents - - // Only methods that create HtmlElements - trait DashboardComponents(using ComponentContext): - def section(label: String, children: Modifier[HtmlElement]): Div - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI - def cardInitials( - initials: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div - def count(label: String, value: Int, color: ColorDef): Span - def counts(children: HtmlElement*): Span - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - -trait DefaultDashboardComponentsModule(using ComponentContext) - extends DashboardComponentsModule: - override val dashboard: DashboardComponents = new DashboardComponents: - def section(label: String, children: Modifier[HtmlElement]): Div = - div( - cls("mt-3"), - h2( - cls("text-gray-500 text-xs font-medium uppercase tracking-wide"), - label - ), - children - ) - - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] = - ul( - cls( - "mt-3 grid gap-5 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" - ), - children - ) - - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI = - li( - div( - cls("col-span-1 flex shadow-sm rounded-md"), - initials, - div( - cls( - "flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate" - ), - div( - cls("flex-1 px-4 py-2 text-sm truncate"), - div( - cls("text-gray-900 font-medium hover:text-gray-600 truncate"), - label - ), - p(cls("text-gray-500"), children) - ) - ) - ) - ) - - def cardInitials( - id: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div = - div( - cls( - "flex-shrink-0 flex flex-col items-center justify-center w-16 text-white text-sm font-medium rounded-l-md" - ), - color.map(_(600).bg), - div(id), - children - ) - - def count(label: String, value: Int, color: ColorDef): Span = - span(color.text, title(label), s"${value}") - - def counts(children: HtmlElement*): Span = - span(interleave(children, span(" / "))) - - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div = - div( - cls("flex flex-col mt-2"), - div( - cls( - "align-middle min-w-full shadow sm:rounded-lg" - ), - L.table( - cls("min-w-full border-separate border-spacing-0"), - styleAttr("border-spacing: 0"), // Tailwind somehow doesn't work - thead( - tr( - cls("border-t border-gray-200"), - headers.map(h => - th( - cls( - "sticky top-0 z-10 px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - h - ) - ) - ) - ), - tbody(cls("bg-white divide-y divide-gray-100"), sections) - ) - ) - ) - - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - td( - cls("bg-gray-50 border-gray-200 border-t border-b"), - cls( - "sticky top-14 z-10 bg-gray-50 px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - colSpan(cs), - label - ), - mods - ) - - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - mods, - td( - cls( - "px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900 truncate" - ), - label - ), - children.map(e => - td( - cls( - "whitespace-nowrap px-2 py-2 text-sm text-gray-500" - ), - e - ) - ) - ) - - private def interleave( - a: Seq[HtmlElement], - separator: => HtmlElement - ): Seq[HtmlElement] = - if a.size < 2 then a - else a.init.flatMap(n => Seq(n, separator)) :+ a.last diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala deleted file mode 100644 index a45beaf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.FileRef - -trait DetailComponentsModule: - - def details: DetailComponents - - trait DetailComponents: - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement - - def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement - - def files(fs: List[FileRef]): HtmlElement - - def file(f: FileRef): HtmlElement - -trait DefaultDetailComponentsModule(using ctx: ComponentContext) - extends DetailComponentsModule: - self: IconsModule => - - override val details: DetailComponents = new DetailComponents: - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement = - div( - div( - cls := "px-4 sm:px-0", - h3( - cls := "text-base font-semibold leading-7 text-gray-900", - title - ), - subtitle.map(st => - p( - cls := "mt-1 max-w-2xl text-sm leading-6 text-gray-500", - st - ) - ) - // TODO: actions - ), - div( - cls := "mt-6 border-t border-gray-100", - dl( - cls := "divide-y divide-gray-100", - fields - ) - ) - ) - - override def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0", - dt( - cls := "text-sm font-medium leading-6 text-gray-900", - title - ), - dd( - cls := "mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0", - content - ) - ) - - override def files(fs: List[FileRef]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-100 rounded-md border border-gray-200", - fs.map(file) - ) - - override def file(f: FileRef): HtmlElement = - li( - cls := "flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6", - div( - cls := "flex w-0 flex-1 items-center", - icons.`paper-clip-solid`(), - div( - cls := "ml-4 flex min-w-0 flex-1 gap-2", - span( - cls := "truncate font-medium", - f.name - ), - f.size.map(size => span(cls := "flex-shrink-0 text-gray-400", size)) - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - "Uložit" - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala new file mode 100644 index 0000000..f6c583f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait FormComponents(using ctx: ComponentContext) extends LocalDateSelectModule: + def searchIcon: SvgElement + + def renderLocalDateSelect( + id: String, + labelText: Option[String], + placeholderText: Option[String], + mods: LocalDateSelect => Modifier[HtmlElement] + ): HtmlElement = + div( + labelText, + input( + idAttr(id), + nameAttr(id), + autoComplete("date"), + tpe("date"), + cls( + "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ), + placeholderText.map(placeholder(_)), + mods(localDateSelect) + ) + ) + + def searchField( + id: String, + placeholderText: Option[String] = None + )( + mods: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls := "flex-1 min-w-0", + label(ctx.messages("forms.search.label"), forId(id), cls("sr-only")), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + searchIcon + ), + input( + tpe := "search", + nameAttr := "search", + idAttr := id, + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholderText + .orElse( + ctx.messages + .opt( + s"forms.search.${id}.placeholder", + s"form.search.placeholder" + ) + ) + .map(placeholder(_)), + mods + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala deleted file mode 100644 index 7714490..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala +++ /dev/null @@ -1,82 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -/** Form components - * - * A form is a collection of sections, each of which contains a collection of - * fields. - * - * Form -> * Section -> * Field - * -> * Action - * - * Each Field has an `id`, `label` and `input` element. The `id` is used to - * link the label to the input. - * - * The `input` element can be a simple text input, or a more complex component - * such as a date picker. The caller is expected to make the input work. - */ -trait FormComponentsModule extends LocalDateSelectModule: - def forms: FormComponents - - trait FormComponents: - - /** Layout the sections and actions of a form */ - def form(mods: Modifier[HtmlElement]*)(sections: Modifier[HtmlElement]*)( - actions: Modifier[HtmlElement]* - ): HtmlElement - - /** Section layout, with a title, optional subtitle and any content. */ - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label and any content */ - def field(label: Modifier[HtmlElement])( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label, input and optional help text */ - def field( - id: String, - label: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement - - def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout an inline form element */ - def inlineForm(mods: Modifier[HtmlElement]*): HtmlElement - - /** An input field with a search icon */ - def searchField(id: String, placeholderText: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement - - /** LocalDate input */ - def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala deleted file mode 100644 index 8acbe7f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala +++ /dev/null @@ -1,28 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.Tag - -trait GenericComponentsModule: - - def generic: GenericComponents - - trait GenericComponents: - def tag(name: String, color: ColorKind): HtmlElement - def tag(t: Tag): HtmlElement = tag(t.value, t.color) - -trait DefaultGenericComponentsModule(using ComponentContext) - extends GenericComponentsModule: - override val generic: GenericComponents = new GenericComponents: - override def tag(name: String, color: ColorKind): HtmlElement = - p( - cls( - "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" - ), - color(800).text, - color(100).bg, - name - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala deleted file mode 100644 index 8cfc1d9..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala +++ /dev/null @@ -1,123 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.CustomAttrs - -trait IconsModule: - def icons: Icons - - trait Icons: - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement - def close(mods: Modifier[SvgElement]*): SvgElement - def `search-solid`(mods: Modifier[SvgElement]*): SvgElement - def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement - def `document-chart-bar-outline`(mods: Modifier[SvgElement]*): SvgElement - def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement - -trait DefaultIconsModule(using ComponentContext) extends IconsModule: - override val icons: Icons = new Icons: - import svg.* - - object aria: - val hidden = CustomAttrs.svg.ariaHidden - - private def withDefault( - mods: Seq[Modifier[SvgElement]], - default: Modifier[SvgElement] - ): Modifier[SvgElement] = - if mods.isEmpty then default else mods - - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-8 w-8"), - 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" - ) - ) - ) - - override def close(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-2 w-2"), - stroke := "currentColor", - fill := "none", - viewBox := "0 0 8 8", - path( - strokeLineCap := "round", - strokeWidth := "1.5", - d := "M1 1l6 6m0-6L1 7" - ) - ) - - override def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 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 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") - ) - ) - - override def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 text-gray-400")), - 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") - ) - ) - - override def `document-chart-bar-outline`( - mods: Modifier[SvgElement]* - ): SvgElement = - svg( - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - strokeWidth := "1.5", - stroke := "currentColor", - withDefault(mods, cls := "h-6 w-6"), - path( - strokeLineCap := "round", - strokeLineJoin := "round", - d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" - ) - ) - - override def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-5 w-5"), - cls := "flex-shrink-0 text-gray-400", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", - clipRule := "evenodd" - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala deleted file mode 100644 index 95ab654..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait LayoutModule: - def layout: LayoutComponents - - trait LayoutComponents: - def card(content: Modifier[HtmlElement]*): HtmlElement - -trait DefaultLayoutModule(using ctx: ComponentContext) extends LayoutModule: - override val layout: LayoutComponents = new LayoutComponents: - override def card(content: Modifier[HtmlElement]*): HtmlElement = - div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala deleted file mode 100644 index 5946974..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom.html.Paragraph -import com.raquo.laminar.nodes.TextNode - -trait ListComponentsModule: - - def list: ListComponents - - trait ListComponents(using ComponentContext): - def label(text: String, color: ColorKind): HtmlElement - def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI - def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] - def listSection(header: String, list: HtmlElement): Div - def navigation(sections: Modifier[HtmlElement]): HtmlElement - -trait DefaultListComponentsModule(using ComponentContext) - extends ListComponentsModule: - self: GenericComponentsModule => - - override val list: ListComponents = new ListComponents: - override def label( - text: String, - color: ColorKind - ): HtmlElement = generic.tag(text, color) - - override def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI = - li( - cls("group"), - div( - cls( - "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" - ), - avatar.map(a => - div( - cls("flex-shrink-0"), - div( - cls( - "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" - ), - a - ) - ) - ), - div( - cls("flex-1 min-w-0"), - p( - cls("text-sm font-medium text-gray-900"), - title, - span(cls("float-right"), right) - ), - subtitle.map(st => - p( - cls("text-sm text-gray-500 truncate"), - st - ) - ) - ) - ) - ) - - override def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = - ul( - cls("relative z-0 divide-y divide-gray-200"), - role("list"), - children - ) - - override def listSection( - header: String, - list: HtmlElement - ): Div = - div( - cls("relative"), - div( - cls( - "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" - ), - header - ), - list - ) - - override def navigation(sections: Modifier[HtmlElement]): HtmlElement = - navTag( - cls("flex-1 min-h-0 overflow-y-auto"), - sections - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala deleted file mode 100644 index 5b5490f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ModalComponentsModule: - - def modal: ModalComponents - - trait ModalComponents: - def modalDialog( - content: HtmlElement, - onClose: Modifier[HtmlElement] - ): HtmlElement - -trait DefaultModalComponentsModule(using ctx: ComponentContext) - extends ModalComponentsModule: - - override val modal: ModalComponents = new ModalComponents: - - override def modalDialog( - content: HtmlElement, - closeMod: Modifier[HtmlElement] - ): 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( - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")), - closeMod - ) - ) - - div( - cls("fixed inset-0 z-20 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - 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-7xl sm:align-middle" - ), - content - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala deleted file mode 100644 index b713d8a..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala +++ /dev/null @@ -1,71 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait PageComponentsModule: - - val page: PageComponents - - trait PageComponents: - def container( - children: Modifier[HtmlElement]* - ): HtmlElement - - def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement - - def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement] = emptyMod, - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement - - /** Visage for clickable text, like links - */ - def clickable: Modifier[HtmlElement] - -trait DefaultPageComponentsModule(using ComponentContext) - extends PageComponentsModule: - - override val page: PageComponents = new PageComponents: - override def container( - children: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), - children - ) - - override def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement = - div( - cls("p-8 bg-gray-100 h-full"), - header, - children - ) - - override def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement = - div( - cls("pb-5 border-b border-gray-200"), - div(cls("float-right"), right), - h1( - cls("text-2xl leading-6 font-medium text-gray-900"), - title - ), - subtitle.map( - p( - cls("text-sm font-medium text-gray-500"), - _ - ) - ) - ) - - override def clickable: Modifier[HtmlElement] = - cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala index a2c637c..bfa063a 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala @@ -1,13 +1,11 @@ package works.iterative.ui.components.laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorDef import com.raquo.laminar.tags.HtmlTag case class Bin[Source, +Value]( label: String, description: Option[String | HtmlElement], - color: ColorDef, valueOf: Source => Value ): def map[NewValue]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala deleted file mode 100644 index 3aac0bf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala +++ /dev/null @@ -1,34 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ButtonComponentsModule: - def buttons: ButtonComponents - - trait ButtonComponents: - def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala deleted file mode 100644 index 646e73d..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala +++ /dev/null @@ -1,191 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.color.ColorDef -import com.raquo.laminar.tags.HtmlTag -import org.scalajs.dom.html.UList -import com.raquo.laminar.nodes.ReactiveHtmlElement -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind - -trait DashboardComponentsModule: - def dashboard: DashboardComponents - - // Only methods that create HtmlElements - trait DashboardComponents(using ComponentContext): - def section(label: String, children: Modifier[HtmlElement]): Div - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI - def cardInitials( - initials: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div - def count(label: String, value: Int, color: ColorDef): Span - def counts(children: HtmlElement*): Span - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - -trait DefaultDashboardComponentsModule(using ComponentContext) - extends DashboardComponentsModule: - override val dashboard: DashboardComponents = new DashboardComponents: - def section(label: String, children: Modifier[HtmlElement]): Div = - div( - cls("mt-3"), - h2( - cls("text-gray-500 text-xs font-medium uppercase tracking-wide"), - label - ), - children - ) - - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] = - ul( - cls( - "mt-3 grid gap-5 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" - ), - children - ) - - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI = - li( - div( - cls("col-span-1 flex shadow-sm rounded-md"), - initials, - div( - cls( - "flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate" - ), - div( - cls("flex-1 px-4 py-2 text-sm truncate"), - div( - cls("text-gray-900 font-medium hover:text-gray-600 truncate"), - label - ), - p(cls("text-gray-500"), children) - ) - ) - ) - ) - - def cardInitials( - id: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div = - div( - cls( - "flex-shrink-0 flex flex-col items-center justify-center w-16 text-white text-sm font-medium rounded-l-md" - ), - color.map(_(600).bg), - div(id), - children - ) - - def count(label: String, value: Int, color: ColorDef): Span = - span(color.text, title(label), s"${value}") - - def counts(children: HtmlElement*): Span = - span(interleave(children, span(" / "))) - - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div = - div( - cls("flex flex-col mt-2"), - div( - cls( - "align-middle min-w-full shadow sm:rounded-lg" - ), - L.table( - cls("min-w-full border-separate border-spacing-0"), - styleAttr("border-spacing: 0"), // Tailwind somehow doesn't work - thead( - tr( - cls("border-t border-gray-200"), - headers.map(h => - th( - cls( - "sticky top-0 z-10 px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - h - ) - ) - ) - ), - tbody(cls("bg-white divide-y divide-gray-100"), sections) - ) - ) - ) - - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - td( - cls("bg-gray-50 border-gray-200 border-t border-b"), - cls( - "sticky top-14 z-10 bg-gray-50 px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - colSpan(cs), - label - ), - mods - ) - - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - mods, - td( - cls( - "px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900 truncate" - ), - label - ), - children.map(e => - td( - cls( - "whitespace-nowrap px-2 py-2 text-sm text-gray-500" - ), - e - ) - ) - ) - - private def interleave( - a: Seq[HtmlElement], - separator: => HtmlElement - ): Seq[HtmlElement] = - if a.size < 2 then a - else a.init.flatMap(n => Seq(n, separator)) :+ a.last diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala deleted file mode 100644 index a45beaf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.FileRef - -trait DetailComponentsModule: - - def details: DetailComponents - - trait DetailComponents: - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement - - def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement - - def files(fs: List[FileRef]): HtmlElement - - def file(f: FileRef): HtmlElement - -trait DefaultDetailComponentsModule(using ctx: ComponentContext) - extends DetailComponentsModule: - self: IconsModule => - - override val details: DetailComponents = new DetailComponents: - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement = - div( - div( - cls := "px-4 sm:px-0", - h3( - cls := "text-base font-semibold leading-7 text-gray-900", - title - ), - subtitle.map(st => - p( - cls := "mt-1 max-w-2xl text-sm leading-6 text-gray-500", - st - ) - ) - // TODO: actions - ), - div( - cls := "mt-6 border-t border-gray-100", - dl( - cls := "divide-y divide-gray-100", - fields - ) - ) - ) - - override def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0", - dt( - cls := "text-sm font-medium leading-6 text-gray-900", - title - ), - dd( - cls := "mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0", - content - ) - ) - - override def files(fs: List[FileRef]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-100 rounded-md border border-gray-200", - fs.map(file) - ) - - override def file(f: FileRef): HtmlElement = - li( - cls := "flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6", - div( - cls := "flex w-0 flex-1 items-center", - icons.`paper-clip-solid`(), - div( - cls := "ml-4 flex min-w-0 flex-1 gap-2", - span( - cls := "truncate font-medium", - f.name - ), - f.size.map(size => span(cls := "flex-shrink-0 text-gray-400", size)) - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - "Uložit" - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala new file mode 100644 index 0000000..f6c583f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait FormComponents(using ctx: ComponentContext) extends LocalDateSelectModule: + def searchIcon: SvgElement + + def renderLocalDateSelect( + id: String, + labelText: Option[String], + placeholderText: Option[String], + mods: LocalDateSelect => Modifier[HtmlElement] + ): HtmlElement = + div( + labelText, + input( + idAttr(id), + nameAttr(id), + autoComplete("date"), + tpe("date"), + cls( + "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ), + placeholderText.map(placeholder(_)), + mods(localDateSelect) + ) + ) + + def searchField( + id: String, + placeholderText: Option[String] = None + )( + mods: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls := "flex-1 min-w-0", + label(ctx.messages("forms.search.label"), forId(id), cls("sr-only")), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + searchIcon + ), + input( + tpe := "search", + nameAttr := "search", + idAttr := id, + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholderText + .orElse( + ctx.messages + .opt( + s"forms.search.${id}.placeholder", + s"form.search.placeholder" + ) + ) + .map(placeholder(_)), + mods + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala deleted file mode 100644 index 7714490..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala +++ /dev/null @@ -1,82 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -/** Form components - * - * A form is a collection of sections, each of which contains a collection of - * fields. - * - * Form -> * Section -> * Field - * -> * Action - * - * Each Field has an `id`, `label` and `input` element. The `id` is used to - * link the label to the input. - * - * The `input` element can be a simple text input, or a more complex component - * such as a date picker. The caller is expected to make the input work. - */ -trait FormComponentsModule extends LocalDateSelectModule: - def forms: FormComponents - - trait FormComponents: - - /** Layout the sections and actions of a form */ - def form(mods: Modifier[HtmlElement]*)(sections: Modifier[HtmlElement]*)( - actions: Modifier[HtmlElement]* - ): HtmlElement - - /** Section layout, with a title, optional subtitle and any content. */ - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label and any content */ - def field(label: Modifier[HtmlElement])( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label, input and optional help text */ - def field( - id: String, - label: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement - - def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout an inline form element */ - def inlineForm(mods: Modifier[HtmlElement]*): HtmlElement - - /** An input field with a search icon */ - def searchField(id: String, placeholderText: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement - - /** LocalDate input */ - def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala deleted file mode 100644 index 8acbe7f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala +++ /dev/null @@ -1,28 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.Tag - -trait GenericComponentsModule: - - def generic: GenericComponents - - trait GenericComponents: - def tag(name: String, color: ColorKind): HtmlElement - def tag(t: Tag): HtmlElement = tag(t.value, t.color) - -trait DefaultGenericComponentsModule(using ComponentContext) - extends GenericComponentsModule: - override val generic: GenericComponents = new GenericComponents: - override def tag(name: String, color: ColorKind): HtmlElement = - p( - cls( - "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" - ), - color(800).text, - color(100).bg, - name - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala deleted file mode 100644 index 8cfc1d9..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala +++ /dev/null @@ -1,123 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.CustomAttrs - -trait IconsModule: - def icons: Icons - - trait Icons: - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement - def close(mods: Modifier[SvgElement]*): SvgElement - def `search-solid`(mods: Modifier[SvgElement]*): SvgElement - def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement - def `document-chart-bar-outline`(mods: Modifier[SvgElement]*): SvgElement - def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement - -trait DefaultIconsModule(using ComponentContext) extends IconsModule: - override val icons: Icons = new Icons: - import svg.* - - object aria: - val hidden = CustomAttrs.svg.ariaHidden - - private def withDefault( - mods: Seq[Modifier[SvgElement]], - default: Modifier[SvgElement] - ): Modifier[SvgElement] = - if mods.isEmpty then default else mods - - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-8 w-8"), - 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" - ) - ) - ) - - override def close(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-2 w-2"), - stroke := "currentColor", - fill := "none", - viewBox := "0 0 8 8", - path( - strokeLineCap := "round", - strokeWidth := "1.5", - d := "M1 1l6 6m0-6L1 7" - ) - ) - - override def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 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 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") - ) - ) - - override def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 text-gray-400")), - 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") - ) - ) - - override def `document-chart-bar-outline`( - mods: Modifier[SvgElement]* - ): SvgElement = - svg( - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - strokeWidth := "1.5", - stroke := "currentColor", - withDefault(mods, cls := "h-6 w-6"), - path( - strokeLineCap := "round", - strokeLineJoin := "round", - d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" - ) - ) - - override def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-5 w-5"), - cls := "flex-shrink-0 text-gray-400", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", - clipRule := "evenodd" - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala deleted file mode 100644 index 95ab654..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait LayoutModule: - def layout: LayoutComponents - - trait LayoutComponents: - def card(content: Modifier[HtmlElement]*): HtmlElement - -trait DefaultLayoutModule(using ctx: ComponentContext) extends LayoutModule: - override val layout: LayoutComponents = new LayoutComponents: - override def card(content: Modifier[HtmlElement]*): HtmlElement = - div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala deleted file mode 100644 index 5946974..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom.html.Paragraph -import com.raquo.laminar.nodes.TextNode - -trait ListComponentsModule: - - def list: ListComponents - - trait ListComponents(using ComponentContext): - def label(text: String, color: ColorKind): HtmlElement - def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI - def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] - def listSection(header: String, list: HtmlElement): Div - def navigation(sections: Modifier[HtmlElement]): HtmlElement - -trait DefaultListComponentsModule(using ComponentContext) - extends ListComponentsModule: - self: GenericComponentsModule => - - override val list: ListComponents = new ListComponents: - override def label( - text: String, - color: ColorKind - ): HtmlElement = generic.tag(text, color) - - override def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI = - li( - cls("group"), - div( - cls( - "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" - ), - avatar.map(a => - div( - cls("flex-shrink-0"), - div( - cls( - "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" - ), - a - ) - ) - ), - div( - cls("flex-1 min-w-0"), - p( - cls("text-sm font-medium text-gray-900"), - title, - span(cls("float-right"), right) - ), - subtitle.map(st => - p( - cls("text-sm text-gray-500 truncate"), - st - ) - ) - ) - ) - ) - - override def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = - ul( - cls("relative z-0 divide-y divide-gray-200"), - role("list"), - children - ) - - override def listSection( - header: String, - list: HtmlElement - ): Div = - div( - cls("relative"), - div( - cls( - "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" - ), - header - ), - list - ) - - override def navigation(sections: Modifier[HtmlElement]): HtmlElement = - navTag( - cls("flex-1 min-h-0 overflow-y-auto"), - sections - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala deleted file mode 100644 index 5b5490f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ModalComponentsModule: - - def modal: ModalComponents - - trait ModalComponents: - def modalDialog( - content: HtmlElement, - onClose: Modifier[HtmlElement] - ): HtmlElement - -trait DefaultModalComponentsModule(using ctx: ComponentContext) - extends ModalComponentsModule: - - override val modal: ModalComponents = new ModalComponents: - - override def modalDialog( - content: HtmlElement, - closeMod: Modifier[HtmlElement] - ): 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( - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")), - closeMod - ) - ) - - div( - cls("fixed inset-0 z-20 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - 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-7xl sm:align-middle" - ), - content - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala deleted file mode 100644 index b713d8a..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala +++ /dev/null @@ -1,71 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait PageComponentsModule: - - val page: PageComponents - - trait PageComponents: - def container( - children: Modifier[HtmlElement]* - ): HtmlElement - - def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement - - def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement] = emptyMod, - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement - - /** Visage for clickable text, like links - */ - def clickable: Modifier[HtmlElement] - -trait DefaultPageComponentsModule(using ComponentContext) - extends PageComponentsModule: - - override val page: PageComponents = new PageComponents: - override def container( - children: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), - children - ) - - override def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement = - div( - cls("p-8 bg-gray-100 h-full"), - header, - children - ) - - override def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement = - div( - cls("pb-5 border-b border-gray-200"), - div(cls("float-right"), right), - h1( - cls("text-2xl leading-6 font-medium text-gray-900"), - title - ), - subtitle.map( - p( - cls("text-sm font-medium text-gray-500"), - _ - ) - ) - ) - - override def clickable: Modifier[HtmlElement] = - cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala deleted file mode 100644 index f70ee87..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala +++ /dev/null @@ -1,108 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} - -trait TableComponentsModule: - def tables: TableComponents - - trait TableComponents: - def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )(table: Modifier[HtmlElement]*): HtmlElement - - def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement - - def headerRow(mods: Modifier[HtmlElement]*)( - cells: HtmlElement* - ): HtmlElement - - def dataRow(mods: Modifier[HtmlElement]*)(cells: HtmlElement*): HtmlElement - - def headerCell(content: Modifier[HtmlElement]): HtmlElement - - def dataCell(content: Modifier[HtmlElement]): HtmlElement - -trait DefaultTableComponentsModule extends TableComponentsModule: - - override lazy val tables: TableComponents = new TableComponents: - - override def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )( - table: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("px-4 sm:px-6 lg:px-8"), - div( - cls("sm:flex sm:items-center"), - div( - cls("sm:flex-auto"), - h1(cls("text-base font-semibold leading-6 text-gray-900"), title), - subtitle.map(st => p(cls("mt-2 text-sm text-gray-700"), st)) - ), - div(cls("mt-4 sm:ml-16 sm:mt-0 sm:flex-none"), actions) - ), - div( - cls("mt-8 flow-root"), - div( - cls("-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"), - div( - cls("inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"), - table - ) - ) - ) - ) - - override def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement = - table( - cls("min-w-full divide-y divide-gray-300"), - thead(header), - tbody(cls("divide-y divide-gray-200"), body) - ) - - override def headerRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-3.5 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-3.5 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-3.5 px-3")) - ) - ) - - override def dataRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - mods, - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-4 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-4 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-4 px-3")) - ) - ) - - override def headerCell(content: Modifier[HtmlElement]): HtmlElement = - th( - cls("text-left text-sm font-semibold text-gray-900"), - content - ) - - override def dataCell(content: Modifier[HtmlElement]): HtmlElement = - td( - cls("whitespace-nowrap text-sm text-gray-500"), - content - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala index a2c637c..bfa063a 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala @@ -1,13 +1,11 @@ package works.iterative.ui.components.laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorDef import com.raquo.laminar.tags.HtmlTag case class Bin[Source, +Value]( label: String, description: Option[String | HtmlElement], - color: ColorDef, valueOf: Source => Value ): def map[NewValue]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala deleted file mode 100644 index 3aac0bf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala +++ /dev/null @@ -1,34 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ButtonComponentsModule: - def buttons: ButtonComponents - - trait ButtonComponents: - def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala deleted file mode 100644 index 646e73d..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala +++ /dev/null @@ -1,191 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.color.ColorDef -import com.raquo.laminar.tags.HtmlTag -import org.scalajs.dom.html.UList -import com.raquo.laminar.nodes.ReactiveHtmlElement -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind - -trait DashboardComponentsModule: - def dashboard: DashboardComponents - - // Only methods that create HtmlElements - trait DashboardComponents(using ComponentContext): - def section(label: String, children: Modifier[HtmlElement]): Div - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI - def cardInitials( - initials: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div - def count(label: String, value: Int, color: ColorDef): Span - def counts(children: HtmlElement*): Span - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - -trait DefaultDashboardComponentsModule(using ComponentContext) - extends DashboardComponentsModule: - override val dashboard: DashboardComponents = new DashboardComponents: - def section(label: String, children: Modifier[HtmlElement]): Div = - div( - cls("mt-3"), - h2( - cls("text-gray-500 text-xs font-medium uppercase tracking-wide"), - label - ), - children - ) - - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] = - ul( - cls( - "mt-3 grid gap-5 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" - ), - children - ) - - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI = - li( - div( - cls("col-span-1 flex shadow-sm rounded-md"), - initials, - div( - cls( - "flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate" - ), - div( - cls("flex-1 px-4 py-2 text-sm truncate"), - div( - cls("text-gray-900 font-medium hover:text-gray-600 truncate"), - label - ), - p(cls("text-gray-500"), children) - ) - ) - ) - ) - - def cardInitials( - id: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div = - div( - cls( - "flex-shrink-0 flex flex-col items-center justify-center w-16 text-white text-sm font-medium rounded-l-md" - ), - color.map(_(600).bg), - div(id), - children - ) - - def count(label: String, value: Int, color: ColorDef): Span = - span(color.text, title(label), s"${value}") - - def counts(children: HtmlElement*): Span = - span(interleave(children, span(" / "))) - - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div = - div( - cls("flex flex-col mt-2"), - div( - cls( - "align-middle min-w-full shadow sm:rounded-lg" - ), - L.table( - cls("min-w-full border-separate border-spacing-0"), - styleAttr("border-spacing: 0"), // Tailwind somehow doesn't work - thead( - tr( - cls("border-t border-gray-200"), - headers.map(h => - th( - cls( - "sticky top-0 z-10 px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - h - ) - ) - ) - ), - tbody(cls("bg-white divide-y divide-gray-100"), sections) - ) - ) - ) - - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - td( - cls("bg-gray-50 border-gray-200 border-t border-b"), - cls( - "sticky top-14 z-10 bg-gray-50 px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - colSpan(cs), - label - ), - mods - ) - - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - mods, - td( - cls( - "px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900 truncate" - ), - label - ), - children.map(e => - td( - cls( - "whitespace-nowrap px-2 py-2 text-sm text-gray-500" - ), - e - ) - ) - ) - - private def interleave( - a: Seq[HtmlElement], - separator: => HtmlElement - ): Seq[HtmlElement] = - if a.size < 2 then a - else a.init.flatMap(n => Seq(n, separator)) :+ a.last diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala deleted file mode 100644 index a45beaf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.FileRef - -trait DetailComponentsModule: - - def details: DetailComponents - - trait DetailComponents: - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement - - def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement - - def files(fs: List[FileRef]): HtmlElement - - def file(f: FileRef): HtmlElement - -trait DefaultDetailComponentsModule(using ctx: ComponentContext) - extends DetailComponentsModule: - self: IconsModule => - - override val details: DetailComponents = new DetailComponents: - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement = - div( - div( - cls := "px-4 sm:px-0", - h3( - cls := "text-base font-semibold leading-7 text-gray-900", - title - ), - subtitle.map(st => - p( - cls := "mt-1 max-w-2xl text-sm leading-6 text-gray-500", - st - ) - ) - // TODO: actions - ), - div( - cls := "mt-6 border-t border-gray-100", - dl( - cls := "divide-y divide-gray-100", - fields - ) - ) - ) - - override def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0", - dt( - cls := "text-sm font-medium leading-6 text-gray-900", - title - ), - dd( - cls := "mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0", - content - ) - ) - - override def files(fs: List[FileRef]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-100 rounded-md border border-gray-200", - fs.map(file) - ) - - override def file(f: FileRef): HtmlElement = - li( - cls := "flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6", - div( - cls := "flex w-0 flex-1 items-center", - icons.`paper-clip-solid`(), - div( - cls := "ml-4 flex min-w-0 flex-1 gap-2", - span( - cls := "truncate font-medium", - f.name - ), - f.size.map(size => span(cls := "flex-shrink-0 text-gray-400", size)) - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - "Uložit" - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala new file mode 100644 index 0000000..f6c583f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait FormComponents(using ctx: ComponentContext) extends LocalDateSelectModule: + def searchIcon: SvgElement + + def renderLocalDateSelect( + id: String, + labelText: Option[String], + placeholderText: Option[String], + mods: LocalDateSelect => Modifier[HtmlElement] + ): HtmlElement = + div( + labelText, + input( + idAttr(id), + nameAttr(id), + autoComplete("date"), + tpe("date"), + cls( + "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ), + placeholderText.map(placeholder(_)), + mods(localDateSelect) + ) + ) + + def searchField( + id: String, + placeholderText: Option[String] = None + )( + mods: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls := "flex-1 min-w-0", + label(ctx.messages("forms.search.label"), forId(id), cls("sr-only")), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + searchIcon + ), + input( + tpe := "search", + nameAttr := "search", + idAttr := id, + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholderText + .orElse( + ctx.messages + .opt( + s"forms.search.${id}.placeholder", + s"form.search.placeholder" + ) + ) + .map(placeholder(_)), + mods + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala deleted file mode 100644 index 7714490..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala +++ /dev/null @@ -1,82 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -/** Form components - * - * A form is a collection of sections, each of which contains a collection of - * fields. - * - * Form -> * Section -> * Field - * -> * Action - * - * Each Field has an `id`, `label` and `input` element. The `id` is used to - * link the label to the input. - * - * The `input` element can be a simple text input, or a more complex component - * such as a date picker. The caller is expected to make the input work. - */ -trait FormComponentsModule extends LocalDateSelectModule: - def forms: FormComponents - - trait FormComponents: - - /** Layout the sections and actions of a form */ - def form(mods: Modifier[HtmlElement]*)(sections: Modifier[HtmlElement]*)( - actions: Modifier[HtmlElement]* - ): HtmlElement - - /** Section layout, with a title, optional subtitle and any content. */ - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label and any content */ - def field(label: Modifier[HtmlElement])( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label, input and optional help text */ - def field( - id: String, - label: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement - - def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout an inline form element */ - def inlineForm(mods: Modifier[HtmlElement]*): HtmlElement - - /** An input field with a search icon */ - def searchField(id: String, placeholderText: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement - - /** LocalDate input */ - def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala deleted file mode 100644 index 8acbe7f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala +++ /dev/null @@ -1,28 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.Tag - -trait GenericComponentsModule: - - def generic: GenericComponents - - trait GenericComponents: - def tag(name: String, color: ColorKind): HtmlElement - def tag(t: Tag): HtmlElement = tag(t.value, t.color) - -trait DefaultGenericComponentsModule(using ComponentContext) - extends GenericComponentsModule: - override val generic: GenericComponents = new GenericComponents: - override def tag(name: String, color: ColorKind): HtmlElement = - p( - cls( - "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" - ), - color(800).text, - color(100).bg, - name - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala deleted file mode 100644 index 8cfc1d9..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala +++ /dev/null @@ -1,123 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.CustomAttrs - -trait IconsModule: - def icons: Icons - - trait Icons: - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement - def close(mods: Modifier[SvgElement]*): SvgElement - def `search-solid`(mods: Modifier[SvgElement]*): SvgElement - def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement - def `document-chart-bar-outline`(mods: Modifier[SvgElement]*): SvgElement - def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement - -trait DefaultIconsModule(using ComponentContext) extends IconsModule: - override val icons: Icons = new Icons: - import svg.* - - object aria: - val hidden = CustomAttrs.svg.ariaHidden - - private def withDefault( - mods: Seq[Modifier[SvgElement]], - default: Modifier[SvgElement] - ): Modifier[SvgElement] = - if mods.isEmpty then default else mods - - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-8 w-8"), - 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" - ) - ) - ) - - override def close(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-2 w-2"), - stroke := "currentColor", - fill := "none", - viewBox := "0 0 8 8", - path( - strokeLineCap := "round", - strokeWidth := "1.5", - d := "M1 1l6 6m0-6L1 7" - ) - ) - - override def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 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 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") - ) - ) - - override def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 text-gray-400")), - 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") - ) - ) - - override def `document-chart-bar-outline`( - mods: Modifier[SvgElement]* - ): SvgElement = - svg( - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - strokeWidth := "1.5", - stroke := "currentColor", - withDefault(mods, cls := "h-6 w-6"), - path( - strokeLineCap := "round", - strokeLineJoin := "round", - d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" - ) - ) - - override def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-5 w-5"), - cls := "flex-shrink-0 text-gray-400", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", - clipRule := "evenodd" - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala deleted file mode 100644 index 95ab654..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait LayoutModule: - def layout: LayoutComponents - - trait LayoutComponents: - def card(content: Modifier[HtmlElement]*): HtmlElement - -trait DefaultLayoutModule(using ctx: ComponentContext) extends LayoutModule: - override val layout: LayoutComponents = new LayoutComponents: - override def card(content: Modifier[HtmlElement]*): HtmlElement = - div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala deleted file mode 100644 index 5946974..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom.html.Paragraph -import com.raquo.laminar.nodes.TextNode - -trait ListComponentsModule: - - def list: ListComponents - - trait ListComponents(using ComponentContext): - def label(text: String, color: ColorKind): HtmlElement - def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI - def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] - def listSection(header: String, list: HtmlElement): Div - def navigation(sections: Modifier[HtmlElement]): HtmlElement - -trait DefaultListComponentsModule(using ComponentContext) - extends ListComponentsModule: - self: GenericComponentsModule => - - override val list: ListComponents = new ListComponents: - override def label( - text: String, - color: ColorKind - ): HtmlElement = generic.tag(text, color) - - override def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI = - li( - cls("group"), - div( - cls( - "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" - ), - avatar.map(a => - div( - cls("flex-shrink-0"), - div( - cls( - "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" - ), - a - ) - ) - ), - div( - cls("flex-1 min-w-0"), - p( - cls("text-sm font-medium text-gray-900"), - title, - span(cls("float-right"), right) - ), - subtitle.map(st => - p( - cls("text-sm text-gray-500 truncate"), - st - ) - ) - ) - ) - ) - - override def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = - ul( - cls("relative z-0 divide-y divide-gray-200"), - role("list"), - children - ) - - override def listSection( - header: String, - list: HtmlElement - ): Div = - div( - cls("relative"), - div( - cls( - "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" - ), - header - ), - list - ) - - override def navigation(sections: Modifier[HtmlElement]): HtmlElement = - navTag( - cls("flex-1 min-h-0 overflow-y-auto"), - sections - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala deleted file mode 100644 index 5b5490f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ModalComponentsModule: - - def modal: ModalComponents - - trait ModalComponents: - def modalDialog( - content: HtmlElement, - onClose: Modifier[HtmlElement] - ): HtmlElement - -trait DefaultModalComponentsModule(using ctx: ComponentContext) - extends ModalComponentsModule: - - override val modal: ModalComponents = new ModalComponents: - - override def modalDialog( - content: HtmlElement, - closeMod: Modifier[HtmlElement] - ): 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( - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")), - closeMod - ) - ) - - div( - cls("fixed inset-0 z-20 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - 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-7xl sm:align-middle" - ), - content - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala deleted file mode 100644 index b713d8a..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala +++ /dev/null @@ -1,71 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait PageComponentsModule: - - val page: PageComponents - - trait PageComponents: - def container( - children: Modifier[HtmlElement]* - ): HtmlElement - - def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement - - def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement] = emptyMod, - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement - - /** Visage for clickable text, like links - */ - def clickable: Modifier[HtmlElement] - -trait DefaultPageComponentsModule(using ComponentContext) - extends PageComponentsModule: - - override val page: PageComponents = new PageComponents: - override def container( - children: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), - children - ) - - override def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement = - div( - cls("p-8 bg-gray-100 h-full"), - header, - children - ) - - override def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement = - div( - cls("pb-5 border-b border-gray-200"), - div(cls("float-right"), right), - h1( - cls("text-2xl leading-6 font-medium text-gray-900"), - title - ), - subtitle.map( - p( - cls("text-sm font-medium text-gray-500"), - _ - ) - ) - ) - - override def clickable: Modifier[HtmlElement] = - cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala deleted file mode 100644 index f70ee87..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala +++ /dev/null @@ -1,108 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} - -trait TableComponentsModule: - def tables: TableComponents - - trait TableComponents: - def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )(table: Modifier[HtmlElement]*): HtmlElement - - def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement - - def headerRow(mods: Modifier[HtmlElement]*)( - cells: HtmlElement* - ): HtmlElement - - def dataRow(mods: Modifier[HtmlElement]*)(cells: HtmlElement*): HtmlElement - - def headerCell(content: Modifier[HtmlElement]): HtmlElement - - def dataCell(content: Modifier[HtmlElement]): HtmlElement - -trait DefaultTableComponentsModule extends TableComponentsModule: - - override lazy val tables: TableComponents = new TableComponents: - - override def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )( - table: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("px-4 sm:px-6 lg:px-8"), - div( - cls("sm:flex sm:items-center"), - div( - cls("sm:flex-auto"), - h1(cls("text-base font-semibold leading-6 text-gray-900"), title), - subtitle.map(st => p(cls("mt-2 text-sm text-gray-700"), st)) - ), - div(cls("mt-4 sm:ml-16 sm:mt-0 sm:flex-none"), actions) - ), - div( - cls("mt-8 flow-root"), - div( - cls("-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"), - div( - cls("inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"), - table - ) - ) - ) - ) - - override def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement = - table( - cls("min-w-full divide-y divide-gray-300"), - thead(header), - tbody(cls("divide-y divide-gray-200"), body) - ) - - override def headerRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-3.5 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-3.5 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-3.5 px-3")) - ) - ) - - override def dataRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - mods, - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-4 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-4 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-4 px-3")) - ) - ) - - override def headerCell(content: Modifier[HtmlElement]): HtmlElement = - th( - cls("text-left text-sm font-semibold text-gray-900"), - content - ) - - override def dataCell(content: Modifier[HtmlElement]): HtmlElement = - td( - cls("whitespace-nowrap text-sm text-gray-500"), - content - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala index b937d45..12d01ae 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala @@ -3,7 +3,7 @@ import com.raquo.laminar.api.L.{*, given} import works.iterative.ui.components.laminar.HtmlTabular import works.iterative.ui.model.tables.Tabular -import works.iterative.ui.components.laminar.TableComponentsModule +import works.iterative.ui.components.laminar.tailwind.ui.TableComponentsModule import works.iterative.core.UserMessage import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given import works.iterative.ui.components.tailwind.ComponentContext diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala index a2c637c..bfa063a 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala @@ -1,13 +1,11 @@ package works.iterative.ui.components.laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorDef import com.raquo.laminar.tags.HtmlTag case class Bin[Source, +Value]( label: String, description: Option[String | HtmlElement], - color: ColorDef, valueOf: Source => Value ): def map[NewValue]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala deleted file mode 100644 index 3aac0bf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala +++ /dev/null @@ -1,34 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ButtonComponentsModule: - def buttons: ButtonComponents - - trait ButtonComponents: - def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala deleted file mode 100644 index 646e73d..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala +++ /dev/null @@ -1,191 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.color.ColorDef -import com.raquo.laminar.tags.HtmlTag -import org.scalajs.dom.html.UList -import com.raquo.laminar.nodes.ReactiveHtmlElement -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind - -trait DashboardComponentsModule: - def dashboard: DashboardComponents - - // Only methods that create HtmlElements - trait DashboardComponents(using ComponentContext): - def section(label: String, children: Modifier[HtmlElement]): Div - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI - def cardInitials( - initials: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div - def count(label: String, value: Int, color: ColorDef): Span - def counts(children: HtmlElement*): Span - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - -trait DefaultDashboardComponentsModule(using ComponentContext) - extends DashboardComponentsModule: - override val dashboard: DashboardComponents = new DashboardComponents: - def section(label: String, children: Modifier[HtmlElement]): Div = - div( - cls("mt-3"), - h2( - cls("text-gray-500 text-xs font-medium uppercase tracking-wide"), - label - ), - children - ) - - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] = - ul( - cls( - "mt-3 grid gap-5 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" - ), - children - ) - - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI = - li( - div( - cls("col-span-1 flex shadow-sm rounded-md"), - initials, - div( - cls( - "flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate" - ), - div( - cls("flex-1 px-4 py-2 text-sm truncate"), - div( - cls("text-gray-900 font-medium hover:text-gray-600 truncate"), - label - ), - p(cls("text-gray-500"), children) - ) - ) - ) - ) - - def cardInitials( - id: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div = - div( - cls( - "flex-shrink-0 flex flex-col items-center justify-center w-16 text-white text-sm font-medium rounded-l-md" - ), - color.map(_(600).bg), - div(id), - children - ) - - def count(label: String, value: Int, color: ColorDef): Span = - span(color.text, title(label), s"${value}") - - def counts(children: HtmlElement*): Span = - span(interleave(children, span(" / "))) - - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div = - div( - cls("flex flex-col mt-2"), - div( - cls( - "align-middle min-w-full shadow sm:rounded-lg" - ), - L.table( - cls("min-w-full border-separate border-spacing-0"), - styleAttr("border-spacing: 0"), // Tailwind somehow doesn't work - thead( - tr( - cls("border-t border-gray-200"), - headers.map(h => - th( - cls( - "sticky top-0 z-10 px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - h - ) - ) - ) - ), - tbody(cls("bg-white divide-y divide-gray-100"), sections) - ) - ) - ) - - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - td( - cls("bg-gray-50 border-gray-200 border-t border-b"), - cls( - "sticky top-14 z-10 bg-gray-50 px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - colSpan(cs), - label - ), - mods - ) - - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - mods, - td( - cls( - "px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900 truncate" - ), - label - ), - children.map(e => - td( - cls( - "whitespace-nowrap px-2 py-2 text-sm text-gray-500" - ), - e - ) - ) - ) - - private def interleave( - a: Seq[HtmlElement], - separator: => HtmlElement - ): Seq[HtmlElement] = - if a.size < 2 then a - else a.init.flatMap(n => Seq(n, separator)) :+ a.last diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala deleted file mode 100644 index a45beaf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.FileRef - -trait DetailComponentsModule: - - def details: DetailComponents - - trait DetailComponents: - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement - - def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement - - def files(fs: List[FileRef]): HtmlElement - - def file(f: FileRef): HtmlElement - -trait DefaultDetailComponentsModule(using ctx: ComponentContext) - extends DetailComponentsModule: - self: IconsModule => - - override val details: DetailComponents = new DetailComponents: - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement = - div( - div( - cls := "px-4 sm:px-0", - h3( - cls := "text-base font-semibold leading-7 text-gray-900", - title - ), - subtitle.map(st => - p( - cls := "mt-1 max-w-2xl text-sm leading-6 text-gray-500", - st - ) - ) - // TODO: actions - ), - div( - cls := "mt-6 border-t border-gray-100", - dl( - cls := "divide-y divide-gray-100", - fields - ) - ) - ) - - override def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0", - dt( - cls := "text-sm font-medium leading-6 text-gray-900", - title - ), - dd( - cls := "mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0", - content - ) - ) - - override def files(fs: List[FileRef]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-100 rounded-md border border-gray-200", - fs.map(file) - ) - - override def file(f: FileRef): HtmlElement = - li( - cls := "flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6", - div( - cls := "flex w-0 flex-1 items-center", - icons.`paper-clip-solid`(), - div( - cls := "ml-4 flex min-w-0 flex-1 gap-2", - span( - cls := "truncate font-medium", - f.name - ), - f.size.map(size => span(cls := "flex-shrink-0 text-gray-400", size)) - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - "Uložit" - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala new file mode 100644 index 0000000..f6c583f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait FormComponents(using ctx: ComponentContext) extends LocalDateSelectModule: + def searchIcon: SvgElement + + def renderLocalDateSelect( + id: String, + labelText: Option[String], + placeholderText: Option[String], + mods: LocalDateSelect => Modifier[HtmlElement] + ): HtmlElement = + div( + labelText, + input( + idAttr(id), + nameAttr(id), + autoComplete("date"), + tpe("date"), + cls( + "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ), + placeholderText.map(placeholder(_)), + mods(localDateSelect) + ) + ) + + def searchField( + id: String, + placeholderText: Option[String] = None + )( + mods: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls := "flex-1 min-w-0", + label(ctx.messages("forms.search.label"), forId(id), cls("sr-only")), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + searchIcon + ), + input( + tpe := "search", + nameAttr := "search", + idAttr := id, + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholderText + .orElse( + ctx.messages + .opt( + s"forms.search.${id}.placeholder", + s"form.search.placeholder" + ) + ) + .map(placeholder(_)), + mods + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala deleted file mode 100644 index 7714490..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala +++ /dev/null @@ -1,82 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -/** Form components - * - * A form is a collection of sections, each of which contains a collection of - * fields. - * - * Form -> * Section -> * Field - * -> * Action - * - * Each Field has an `id`, `label` and `input` element. The `id` is used to - * link the label to the input. - * - * The `input` element can be a simple text input, or a more complex component - * such as a date picker. The caller is expected to make the input work. - */ -trait FormComponentsModule extends LocalDateSelectModule: - def forms: FormComponents - - trait FormComponents: - - /** Layout the sections and actions of a form */ - def form(mods: Modifier[HtmlElement]*)(sections: Modifier[HtmlElement]*)( - actions: Modifier[HtmlElement]* - ): HtmlElement - - /** Section layout, with a title, optional subtitle and any content. */ - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label and any content */ - def field(label: Modifier[HtmlElement])( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label, input and optional help text */ - def field( - id: String, - label: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement - - def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout an inline form element */ - def inlineForm(mods: Modifier[HtmlElement]*): HtmlElement - - /** An input field with a search icon */ - def searchField(id: String, placeholderText: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement - - /** LocalDate input */ - def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala deleted file mode 100644 index 8acbe7f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala +++ /dev/null @@ -1,28 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.Tag - -trait GenericComponentsModule: - - def generic: GenericComponents - - trait GenericComponents: - def tag(name: String, color: ColorKind): HtmlElement - def tag(t: Tag): HtmlElement = tag(t.value, t.color) - -trait DefaultGenericComponentsModule(using ComponentContext) - extends GenericComponentsModule: - override val generic: GenericComponents = new GenericComponents: - override def tag(name: String, color: ColorKind): HtmlElement = - p( - cls( - "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" - ), - color(800).text, - color(100).bg, - name - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala deleted file mode 100644 index 8cfc1d9..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala +++ /dev/null @@ -1,123 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.CustomAttrs - -trait IconsModule: - def icons: Icons - - trait Icons: - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement - def close(mods: Modifier[SvgElement]*): SvgElement - def `search-solid`(mods: Modifier[SvgElement]*): SvgElement - def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement - def `document-chart-bar-outline`(mods: Modifier[SvgElement]*): SvgElement - def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement - -trait DefaultIconsModule(using ComponentContext) extends IconsModule: - override val icons: Icons = new Icons: - import svg.* - - object aria: - val hidden = CustomAttrs.svg.ariaHidden - - private def withDefault( - mods: Seq[Modifier[SvgElement]], - default: Modifier[SvgElement] - ): Modifier[SvgElement] = - if mods.isEmpty then default else mods - - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-8 w-8"), - 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" - ) - ) - ) - - override def close(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-2 w-2"), - stroke := "currentColor", - fill := "none", - viewBox := "0 0 8 8", - path( - strokeLineCap := "round", - strokeWidth := "1.5", - d := "M1 1l6 6m0-6L1 7" - ) - ) - - override def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 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 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") - ) - ) - - override def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 text-gray-400")), - 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") - ) - ) - - override def `document-chart-bar-outline`( - mods: Modifier[SvgElement]* - ): SvgElement = - svg( - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - strokeWidth := "1.5", - stroke := "currentColor", - withDefault(mods, cls := "h-6 w-6"), - path( - strokeLineCap := "round", - strokeLineJoin := "round", - d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" - ) - ) - - override def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-5 w-5"), - cls := "flex-shrink-0 text-gray-400", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", - clipRule := "evenodd" - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala deleted file mode 100644 index 95ab654..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait LayoutModule: - def layout: LayoutComponents - - trait LayoutComponents: - def card(content: Modifier[HtmlElement]*): HtmlElement - -trait DefaultLayoutModule(using ctx: ComponentContext) extends LayoutModule: - override val layout: LayoutComponents = new LayoutComponents: - override def card(content: Modifier[HtmlElement]*): HtmlElement = - div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala deleted file mode 100644 index 5946974..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom.html.Paragraph -import com.raquo.laminar.nodes.TextNode - -trait ListComponentsModule: - - def list: ListComponents - - trait ListComponents(using ComponentContext): - def label(text: String, color: ColorKind): HtmlElement - def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI - def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] - def listSection(header: String, list: HtmlElement): Div - def navigation(sections: Modifier[HtmlElement]): HtmlElement - -trait DefaultListComponentsModule(using ComponentContext) - extends ListComponentsModule: - self: GenericComponentsModule => - - override val list: ListComponents = new ListComponents: - override def label( - text: String, - color: ColorKind - ): HtmlElement = generic.tag(text, color) - - override def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI = - li( - cls("group"), - div( - cls( - "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" - ), - avatar.map(a => - div( - cls("flex-shrink-0"), - div( - cls( - "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" - ), - a - ) - ) - ), - div( - cls("flex-1 min-w-0"), - p( - cls("text-sm font-medium text-gray-900"), - title, - span(cls("float-right"), right) - ), - subtitle.map(st => - p( - cls("text-sm text-gray-500 truncate"), - st - ) - ) - ) - ) - ) - - override def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = - ul( - cls("relative z-0 divide-y divide-gray-200"), - role("list"), - children - ) - - override def listSection( - header: String, - list: HtmlElement - ): Div = - div( - cls("relative"), - div( - cls( - "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" - ), - header - ), - list - ) - - override def navigation(sections: Modifier[HtmlElement]): HtmlElement = - navTag( - cls("flex-1 min-h-0 overflow-y-auto"), - sections - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala deleted file mode 100644 index 5b5490f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ModalComponentsModule: - - def modal: ModalComponents - - trait ModalComponents: - def modalDialog( - content: HtmlElement, - onClose: Modifier[HtmlElement] - ): HtmlElement - -trait DefaultModalComponentsModule(using ctx: ComponentContext) - extends ModalComponentsModule: - - override val modal: ModalComponents = new ModalComponents: - - override def modalDialog( - content: HtmlElement, - closeMod: Modifier[HtmlElement] - ): 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( - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")), - closeMod - ) - ) - - div( - cls("fixed inset-0 z-20 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - 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-7xl sm:align-middle" - ), - content - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala deleted file mode 100644 index b713d8a..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala +++ /dev/null @@ -1,71 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait PageComponentsModule: - - val page: PageComponents - - trait PageComponents: - def container( - children: Modifier[HtmlElement]* - ): HtmlElement - - def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement - - def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement] = emptyMod, - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement - - /** Visage for clickable text, like links - */ - def clickable: Modifier[HtmlElement] - -trait DefaultPageComponentsModule(using ComponentContext) - extends PageComponentsModule: - - override val page: PageComponents = new PageComponents: - override def container( - children: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), - children - ) - - override def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement = - div( - cls("p-8 bg-gray-100 h-full"), - header, - children - ) - - override def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement = - div( - cls("pb-5 border-b border-gray-200"), - div(cls("float-right"), right), - h1( - cls("text-2xl leading-6 font-medium text-gray-900"), - title - ), - subtitle.map( - p( - cls("text-sm font-medium text-gray-500"), - _ - ) - ) - ) - - override def clickable: Modifier[HtmlElement] = - cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala deleted file mode 100644 index f70ee87..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala +++ /dev/null @@ -1,108 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} - -trait TableComponentsModule: - def tables: TableComponents - - trait TableComponents: - def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )(table: Modifier[HtmlElement]*): HtmlElement - - def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement - - def headerRow(mods: Modifier[HtmlElement]*)( - cells: HtmlElement* - ): HtmlElement - - def dataRow(mods: Modifier[HtmlElement]*)(cells: HtmlElement*): HtmlElement - - def headerCell(content: Modifier[HtmlElement]): HtmlElement - - def dataCell(content: Modifier[HtmlElement]): HtmlElement - -trait DefaultTableComponentsModule extends TableComponentsModule: - - override lazy val tables: TableComponents = new TableComponents: - - override def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )( - table: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("px-4 sm:px-6 lg:px-8"), - div( - cls("sm:flex sm:items-center"), - div( - cls("sm:flex-auto"), - h1(cls("text-base font-semibold leading-6 text-gray-900"), title), - subtitle.map(st => p(cls("mt-2 text-sm text-gray-700"), st)) - ), - div(cls("mt-4 sm:ml-16 sm:mt-0 sm:flex-none"), actions) - ), - div( - cls("mt-8 flow-root"), - div( - cls("-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"), - div( - cls("inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"), - table - ) - ) - ) - ) - - override def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement = - table( - cls("min-w-full divide-y divide-gray-300"), - thead(header), - tbody(cls("divide-y divide-gray-200"), body) - ) - - override def headerRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-3.5 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-3.5 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-3.5 px-3")) - ) - ) - - override def dataRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - mods, - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-4 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-4 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-4 px-3")) - ) - ) - - override def headerCell(content: Modifier[HtmlElement]): HtmlElement = - th( - cls("text-left text-sm font-semibold text-gray-900"), - content - ) - - override def dataCell(content: Modifier[HtmlElement]): HtmlElement = - td( - cls("whitespace-nowrap text-sm text-gray-500"), - content - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala index b937d45..12d01ae 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala @@ -3,7 +3,7 @@ import com.raquo.laminar.api.L.{*, given} import works.iterative.ui.components.laminar.HtmlTabular import works.iterative.ui.model.tables.Tabular -import works.iterative.ui.components.laminar.TableComponentsModule +import works.iterative.ui.components.laminar.tailwind.ui.TableComponentsModule import works.iterative.core.UserMessage import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given import works.iterative.ui.components.tailwind.ComponentContext diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala new file mode 100644 index 0000000..87f3e8d --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala @@ -0,0 +1,54 @@ +package works.iterative.ui.components.laminar.tailwind.color + +import com.raquo.laminar.api.L.{*, given} + +/** Complete color definition that can be rendered to CSS. + * + * Includes the area, kind and weight of the color. + */ +case class Color(area: ColorArea, color: ColorDef): + def toCSS: String = s"${area.name}-${color.toCSS}" + +object Color: + import ColorDef.given + + given colorToCSS: Conversion[Color, HtmlMod] with + def apply(c: Color) = cls(c.toCSS) + + given colorToSVGCSS: Conversion[Color, SvgMod] with + def apply(c: Color) = svg.cls(c.toCSS) + + given colorSignalToCSS: Conversion[Signal[Color], HtmlMod] with + def apply(c: Signal[Color]) = cls <-- c.map(_.toCSS) + + given colorSignalToSVGCSS: Conversion[Signal[Color], SvgMod] with + def apply(c: Signal[Color]) = svg.cls <-- c.map(_.toCSS) + + def current = ColorDef(ColorKind.current) + def inherit = ColorDef(ColorKind.inherit) + def transp = ColorDef(ColorKind.transp) + def auto = ColorDef(ColorKind.auto) + def black = ColorDef(ColorKind.black) + def white = ColorDef(ColorKind.white) + def slate(weight: ColorWeight) = ColorDef(ColorKind.slate, weight) + def gray(weight: ColorWeight) = ColorDef(ColorKind.gray, weight) + def zinc(weight: ColorWeight) = ColorDef(ColorKind.zinc, weight) + def neutral(weight: ColorWeight) = ColorDef(ColorKind.neutral, weight) + def stone(weight: ColorWeight) = ColorDef(ColorKind.stone, weight) + def red(weight: ColorWeight) = ColorDef(ColorKind.red, weight) + def orange(weight: ColorWeight) = ColorDef(ColorKind.orange, weight) + def amber(weight: ColorWeight) = ColorDef(ColorKind.amber, weight) + def yellow(weight: ColorWeight) = ColorDef(ColorKind.yellow, weight) + def lime(weight: ColorWeight) = ColorDef(ColorKind.lime, weight) + def green(weight: ColorWeight) = ColorDef(ColorKind.green, weight) + def emerald(weight: ColorWeight) = ColorDef(ColorKind.emerald, weight) + def teal(weight: ColorWeight) = ColorDef(ColorKind.teal, weight) + def cyan(weight: ColorWeight) = ColorDef(ColorKind.cyan, weight) + def sky(weight: ColorWeight) = ColorDef(ColorKind.sky, weight) + def blue(weight: ColorWeight) = ColorDef(ColorKind.blue, weight) + def indigo(weight: ColorWeight) = ColorDef(ColorKind.indigo, weight) + def violet(weight: ColorWeight) = ColorDef(ColorKind.violet, weight) + def purple(weight: ColorWeight) = ColorDef(ColorKind.purple, weight) + def fuchsia(weight: ColorWeight) = ColorDef(ColorKind.fuchsia, weight) + def pink(weight: ColorWeight) = ColorDef(ColorKind.pink, weight) + def rose(weight: ColorWeight) = ColorDef(ColorKind.rose, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala index a2c637c..bfa063a 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala @@ -1,13 +1,11 @@ package works.iterative.ui.components.laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorDef import com.raquo.laminar.tags.HtmlTag case class Bin[Source, +Value]( label: String, description: Option[String | HtmlElement], - color: ColorDef, valueOf: Source => Value ): def map[NewValue]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala deleted file mode 100644 index 3aac0bf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala +++ /dev/null @@ -1,34 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ButtonComponentsModule: - def buttons: ButtonComponents - - trait ButtonComponents: - def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala deleted file mode 100644 index 646e73d..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala +++ /dev/null @@ -1,191 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.color.ColorDef -import com.raquo.laminar.tags.HtmlTag -import org.scalajs.dom.html.UList -import com.raquo.laminar.nodes.ReactiveHtmlElement -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind - -trait DashboardComponentsModule: - def dashboard: DashboardComponents - - // Only methods that create HtmlElements - trait DashboardComponents(using ComponentContext): - def section(label: String, children: Modifier[HtmlElement]): Div - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI - def cardInitials( - initials: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div - def count(label: String, value: Int, color: ColorDef): Span - def counts(children: HtmlElement*): Span - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - -trait DefaultDashboardComponentsModule(using ComponentContext) - extends DashboardComponentsModule: - override val dashboard: DashboardComponents = new DashboardComponents: - def section(label: String, children: Modifier[HtmlElement]): Div = - div( - cls("mt-3"), - h2( - cls("text-gray-500 text-xs font-medium uppercase tracking-wide"), - label - ), - children - ) - - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] = - ul( - cls( - "mt-3 grid gap-5 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" - ), - children - ) - - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI = - li( - div( - cls("col-span-1 flex shadow-sm rounded-md"), - initials, - div( - cls( - "flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate" - ), - div( - cls("flex-1 px-4 py-2 text-sm truncate"), - div( - cls("text-gray-900 font-medium hover:text-gray-600 truncate"), - label - ), - p(cls("text-gray-500"), children) - ) - ) - ) - ) - - def cardInitials( - id: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div = - div( - cls( - "flex-shrink-0 flex flex-col items-center justify-center w-16 text-white text-sm font-medium rounded-l-md" - ), - color.map(_(600).bg), - div(id), - children - ) - - def count(label: String, value: Int, color: ColorDef): Span = - span(color.text, title(label), s"${value}") - - def counts(children: HtmlElement*): Span = - span(interleave(children, span(" / "))) - - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div = - div( - cls("flex flex-col mt-2"), - div( - cls( - "align-middle min-w-full shadow sm:rounded-lg" - ), - L.table( - cls("min-w-full border-separate border-spacing-0"), - styleAttr("border-spacing: 0"), // Tailwind somehow doesn't work - thead( - tr( - cls("border-t border-gray-200"), - headers.map(h => - th( - cls( - "sticky top-0 z-10 px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - h - ) - ) - ) - ), - tbody(cls("bg-white divide-y divide-gray-100"), sections) - ) - ) - ) - - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - td( - cls("bg-gray-50 border-gray-200 border-t border-b"), - cls( - "sticky top-14 z-10 bg-gray-50 px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - colSpan(cs), - label - ), - mods - ) - - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - mods, - td( - cls( - "px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900 truncate" - ), - label - ), - children.map(e => - td( - cls( - "whitespace-nowrap px-2 py-2 text-sm text-gray-500" - ), - e - ) - ) - ) - - private def interleave( - a: Seq[HtmlElement], - separator: => HtmlElement - ): Seq[HtmlElement] = - if a.size < 2 then a - else a.init.flatMap(n => Seq(n, separator)) :+ a.last diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala deleted file mode 100644 index a45beaf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.FileRef - -trait DetailComponentsModule: - - def details: DetailComponents - - trait DetailComponents: - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement - - def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement - - def files(fs: List[FileRef]): HtmlElement - - def file(f: FileRef): HtmlElement - -trait DefaultDetailComponentsModule(using ctx: ComponentContext) - extends DetailComponentsModule: - self: IconsModule => - - override val details: DetailComponents = new DetailComponents: - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement = - div( - div( - cls := "px-4 sm:px-0", - h3( - cls := "text-base font-semibold leading-7 text-gray-900", - title - ), - subtitle.map(st => - p( - cls := "mt-1 max-w-2xl text-sm leading-6 text-gray-500", - st - ) - ) - // TODO: actions - ), - div( - cls := "mt-6 border-t border-gray-100", - dl( - cls := "divide-y divide-gray-100", - fields - ) - ) - ) - - override def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0", - dt( - cls := "text-sm font-medium leading-6 text-gray-900", - title - ), - dd( - cls := "mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0", - content - ) - ) - - override def files(fs: List[FileRef]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-100 rounded-md border border-gray-200", - fs.map(file) - ) - - override def file(f: FileRef): HtmlElement = - li( - cls := "flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6", - div( - cls := "flex w-0 flex-1 items-center", - icons.`paper-clip-solid`(), - div( - cls := "ml-4 flex min-w-0 flex-1 gap-2", - span( - cls := "truncate font-medium", - f.name - ), - f.size.map(size => span(cls := "flex-shrink-0 text-gray-400", size)) - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - "Uložit" - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala new file mode 100644 index 0000000..f6c583f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait FormComponents(using ctx: ComponentContext) extends LocalDateSelectModule: + def searchIcon: SvgElement + + def renderLocalDateSelect( + id: String, + labelText: Option[String], + placeholderText: Option[String], + mods: LocalDateSelect => Modifier[HtmlElement] + ): HtmlElement = + div( + labelText, + input( + idAttr(id), + nameAttr(id), + autoComplete("date"), + tpe("date"), + cls( + "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ), + placeholderText.map(placeholder(_)), + mods(localDateSelect) + ) + ) + + def searchField( + id: String, + placeholderText: Option[String] = None + )( + mods: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls := "flex-1 min-w-0", + label(ctx.messages("forms.search.label"), forId(id), cls("sr-only")), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + searchIcon + ), + input( + tpe := "search", + nameAttr := "search", + idAttr := id, + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholderText + .orElse( + ctx.messages + .opt( + s"forms.search.${id}.placeholder", + s"form.search.placeholder" + ) + ) + .map(placeholder(_)), + mods + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala deleted file mode 100644 index 7714490..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala +++ /dev/null @@ -1,82 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -/** Form components - * - * A form is a collection of sections, each of which contains a collection of - * fields. - * - * Form -> * Section -> * Field - * -> * Action - * - * Each Field has an `id`, `label` and `input` element. The `id` is used to - * link the label to the input. - * - * The `input` element can be a simple text input, or a more complex component - * such as a date picker. The caller is expected to make the input work. - */ -trait FormComponentsModule extends LocalDateSelectModule: - def forms: FormComponents - - trait FormComponents: - - /** Layout the sections and actions of a form */ - def form(mods: Modifier[HtmlElement]*)(sections: Modifier[HtmlElement]*)( - actions: Modifier[HtmlElement]* - ): HtmlElement - - /** Section layout, with a title, optional subtitle and any content. */ - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label and any content */ - def field(label: Modifier[HtmlElement])( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label, input and optional help text */ - def field( - id: String, - label: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement - - def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout an inline form element */ - def inlineForm(mods: Modifier[HtmlElement]*): HtmlElement - - /** An input field with a search icon */ - def searchField(id: String, placeholderText: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement - - /** LocalDate input */ - def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala deleted file mode 100644 index 8acbe7f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala +++ /dev/null @@ -1,28 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.Tag - -trait GenericComponentsModule: - - def generic: GenericComponents - - trait GenericComponents: - def tag(name: String, color: ColorKind): HtmlElement - def tag(t: Tag): HtmlElement = tag(t.value, t.color) - -trait DefaultGenericComponentsModule(using ComponentContext) - extends GenericComponentsModule: - override val generic: GenericComponents = new GenericComponents: - override def tag(name: String, color: ColorKind): HtmlElement = - p( - cls( - "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" - ), - color(800).text, - color(100).bg, - name - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala deleted file mode 100644 index 8cfc1d9..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala +++ /dev/null @@ -1,123 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.CustomAttrs - -trait IconsModule: - def icons: Icons - - trait Icons: - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement - def close(mods: Modifier[SvgElement]*): SvgElement - def `search-solid`(mods: Modifier[SvgElement]*): SvgElement - def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement - def `document-chart-bar-outline`(mods: Modifier[SvgElement]*): SvgElement - def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement - -trait DefaultIconsModule(using ComponentContext) extends IconsModule: - override val icons: Icons = new Icons: - import svg.* - - object aria: - val hidden = CustomAttrs.svg.ariaHidden - - private def withDefault( - mods: Seq[Modifier[SvgElement]], - default: Modifier[SvgElement] - ): Modifier[SvgElement] = - if mods.isEmpty then default else mods - - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-8 w-8"), - 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" - ) - ) - ) - - override def close(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-2 w-2"), - stroke := "currentColor", - fill := "none", - viewBox := "0 0 8 8", - path( - strokeLineCap := "round", - strokeWidth := "1.5", - d := "M1 1l6 6m0-6L1 7" - ) - ) - - override def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 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 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") - ) - ) - - override def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 text-gray-400")), - 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") - ) - ) - - override def `document-chart-bar-outline`( - mods: Modifier[SvgElement]* - ): SvgElement = - svg( - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - strokeWidth := "1.5", - stroke := "currentColor", - withDefault(mods, cls := "h-6 w-6"), - path( - strokeLineCap := "round", - strokeLineJoin := "round", - d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" - ) - ) - - override def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-5 w-5"), - cls := "flex-shrink-0 text-gray-400", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", - clipRule := "evenodd" - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala deleted file mode 100644 index 95ab654..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait LayoutModule: - def layout: LayoutComponents - - trait LayoutComponents: - def card(content: Modifier[HtmlElement]*): HtmlElement - -trait DefaultLayoutModule(using ctx: ComponentContext) extends LayoutModule: - override val layout: LayoutComponents = new LayoutComponents: - override def card(content: Modifier[HtmlElement]*): HtmlElement = - div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala deleted file mode 100644 index 5946974..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom.html.Paragraph -import com.raquo.laminar.nodes.TextNode - -trait ListComponentsModule: - - def list: ListComponents - - trait ListComponents(using ComponentContext): - def label(text: String, color: ColorKind): HtmlElement - def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI - def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] - def listSection(header: String, list: HtmlElement): Div - def navigation(sections: Modifier[HtmlElement]): HtmlElement - -trait DefaultListComponentsModule(using ComponentContext) - extends ListComponentsModule: - self: GenericComponentsModule => - - override val list: ListComponents = new ListComponents: - override def label( - text: String, - color: ColorKind - ): HtmlElement = generic.tag(text, color) - - override def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI = - li( - cls("group"), - div( - cls( - "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" - ), - avatar.map(a => - div( - cls("flex-shrink-0"), - div( - cls( - "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" - ), - a - ) - ) - ), - div( - cls("flex-1 min-w-0"), - p( - cls("text-sm font-medium text-gray-900"), - title, - span(cls("float-right"), right) - ), - subtitle.map(st => - p( - cls("text-sm text-gray-500 truncate"), - st - ) - ) - ) - ) - ) - - override def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = - ul( - cls("relative z-0 divide-y divide-gray-200"), - role("list"), - children - ) - - override def listSection( - header: String, - list: HtmlElement - ): Div = - div( - cls("relative"), - div( - cls( - "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" - ), - header - ), - list - ) - - override def navigation(sections: Modifier[HtmlElement]): HtmlElement = - navTag( - cls("flex-1 min-h-0 overflow-y-auto"), - sections - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala deleted file mode 100644 index 5b5490f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ModalComponentsModule: - - def modal: ModalComponents - - trait ModalComponents: - def modalDialog( - content: HtmlElement, - onClose: Modifier[HtmlElement] - ): HtmlElement - -trait DefaultModalComponentsModule(using ctx: ComponentContext) - extends ModalComponentsModule: - - override val modal: ModalComponents = new ModalComponents: - - override def modalDialog( - content: HtmlElement, - closeMod: Modifier[HtmlElement] - ): 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( - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")), - closeMod - ) - ) - - div( - cls("fixed inset-0 z-20 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - 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-7xl sm:align-middle" - ), - content - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala deleted file mode 100644 index b713d8a..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala +++ /dev/null @@ -1,71 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait PageComponentsModule: - - val page: PageComponents - - trait PageComponents: - def container( - children: Modifier[HtmlElement]* - ): HtmlElement - - def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement - - def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement] = emptyMod, - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement - - /** Visage for clickable text, like links - */ - def clickable: Modifier[HtmlElement] - -trait DefaultPageComponentsModule(using ComponentContext) - extends PageComponentsModule: - - override val page: PageComponents = new PageComponents: - override def container( - children: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), - children - ) - - override def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement = - div( - cls("p-8 bg-gray-100 h-full"), - header, - children - ) - - override def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement = - div( - cls("pb-5 border-b border-gray-200"), - div(cls("float-right"), right), - h1( - cls("text-2xl leading-6 font-medium text-gray-900"), - title - ), - subtitle.map( - p( - cls("text-sm font-medium text-gray-500"), - _ - ) - ) - ) - - override def clickable: Modifier[HtmlElement] = - cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala deleted file mode 100644 index f70ee87..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala +++ /dev/null @@ -1,108 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} - -trait TableComponentsModule: - def tables: TableComponents - - trait TableComponents: - def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )(table: Modifier[HtmlElement]*): HtmlElement - - def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement - - def headerRow(mods: Modifier[HtmlElement]*)( - cells: HtmlElement* - ): HtmlElement - - def dataRow(mods: Modifier[HtmlElement]*)(cells: HtmlElement*): HtmlElement - - def headerCell(content: Modifier[HtmlElement]): HtmlElement - - def dataCell(content: Modifier[HtmlElement]): HtmlElement - -trait DefaultTableComponentsModule extends TableComponentsModule: - - override lazy val tables: TableComponents = new TableComponents: - - override def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )( - table: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("px-4 sm:px-6 lg:px-8"), - div( - cls("sm:flex sm:items-center"), - div( - cls("sm:flex-auto"), - h1(cls("text-base font-semibold leading-6 text-gray-900"), title), - subtitle.map(st => p(cls("mt-2 text-sm text-gray-700"), st)) - ), - div(cls("mt-4 sm:ml-16 sm:mt-0 sm:flex-none"), actions) - ), - div( - cls("mt-8 flow-root"), - div( - cls("-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"), - div( - cls("inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"), - table - ) - ) - ) - ) - - override def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement = - table( - cls("min-w-full divide-y divide-gray-300"), - thead(header), - tbody(cls("divide-y divide-gray-200"), body) - ) - - override def headerRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-3.5 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-3.5 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-3.5 px-3")) - ) - ) - - override def dataRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - mods, - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-4 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-4 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-4 px-3")) - ) - ) - - override def headerCell(content: Modifier[HtmlElement]): HtmlElement = - th( - cls("text-left text-sm font-semibold text-gray-900"), - content - ) - - override def dataCell(content: Modifier[HtmlElement]): HtmlElement = - td( - cls("whitespace-nowrap text-sm text-gray-500"), - content - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala index b937d45..12d01ae 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala @@ -3,7 +3,7 @@ import com.raquo.laminar.api.L.{*, given} import works.iterative.ui.components.laminar.HtmlTabular import works.iterative.ui.model.tables.Tabular -import works.iterative.ui.components.laminar.TableComponentsModule +import works.iterative.ui.components.laminar.tailwind.ui.TableComponentsModule import works.iterative.core.UserMessage import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given import works.iterative.ui.components.tailwind.ComponentContext diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala new file mode 100644 index 0000000..87f3e8d --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala @@ -0,0 +1,54 @@ +package works.iterative.ui.components.laminar.tailwind.color + +import com.raquo.laminar.api.L.{*, given} + +/** Complete color definition that can be rendered to CSS. + * + * Includes the area, kind and weight of the color. + */ +case class Color(area: ColorArea, color: ColorDef): + def toCSS: String = s"${area.name}-${color.toCSS}" + +object Color: + import ColorDef.given + + given colorToCSS: Conversion[Color, HtmlMod] with + def apply(c: Color) = cls(c.toCSS) + + given colorToSVGCSS: Conversion[Color, SvgMod] with + def apply(c: Color) = svg.cls(c.toCSS) + + given colorSignalToCSS: Conversion[Signal[Color], HtmlMod] with + def apply(c: Signal[Color]) = cls <-- c.map(_.toCSS) + + given colorSignalToSVGCSS: Conversion[Signal[Color], SvgMod] with + def apply(c: Signal[Color]) = svg.cls <-- c.map(_.toCSS) + + def current = ColorDef(ColorKind.current) + def inherit = ColorDef(ColorKind.inherit) + def transp = ColorDef(ColorKind.transp) + def auto = ColorDef(ColorKind.auto) + def black = ColorDef(ColorKind.black) + def white = ColorDef(ColorKind.white) + def slate(weight: ColorWeight) = ColorDef(ColorKind.slate, weight) + def gray(weight: ColorWeight) = ColorDef(ColorKind.gray, weight) + def zinc(weight: ColorWeight) = ColorDef(ColorKind.zinc, weight) + def neutral(weight: ColorWeight) = ColorDef(ColorKind.neutral, weight) + def stone(weight: ColorWeight) = ColorDef(ColorKind.stone, weight) + def red(weight: ColorWeight) = ColorDef(ColorKind.red, weight) + def orange(weight: ColorWeight) = ColorDef(ColorKind.orange, weight) + def amber(weight: ColorWeight) = ColorDef(ColorKind.amber, weight) + def yellow(weight: ColorWeight) = ColorDef(ColorKind.yellow, weight) + def lime(weight: ColorWeight) = ColorDef(ColorKind.lime, weight) + def green(weight: ColorWeight) = ColorDef(ColorKind.green, weight) + def emerald(weight: ColorWeight) = ColorDef(ColorKind.emerald, weight) + def teal(weight: ColorWeight) = ColorDef(ColorKind.teal, weight) + def cyan(weight: ColorWeight) = ColorDef(ColorKind.cyan, weight) + def sky(weight: ColorWeight) = ColorDef(ColorKind.sky, weight) + def blue(weight: ColorWeight) = ColorDef(ColorKind.blue, weight) + def indigo(weight: ColorWeight) = ColorDef(ColorKind.indigo, weight) + def violet(weight: ColorWeight) = ColorDef(ColorKind.violet, weight) + def purple(weight: ColorWeight) = ColorDef(ColorKind.purple, weight) + def fuchsia(weight: ColorWeight) = ColorDef(ColorKind.fuchsia, weight) + def pink(weight: ColorWeight) = ColorDef(ColorKind.pink, weight) + def rose(weight: ColorWeight) = ColorDef(ColorKind.rose, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala new file mode 100644 index 0000000..4130840 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala @@ -0,0 +1,16 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines the area the color should apply to, eg. background, text, border, + * etc. + */ +enum ColorArea(val name: String): + case bg extends ColorArea("bg") + case text extends ColorArea("text") + case decoration extends ColorArea("decoration") + case border extends ColorArea("border") + case outline extends ColorArea("outline") + case divide extends ColorArea("divide") + case ring extends ColorArea("ring") + case ringOffset extends ColorArea("ring-offset") + case shadow extends ColorArea("shadow") + case accent extends ColorArea("accent") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala index a2c637c..bfa063a 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala @@ -1,13 +1,11 @@ package works.iterative.ui.components.laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorDef import com.raquo.laminar.tags.HtmlTag case class Bin[Source, +Value]( label: String, description: Option[String | HtmlElement], - color: ColorDef, valueOf: Source => Value ): def map[NewValue]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala deleted file mode 100644 index 3aac0bf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala +++ /dev/null @@ -1,34 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ButtonComponentsModule: - def buttons: ButtonComponents - - trait ButtonComponents: - def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala deleted file mode 100644 index 646e73d..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala +++ /dev/null @@ -1,191 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.color.ColorDef -import com.raquo.laminar.tags.HtmlTag -import org.scalajs.dom.html.UList -import com.raquo.laminar.nodes.ReactiveHtmlElement -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind - -trait DashboardComponentsModule: - def dashboard: DashboardComponents - - // Only methods that create HtmlElements - trait DashboardComponents(using ComponentContext): - def section(label: String, children: Modifier[HtmlElement]): Div - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI - def cardInitials( - initials: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div - def count(label: String, value: Int, color: ColorDef): Span - def counts(children: HtmlElement*): Span - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - -trait DefaultDashboardComponentsModule(using ComponentContext) - extends DashboardComponentsModule: - override val dashboard: DashboardComponents = new DashboardComponents: - def section(label: String, children: Modifier[HtmlElement]): Div = - div( - cls("mt-3"), - h2( - cls("text-gray-500 text-xs font-medium uppercase tracking-wide"), - label - ), - children - ) - - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] = - ul( - cls( - "mt-3 grid gap-5 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" - ), - children - ) - - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI = - li( - div( - cls("col-span-1 flex shadow-sm rounded-md"), - initials, - div( - cls( - "flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate" - ), - div( - cls("flex-1 px-4 py-2 text-sm truncate"), - div( - cls("text-gray-900 font-medium hover:text-gray-600 truncate"), - label - ), - p(cls("text-gray-500"), children) - ) - ) - ) - ) - - def cardInitials( - id: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div = - div( - cls( - "flex-shrink-0 flex flex-col items-center justify-center w-16 text-white text-sm font-medium rounded-l-md" - ), - color.map(_(600).bg), - div(id), - children - ) - - def count(label: String, value: Int, color: ColorDef): Span = - span(color.text, title(label), s"${value}") - - def counts(children: HtmlElement*): Span = - span(interleave(children, span(" / "))) - - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div = - div( - cls("flex flex-col mt-2"), - div( - cls( - "align-middle min-w-full shadow sm:rounded-lg" - ), - L.table( - cls("min-w-full border-separate border-spacing-0"), - styleAttr("border-spacing: 0"), // Tailwind somehow doesn't work - thead( - tr( - cls("border-t border-gray-200"), - headers.map(h => - th( - cls( - "sticky top-0 z-10 px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - h - ) - ) - ) - ), - tbody(cls("bg-white divide-y divide-gray-100"), sections) - ) - ) - ) - - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - td( - cls("bg-gray-50 border-gray-200 border-t border-b"), - cls( - "sticky top-14 z-10 bg-gray-50 px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - colSpan(cs), - label - ), - mods - ) - - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - mods, - td( - cls( - "px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900 truncate" - ), - label - ), - children.map(e => - td( - cls( - "whitespace-nowrap px-2 py-2 text-sm text-gray-500" - ), - e - ) - ) - ) - - private def interleave( - a: Seq[HtmlElement], - separator: => HtmlElement - ): Seq[HtmlElement] = - if a.size < 2 then a - else a.init.flatMap(n => Seq(n, separator)) :+ a.last diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala deleted file mode 100644 index a45beaf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.FileRef - -trait DetailComponentsModule: - - def details: DetailComponents - - trait DetailComponents: - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement - - def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement - - def files(fs: List[FileRef]): HtmlElement - - def file(f: FileRef): HtmlElement - -trait DefaultDetailComponentsModule(using ctx: ComponentContext) - extends DetailComponentsModule: - self: IconsModule => - - override val details: DetailComponents = new DetailComponents: - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement = - div( - div( - cls := "px-4 sm:px-0", - h3( - cls := "text-base font-semibold leading-7 text-gray-900", - title - ), - subtitle.map(st => - p( - cls := "mt-1 max-w-2xl text-sm leading-6 text-gray-500", - st - ) - ) - // TODO: actions - ), - div( - cls := "mt-6 border-t border-gray-100", - dl( - cls := "divide-y divide-gray-100", - fields - ) - ) - ) - - override def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0", - dt( - cls := "text-sm font-medium leading-6 text-gray-900", - title - ), - dd( - cls := "mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0", - content - ) - ) - - override def files(fs: List[FileRef]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-100 rounded-md border border-gray-200", - fs.map(file) - ) - - override def file(f: FileRef): HtmlElement = - li( - cls := "flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6", - div( - cls := "flex w-0 flex-1 items-center", - icons.`paper-clip-solid`(), - div( - cls := "ml-4 flex min-w-0 flex-1 gap-2", - span( - cls := "truncate font-medium", - f.name - ), - f.size.map(size => span(cls := "flex-shrink-0 text-gray-400", size)) - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - "Uložit" - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala new file mode 100644 index 0000000..f6c583f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait FormComponents(using ctx: ComponentContext) extends LocalDateSelectModule: + def searchIcon: SvgElement + + def renderLocalDateSelect( + id: String, + labelText: Option[String], + placeholderText: Option[String], + mods: LocalDateSelect => Modifier[HtmlElement] + ): HtmlElement = + div( + labelText, + input( + idAttr(id), + nameAttr(id), + autoComplete("date"), + tpe("date"), + cls( + "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ), + placeholderText.map(placeholder(_)), + mods(localDateSelect) + ) + ) + + def searchField( + id: String, + placeholderText: Option[String] = None + )( + mods: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls := "flex-1 min-w-0", + label(ctx.messages("forms.search.label"), forId(id), cls("sr-only")), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + searchIcon + ), + input( + tpe := "search", + nameAttr := "search", + idAttr := id, + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholderText + .orElse( + ctx.messages + .opt( + s"forms.search.${id}.placeholder", + s"form.search.placeholder" + ) + ) + .map(placeholder(_)), + mods + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala deleted file mode 100644 index 7714490..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala +++ /dev/null @@ -1,82 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -/** Form components - * - * A form is a collection of sections, each of which contains a collection of - * fields. - * - * Form -> * Section -> * Field - * -> * Action - * - * Each Field has an `id`, `label` and `input` element. The `id` is used to - * link the label to the input. - * - * The `input` element can be a simple text input, or a more complex component - * such as a date picker. The caller is expected to make the input work. - */ -trait FormComponentsModule extends LocalDateSelectModule: - def forms: FormComponents - - trait FormComponents: - - /** Layout the sections and actions of a form */ - def form(mods: Modifier[HtmlElement]*)(sections: Modifier[HtmlElement]*)( - actions: Modifier[HtmlElement]* - ): HtmlElement - - /** Section layout, with a title, optional subtitle and any content. */ - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label and any content */ - def field(label: Modifier[HtmlElement])( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label, input and optional help text */ - def field( - id: String, - label: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement - - def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout an inline form element */ - def inlineForm(mods: Modifier[HtmlElement]*): HtmlElement - - /** An input field with a search icon */ - def searchField(id: String, placeholderText: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement - - /** LocalDate input */ - def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala deleted file mode 100644 index 8acbe7f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala +++ /dev/null @@ -1,28 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.Tag - -trait GenericComponentsModule: - - def generic: GenericComponents - - trait GenericComponents: - def tag(name: String, color: ColorKind): HtmlElement - def tag(t: Tag): HtmlElement = tag(t.value, t.color) - -trait DefaultGenericComponentsModule(using ComponentContext) - extends GenericComponentsModule: - override val generic: GenericComponents = new GenericComponents: - override def tag(name: String, color: ColorKind): HtmlElement = - p( - cls( - "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" - ), - color(800).text, - color(100).bg, - name - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala deleted file mode 100644 index 8cfc1d9..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala +++ /dev/null @@ -1,123 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.CustomAttrs - -trait IconsModule: - def icons: Icons - - trait Icons: - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement - def close(mods: Modifier[SvgElement]*): SvgElement - def `search-solid`(mods: Modifier[SvgElement]*): SvgElement - def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement - def `document-chart-bar-outline`(mods: Modifier[SvgElement]*): SvgElement - def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement - -trait DefaultIconsModule(using ComponentContext) extends IconsModule: - override val icons: Icons = new Icons: - import svg.* - - object aria: - val hidden = CustomAttrs.svg.ariaHidden - - private def withDefault( - mods: Seq[Modifier[SvgElement]], - default: Modifier[SvgElement] - ): Modifier[SvgElement] = - if mods.isEmpty then default else mods - - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-8 w-8"), - 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" - ) - ) - ) - - override def close(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-2 w-2"), - stroke := "currentColor", - fill := "none", - viewBox := "0 0 8 8", - path( - strokeLineCap := "round", - strokeWidth := "1.5", - d := "M1 1l6 6m0-6L1 7" - ) - ) - - override def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 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 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") - ) - ) - - override def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 text-gray-400")), - 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") - ) - ) - - override def `document-chart-bar-outline`( - mods: Modifier[SvgElement]* - ): SvgElement = - svg( - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - strokeWidth := "1.5", - stroke := "currentColor", - withDefault(mods, cls := "h-6 w-6"), - path( - strokeLineCap := "round", - strokeLineJoin := "round", - d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" - ) - ) - - override def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-5 w-5"), - cls := "flex-shrink-0 text-gray-400", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", - clipRule := "evenodd" - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala deleted file mode 100644 index 95ab654..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait LayoutModule: - def layout: LayoutComponents - - trait LayoutComponents: - def card(content: Modifier[HtmlElement]*): HtmlElement - -trait DefaultLayoutModule(using ctx: ComponentContext) extends LayoutModule: - override val layout: LayoutComponents = new LayoutComponents: - override def card(content: Modifier[HtmlElement]*): HtmlElement = - div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala deleted file mode 100644 index 5946974..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom.html.Paragraph -import com.raquo.laminar.nodes.TextNode - -trait ListComponentsModule: - - def list: ListComponents - - trait ListComponents(using ComponentContext): - def label(text: String, color: ColorKind): HtmlElement - def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI - def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] - def listSection(header: String, list: HtmlElement): Div - def navigation(sections: Modifier[HtmlElement]): HtmlElement - -trait DefaultListComponentsModule(using ComponentContext) - extends ListComponentsModule: - self: GenericComponentsModule => - - override val list: ListComponents = new ListComponents: - override def label( - text: String, - color: ColorKind - ): HtmlElement = generic.tag(text, color) - - override def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI = - li( - cls("group"), - div( - cls( - "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" - ), - avatar.map(a => - div( - cls("flex-shrink-0"), - div( - cls( - "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" - ), - a - ) - ) - ), - div( - cls("flex-1 min-w-0"), - p( - cls("text-sm font-medium text-gray-900"), - title, - span(cls("float-right"), right) - ), - subtitle.map(st => - p( - cls("text-sm text-gray-500 truncate"), - st - ) - ) - ) - ) - ) - - override def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = - ul( - cls("relative z-0 divide-y divide-gray-200"), - role("list"), - children - ) - - override def listSection( - header: String, - list: HtmlElement - ): Div = - div( - cls("relative"), - div( - cls( - "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" - ), - header - ), - list - ) - - override def navigation(sections: Modifier[HtmlElement]): HtmlElement = - navTag( - cls("flex-1 min-h-0 overflow-y-auto"), - sections - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala deleted file mode 100644 index 5b5490f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ModalComponentsModule: - - def modal: ModalComponents - - trait ModalComponents: - def modalDialog( - content: HtmlElement, - onClose: Modifier[HtmlElement] - ): HtmlElement - -trait DefaultModalComponentsModule(using ctx: ComponentContext) - extends ModalComponentsModule: - - override val modal: ModalComponents = new ModalComponents: - - override def modalDialog( - content: HtmlElement, - closeMod: Modifier[HtmlElement] - ): 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( - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")), - closeMod - ) - ) - - div( - cls("fixed inset-0 z-20 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - 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-7xl sm:align-middle" - ), - content - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala deleted file mode 100644 index b713d8a..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala +++ /dev/null @@ -1,71 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait PageComponentsModule: - - val page: PageComponents - - trait PageComponents: - def container( - children: Modifier[HtmlElement]* - ): HtmlElement - - def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement - - def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement] = emptyMod, - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement - - /** Visage for clickable text, like links - */ - def clickable: Modifier[HtmlElement] - -trait DefaultPageComponentsModule(using ComponentContext) - extends PageComponentsModule: - - override val page: PageComponents = new PageComponents: - override def container( - children: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), - children - ) - - override def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement = - div( - cls("p-8 bg-gray-100 h-full"), - header, - children - ) - - override def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement = - div( - cls("pb-5 border-b border-gray-200"), - div(cls("float-right"), right), - h1( - cls("text-2xl leading-6 font-medium text-gray-900"), - title - ), - subtitle.map( - p( - cls("text-sm font-medium text-gray-500"), - _ - ) - ) - ) - - override def clickable: Modifier[HtmlElement] = - cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala deleted file mode 100644 index f70ee87..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala +++ /dev/null @@ -1,108 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} - -trait TableComponentsModule: - def tables: TableComponents - - trait TableComponents: - def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )(table: Modifier[HtmlElement]*): HtmlElement - - def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement - - def headerRow(mods: Modifier[HtmlElement]*)( - cells: HtmlElement* - ): HtmlElement - - def dataRow(mods: Modifier[HtmlElement]*)(cells: HtmlElement*): HtmlElement - - def headerCell(content: Modifier[HtmlElement]): HtmlElement - - def dataCell(content: Modifier[HtmlElement]): HtmlElement - -trait DefaultTableComponentsModule extends TableComponentsModule: - - override lazy val tables: TableComponents = new TableComponents: - - override def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )( - table: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("px-4 sm:px-6 lg:px-8"), - div( - cls("sm:flex sm:items-center"), - div( - cls("sm:flex-auto"), - h1(cls("text-base font-semibold leading-6 text-gray-900"), title), - subtitle.map(st => p(cls("mt-2 text-sm text-gray-700"), st)) - ), - div(cls("mt-4 sm:ml-16 sm:mt-0 sm:flex-none"), actions) - ), - div( - cls("mt-8 flow-root"), - div( - cls("-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"), - div( - cls("inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"), - table - ) - ) - ) - ) - - override def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement = - table( - cls("min-w-full divide-y divide-gray-300"), - thead(header), - tbody(cls("divide-y divide-gray-200"), body) - ) - - override def headerRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-3.5 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-3.5 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-3.5 px-3")) - ) - ) - - override def dataRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - mods, - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-4 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-4 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-4 px-3")) - ) - ) - - override def headerCell(content: Modifier[HtmlElement]): HtmlElement = - th( - cls("text-left text-sm font-semibold text-gray-900"), - content - ) - - override def dataCell(content: Modifier[HtmlElement]): HtmlElement = - td( - cls("whitespace-nowrap text-sm text-gray-500"), - content - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala index b937d45..12d01ae 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala @@ -3,7 +3,7 @@ import com.raquo.laminar.api.L.{*, given} import works.iterative.ui.components.laminar.HtmlTabular import works.iterative.ui.model.tables.Tabular -import works.iterative.ui.components.laminar.TableComponentsModule +import works.iterative.ui.components.laminar.tailwind.ui.TableComponentsModule import works.iterative.core.UserMessage import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given import works.iterative.ui.components.tailwind.ComponentContext diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala new file mode 100644 index 0000000..87f3e8d --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala @@ -0,0 +1,54 @@ +package works.iterative.ui.components.laminar.tailwind.color + +import com.raquo.laminar.api.L.{*, given} + +/** Complete color definition that can be rendered to CSS. + * + * Includes the area, kind and weight of the color. + */ +case class Color(area: ColorArea, color: ColorDef): + def toCSS: String = s"${area.name}-${color.toCSS}" + +object Color: + import ColorDef.given + + given colorToCSS: Conversion[Color, HtmlMod] with + def apply(c: Color) = cls(c.toCSS) + + given colorToSVGCSS: Conversion[Color, SvgMod] with + def apply(c: Color) = svg.cls(c.toCSS) + + given colorSignalToCSS: Conversion[Signal[Color], HtmlMod] with + def apply(c: Signal[Color]) = cls <-- c.map(_.toCSS) + + given colorSignalToSVGCSS: Conversion[Signal[Color], SvgMod] with + def apply(c: Signal[Color]) = svg.cls <-- c.map(_.toCSS) + + def current = ColorDef(ColorKind.current) + def inherit = ColorDef(ColorKind.inherit) + def transp = ColorDef(ColorKind.transp) + def auto = ColorDef(ColorKind.auto) + def black = ColorDef(ColorKind.black) + def white = ColorDef(ColorKind.white) + def slate(weight: ColorWeight) = ColorDef(ColorKind.slate, weight) + def gray(weight: ColorWeight) = ColorDef(ColorKind.gray, weight) + def zinc(weight: ColorWeight) = ColorDef(ColorKind.zinc, weight) + def neutral(weight: ColorWeight) = ColorDef(ColorKind.neutral, weight) + def stone(weight: ColorWeight) = ColorDef(ColorKind.stone, weight) + def red(weight: ColorWeight) = ColorDef(ColorKind.red, weight) + def orange(weight: ColorWeight) = ColorDef(ColorKind.orange, weight) + def amber(weight: ColorWeight) = ColorDef(ColorKind.amber, weight) + def yellow(weight: ColorWeight) = ColorDef(ColorKind.yellow, weight) + def lime(weight: ColorWeight) = ColorDef(ColorKind.lime, weight) + def green(weight: ColorWeight) = ColorDef(ColorKind.green, weight) + def emerald(weight: ColorWeight) = ColorDef(ColorKind.emerald, weight) + def teal(weight: ColorWeight) = ColorDef(ColorKind.teal, weight) + def cyan(weight: ColorWeight) = ColorDef(ColorKind.cyan, weight) + def sky(weight: ColorWeight) = ColorDef(ColorKind.sky, weight) + def blue(weight: ColorWeight) = ColorDef(ColorKind.blue, weight) + def indigo(weight: ColorWeight) = ColorDef(ColorKind.indigo, weight) + def violet(weight: ColorWeight) = ColorDef(ColorKind.violet, weight) + def purple(weight: ColorWeight) = ColorDef(ColorKind.purple, weight) + def fuchsia(weight: ColorWeight) = ColorDef(ColorKind.fuchsia, weight) + def pink(weight: ColorWeight) = ColorDef(ColorKind.pink, weight) + def rose(weight: ColorWeight) = ColorDef(ColorKind.rose, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala new file mode 100644 index 0000000..4130840 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala @@ -0,0 +1,16 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines the area the color should apply to, eg. background, text, border, + * etc. + */ +enum ColorArea(val name: String): + case bg extends ColorArea("bg") + case text extends ColorArea("text") + case decoration extends ColorArea("decoration") + case border extends ColorArea("border") + case outline extends ColorArea("outline") + case divide extends ColorArea("divide") + case ring extends ColorArea("ring") + case ringOffset extends ColorArea("ring-offset") + case shadow extends ColorArea("shadow") + case accent extends ColorArea("accent") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala new file mode 100644 index 0000000..fab9404 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala @@ -0,0 +1,43 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** A combination of ColorKind and ColorWeight, if applicable. + * + * By applying area we get the full Color definition. + */ +sealed trait ColorDef: + def toCSS: String + + def bg = Color(ColorArea.bg, this) + def text = Color(ColorArea.text, this) + def decoration = Color(ColorArea.decoration, this) + def border = Color(ColorArea.border, this) + def outline = Color(ColorArea.outline, this) + def divide = Color(ColorArea.divide, this) + def ring = Color(ColorArea.ring, this) + def ringOffset = Color(ColorArea.ringOffset, this) + def shadow = Color(ColorArea.shadow, this) + def accent = Color(ColorArea.accent, this) + +// TODO: create a macro that will output the tailwind class as string directly during compilation, where possible. +object ColorDef: + case class WeightedColorDef( + kind: ColorKind, + weight: ColorWeight + ) extends ColorDef: + override def toCSS: String = s"${kind.name}-${weight.value}" + + case class UnweightedColorDef( + kind: ColorKind + ) extends ColorDef: + override def toCSS: String = kind.name + + // TODO: check that the kind is valid unweighted kind + // that means current, inherit, auto, transparent, black, white + // tried using implicit evidence, but the type inference for enumerations + // tends to generalize to the enum, instead of the real type + def apply[T <: ColorKind](kind: T)(using + ev: T <:< ColorKind.Unweighted + ): ColorDef = + UnweightedColorDef(kind) + def apply(kind: ColorKind, weight: ColorWeight): ColorDef = + WeightedColorDef(kind, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala index a2c637c..bfa063a 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala @@ -1,13 +1,11 @@ package works.iterative.ui.components.laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorDef import com.raquo.laminar.tags.HtmlTag case class Bin[Source, +Value]( label: String, description: Option[String | HtmlElement], - color: ColorDef, valueOf: Source => Value ): def map[NewValue]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala deleted file mode 100644 index 3aac0bf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala +++ /dev/null @@ -1,34 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ButtonComponentsModule: - def buttons: ButtonComponents - - trait ButtonComponents: - def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala deleted file mode 100644 index 646e73d..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala +++ /dev/null @@ -1,191 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.color.ColorDef -import com.raquo.laminar.tags.HtmlTag -import org.scalajs.dom.html.UList -import com.raquo.laminar.nodes.ReactiveHtmlElement -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind - -trait DashboardComponentsModule: - def dashboard: DashboardComponents - - // Only methods that create HtmlElements - trait DashboardComponents(using ComponentContext): - def section(label: String, children: Modifier[HtmlElement]): Div - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI - def cardInitials( - initials: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div - def count(label: String, value: Int, color: ColorDef): Span - def counts(children: HtmlElement*): Span - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - -trait DefaultDashboardComponentsModule(using ComponentContext) - extends DashboardComponentsModule: - override val dashboard: DashboardComponents = new DashboardComponents: - def section(label: String, children: Modifier[HtmlElement]): Div = - div( - cls("mt-3"), - h2( - cls("text-gray-500 text-xs font-medium uppercase tracking-wide"), - label - ), - children - ) - - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] = - ul( - cls( - "mt-3 grid gap-5 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" - ), - children - ) - - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI = - li( - div( - cls("col-span-1 flex shadow-sm rounded-md"), - initials, - div( - cls( - "flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate" - ), - div( - cls("flex-1 px-4 py-2 text-sm truncate"), - div( - cls("text-gray-900 font-medium hover:text-gray-600 truncate"), - label - ), - p(cls("text-gray-500"), children) - ) - ) - ) - ) - - def cardInitials( - id: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div = - div( - cls( - "flex-shrink-0 flex flex-col items-center justify-center w-16 text-white text-sm font-medium rounded-l-md" - ), - color.map(_(600).bg), - div(id), - children - ) - - def count(label: String, value: Int, color: ColorDef): Span = - span(color.text, title(label), s"${value}") - - def counts(children: HtmlElement*): Span = - span(interleave(children, span(" / "))) - - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div = - div( - cls("flex flex-col mt-2"), - div( - cls( - "align-middle min-w-full shadow sm:rounded-lg" - ), - L.table( - cls("min-w-full border-separate border-spacing-0"), - styleAttr("border-spacing: 0"), // Tailwind somehow doesn't work - thead( - tr( - cls("border-t border-gray-200"), - headers.map(h => - th( - cls( - "sticky top-0 z-10 px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - h - ) - ) - ) - ), - tbody(cls("bg-white divide-y divide-gray-100"), sections) - ) - ) - ) - - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - td( - cls("bg-gray-50 border-gray-200 border-t border-b"), - cls( - "sticky top-14 z-10 bg-gray-50 px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - colSpan(cs), - label - ), - mods - ) - - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - mods, - td( - cls( - "px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900 truncate" - ), - label - ), - children.map(e => - td( - cls( - "whitespace-nowrap px-2 py-2 text-sm text-gray-500" - ), - e - ) - ) - ) - - private def interleave( - a: Seq[HtmlElement], - separator: => HtmlElement - ): Seq[HtmlElement] = - if a.size < 2 then a - else a.init.flatMap(n => Seq(n, separator)) :+ a.last diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala deleted file mode 100644 index a45beaf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.FileRef - -trait DetailComponentsModule: - - def details: DetailComponents - - trait DetailComponents: - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement - - def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement - - def files(fs: List[FileRef]): HtmlElement - - def file(f: FileRef): HtmlElement - -trait DefaultDetailComponentsModule(using ctx: ComponentContext) - extends DetailComponentsModule: - self: IconsModule => - - override val details: DetailComponents = new DetailComponents: - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement = - div( - div( - cls := "px-4 sm:px-0", - h3( - cls := "text-base font-semibold leading-7 text-gray-900", - title - ), - subtitle.map(st => - p( - cls := "mt-1 max-w-2xl text-sm leading-6 text-gray-500", - st - ) - ) - // TODO: actions - ), - div( - cls := "mt-6 border-t border-gray-100", - dl( - cls := "divide-y divide-gray-100", - fields - ) - ) - ) - - override def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0", - dt( - cls := "text-sm font-medium leading-6 text-gray-900", - title - ), - dd( - cls := "mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0", - content - ) - ) - - override def files(fs: List[FileRef]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-100 rounded-md border border-gray-200", - fs.map(file) - ) - - override def file(f: FileRef): HtmlElement = - li( - cls := "flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6", - div( - cls := "flex w-0 flex-1 items-center", - icons.`paper-clip-solid`(), - div( - cls := "ml-4 flex min-w-0 flex-1 gap-2", - span( - cls := "truncate font-medium", - f.name - ), - f.size.map(size => span(cls := "flex-shrink-0 text-gray-400", size)) - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - "Uložit" - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala new file mode 100644 index 0000000..f6c583f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait FormComponents(using ctx: ComponentContext) extends LocalDateSelectModule: + def searchIcon: SvgElement + + def renderLocalDateSelect( + id: String, + labelText: Option[String], + placeholderText: Option[String], + mods: LocalDateSelect => Modifier[HtmlElement] + ): HtmlElement = + div( + labelText, + input( + idAttr(id), + nameAttr(id), + autoComplete("date"), + tpe("date"), + cls( + "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ), + placeholderText.map(placeholder(_)), + mods(localDateSelect) + ) + ) + + def searchField( + id: String, + placeholderText: Option[String] = None + )( + mods: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls := "flex-1 min-w-0", + label(ctx.messages("forms.search.label"), forId(id), cls("sr-only")), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + searchIcon + ), + input( + tpe := "search", + nameAttr := "search", + idAttr := id, + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholderText + .orElse( + ctx.messages + .opt( + s"forms.search.${id}.placeholder", + s"form.search.placeholder" + ) + ) + .map(placeholder(_)), + mods + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala deleted file mode 100644 index 7714490..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala +++ /dev/null @@ -1,82 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -/** Form components - * - * A form is a collection of sections, each of which contains a collection of - * fields. - * - * Form -> * Section -> * Field - * -> * Action - * - * Each Field has an `id`, `label` and `input` element. The `id` is used to - * link the label to the input. - * - * The `input` element can be a simple text input, or a more complex component - * such as a date picker. The caller is expected to make the input work. - */ -trait FormComponentsModule extends LocalDateSelectModule: - def forms: FormComponents - - trait FormComponents: - - /** Layout the sections and actions of a form */ - def form(mods: Modifier[HtmlElement]*)(sections: Modifier[HtmlElement]*)( - actions: Modifier[HtmlElement]* - ): HtmlElement - - /** Section layout, with a title, optional subtitle and any content. */ - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label and any content */ - def field(label: Modifier[HtmlElement])( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label, input and optional help text */ - def field( - id: String, - label: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement - - def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout an inline form element */ - def inlineForm(mods: Modifier[HtmlElement]*): HtmlElement - - /** An input field with a search icon */ - def searchField(id: String, placeholderText: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement - - /** LocalDate input */ - def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala deleted file mode 100644 index 8acbe7f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala +++ /dev/null @@ -1,28 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.Tag - -trait GenericComponentsModule: - - def generic: GenericComponents - - trait GenericComponents: - def tag(name: String, color: ColorKind): HtmlElement - def tag(t: Tag): HtmlElement = tag(t.value, t.color) - -trait DefaultGenericComponentsModule(using ComponentContext) - extends GenericComponentsModule: - override val generic: GenericComponents = new GenericComponents: - override def tag(name: String, color: ColorKind): HtmlElement = - p( - cls( - "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" - ), - color(800).text, - color(100).bg, - name - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala deleted file mode 100644 index 8cfc1d9..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala +++ /dev/null @@ -1,123 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.CustomAttrs - -trait IconsModule: - def icons: Icons - - trait Icons: - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement - def close(mods: Modifier[SvgElement]*): SvgElement - def `search-solid`(mods: Modifier[SvgElement]*): SvgElement - def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement - def `document-chart-bar-outline`(mods: Modifier[SvgElement]*): SvgElement - def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement - -trait DefaultIconsModule(using ComponentContext) extends IconsModule: - override val icons: Icons = new Icons: - import svg.* - - object aria: - val hidden = CustomAttrs.svg.ariaHidden - - private def withDefault( - mods: Seq[Modifier[SvgElement]], - default: Modifier[SvgElement] - ): Modifier[SvgElement] = - if mods.isEmpty then default else mods - - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-8 w-8"), - 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" - ) - ) - ) - - override def close(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-2 w-2"), - stroke := "currentColor", - fill := "none", - viewBox := "0 0 8 8", - path( - strokeLineCap := "round", - strokeWidth := "1.5", - d := "M1 1l6 6m0-6L1 7" - ) - ) - - override def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 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 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") - ) - ) - - override def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 text-gray-400")), - 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") - ) - ) - - override def `document-chart-bar-outline`( - mods: Modifier[SvgElement]* - ): SvgElement = - svg( - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - strokeWidth := "1.5", - stroke := "currentColor", - withDefault(mods, cls := "h-6 w-6"), - path( - strokeLineCap := "round", - strokeLineJoin := "round", - d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" - ) - ) - - override def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-5 w-5"), - cls := "flex-shrink-0 text-gray-400", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", - clipRule := "evenodd" - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala deleted file mode 100644 index 95ab654..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait LayoutModule: - def layout: LayoutComponents - - trait LayoutComponents: - def card(content: Modifier[HtmlElement]*): HtmlElement - -trait DefaultLayoutModule(using ctx: ComponentContext) extends LayoutModule: - override val layout: LayoutComponents = new LayoutComponents: - override def card(content: Modifier[HtmlElement]*): HtmlElement = - div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala deleted file mode 100644 index 5946974..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom.html.Paragraph -import com.raquo.laminar.nodes.TextNode - -trait ListComponentsModule: - - def list: ListComponents - - trait ListComponents(using ComponentContext): - def label(text: String, color: ColorKind): HtmlElement - def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI - def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] - def listSection(header: String, list: HtmlElement): Div - def navigation(sections: Modifier[HtmlElement]): HtmlElement - -trait DefaultListComponentsModule(using ComponentContext) - extends ListComponentsModule: - self: GenericComponentsModule => - - override val list: ListComponents = new ListComponents: - override def label( - text: String, - color: ColorKind - ): HtmlElement = generic.tag(text, color) - - override def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI = - li( - cls("group"), - div( - cls( - "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" - ), - avatar.map(a => - div( - cls("flex-shrink-0"), - div( - cls( - "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" - ), - a - ) - ) - ), - div( - cls("flex-1 min-w-0"), - p( - cls("text-sm font-medium text-gray-900"), - title, - span(cls("float-right"), right) - ), - subtitle.map(st => - p( - cls("text-sm text-gray-500 truncate"), - st - ) - ) - ) - ) - ) - - override def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = - ul( - cls("relative z-0 divide-y divide-gray-200"), - role("list"), - children - ) - - override def listSection( - header: String, - list: HtmlElement - ): Div = - div( - cls("relative"), - div( - cls( - "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" - ), - header - ), - list - ) - - override def navigation(sections: Modifier[HtmlElement]): HtmlElement = - navTag( - cls("flex-1 min-h-0 overflow-y-auto"), - sections - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala deleted file mode 100644 index 5b5490f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ModalComponentsModule: - - def modal: ModalComponents - - trait ModalComponents: - def modalDialog( - content: HtmlElement, - onClose: Modifier[HtmlElement] - ): HtmlElement - -trait DefaultModalComponentsModule(using ctx: ComponentContext) - extends ModalComponentsModule: - - override val modal: ModalComponents = new ModalComponents: - - override def modalDialog( - content: HtmlElement, - closeMod: Modifier[HtmlElement] - ): 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( - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")), - closeMod - ) - ) - - div( - cls("fixed inset-0 z-20 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - 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-7xl sm:align-middle" - ), - content - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala deleted file mode 100644 index b713d8a..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala +++ /dev/null @@ -1,71 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait PageComponentsModule: - - val page: PageComponents - - trait PageComponents: - def container( - children: Modifier[HtmlElement]* - ): HtmlElement - - def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement - - def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement] = emptyMod, - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement - - /** Visage for clickable text, like links - */ - def clickable: Modifier[HtmlElement] - -trait DefaultPageComponentsModule(using ComponentContext) - extends PageComponentsModule: - - override val page: PageComponents = new PageComponents: - override def container( - children: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), - children - ) - - override def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement = - div( - cls("p-8 bg-gray-100 h-full"), - header, - children - ) - - override def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement = - div( - cls("pb-5 border-b border-gray-200"), - div(cls("float-right"), right), - h1( - cls("text-2xl leading-6 font-medium text-gray-900"), - title - ), - subtitle.map( - p( - cls("text-sm font-medium text-gray-500"), - _ - ) - ) - ) - - override def clickable: Modifier[HtmlElement] = - cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala deleted file mode 100644 index f70ee87..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala +++ /dev/null @@ -1,108 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} - -trait TableComponentsModule: - def tables: TableComponents - - trait TableComponents: - def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )(table: Modifier[HtmlElement]*): HtmlElement - - def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement - - def headerRow(mods: Modifier[HtmlElement]*)( - cells: HtmlElement* - ): HtmlElement - - def dataRow(mods: Modifier[HtmlElement]*)(cells: HtmlElement*): HtmlElement - - def headerCell(content: Modifier[HtmlElement]): HtmlElement - - def dataCell(content: Modifier[HtmlElement]): HtmlElement - -trait DefaultTableComponentsModule extends TableComponentsModule: - - override lazy val tables: TableComponents = new TableComponents: - - override def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )( - table: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("px-4 sm:px-6 lg:px-8"), - div( - cls("sm:flex sm:items-center"), - div( - cls("sm:flex-auto"), - h1(cls("text-base font-semibold leading-6 text-gray-900"), title), - subtitle.map(st => p(cls("mt-2 text-sm text-gray-700"), st)) - ), - div(cls("mt-4 sm:ml-16 sm:mt-0 sm:flex-none"), actions) - ), - div( - cls("mt-8 flow-root"), - div( - cls("-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"), - div( - cls("inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"), - table - ) - ) - ) - ) - - override def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement = - table( - cls("min-w-full divide-y divide-gray-300"), - thead(header), - tbody(cls("divide-y divide-gray-200"), body) - ) - - override def headerRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-3.5 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-3.5 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-3.5 px-3")) - ) - ) - - override def dataRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - mods, - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-4 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-4 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-4 px-3")) - ) - ) - - override def headerCell(content: Modifier[HtmlElement]): HtmlElement = - th( - cls("text-left text-sm font-semibold text-gray-900"), - content - ) - - override def dataCell(content: Modifier[HtmlElement]): HtmlElement = - td( - cls("whitespace-nowrap text-sm text-gray-500"), - content - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala index b937d45..12d01ae 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala @@ -3,7 +3,7 @@ import com.raquo.laminar.api.L.{*, given} import works.iterative.ui.components.laminar.HtmlTabular import works.iterative.ui.model.tables.Tabular -import works.iterative.ui.components.laminar.TableComponentsModule +import works.iterative.ui.components.laminar.tailwind.ui.TableComponentsModule import works.iterative.core.UserMessage import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given import works.iterative.ui.components.tailwind.ComponentContext diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala new file mode 100644 index 0000000..87f3e8d --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala @@ -0,0 +1,54 @@ +package works.iterative.ui.components.laminar.tailwind.color + +import com.raquo.laminar.api.L.{*, given} + +/** Complete color definition that can be rendered to CSS. + * + * Includes the area, kind and weight of the color. + */ +case class Color(area: ColorArea, color: ColorDef): + def toCSS: String = s"${area.name}-${color.toCSS}" + +object Color: + import ColorDef.given + + given colorToCSS: Conversion[Color, HtmlMod] with + def apply(c: Color) = cls(c.toCSS) + + given colorToSVGCSS: Conversion[Color, SvgMod] with + def apply(c: Color) = svg.cls(c.toCSS) + + given colorSignalToCSS: Conversion[Signal[Color], HtmlMod] with + def apply(c: Signal[Color]) = cls <-- c.map(_.toCSS) + + given colorSignalToSVGCSS: Conversion[Signal[Color], SvgMod] with + def apply(c: Signal[Color]) = svg.cls <-- c.map(_.toCSS) + + def current = ColorDef(ColorKind.current) + def inherit = ColorDef(ColorKind.inherit) + def transp = ColorDef(ColorKind.transp) + def auto = ColorDef(ColorKind.auto) + def black = ColorDef(ColorKind.black) + def white = ColorDef(ColorKind.white) + def slate(weight: ColorWeight) = ColorDef(ColorKind.slate, weight) + def gray(weight: ColorWeight) = ColorDef(ColorKind.gray, weight) + def zinc(weight: ColorWeight) = ColorDef(ColorKind.zinc, weight) + def neutral(weight: ColorWeight) = ColorDef(ColorKind.neutral, weight) + def stone(weight: ColorWeight) = ColorDef(ColorKind.stone, weight) + def red(weight: ColorWeight) = ColorDef(ColorKind.red, weight) + def orange(weight: ColorWeight) = ColorDef(ColorKind.orange, weight) + def amber(weight: ColorWeight) = ColorDef(ColorKind.amber, weight) + def yellow(weight: ColorWeight) = ColorDef(ColorKind.yellow, weight) + def lime(weight: ColorWeight) = ColorDef(ColorKind.lime, weight) + def green(weight: ColorWeight) = ColorDef(ColorKind.green, weight) + def emerald(weight: ColorWeight) = ColorDef(ColorKind.emerald, weight) + def teal(weight: ColorWeight) = ColorDef(ColorKind.teal, weight) + def cyan(weight: ColorWeight) = ColorDef(ColorKind.cyan, weight) + def sky(weight: ColorWeight) = ColorDef(ColorKind.sky, weight) + def blue(weight: ColorWeight) = ColorDef(ColorKind.blue, weight) + def indigo(weight: ColorWeight) = ColorDef(ColorKind.indigo, weight) + def violet(weight: ColorWeight) = ColorDef(ColorKind.violet, weight) + def purple(weight: ColorWeight) = ColorDef(ColorKind.purple, weight) + def fuchsia(weight: ColorWeight) = ColorDef(ColorKind.fuchsia, weight) + def pink(weight: ColorWeight) = ColorDef(ColorKind.pink, weight) + def rose(weight: ColorWeight) = ColorDef(ColorKind.rose, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala new file mode 100644 index 0000000..4130840 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala @@ -0,0 +1,16 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines the area the color should apply to, eg. background, text, border, + * etc. + */ +enum ColorArea(val name: String): + case bg extends ColorArea("bg") + case text extends ColorArea("text") + case decoration extends ColorArea("decoration") + case border extends ColorArea("border") + case outline extends ColorArea("outline") + case divide extends ColorArea("divide") + case ring extends ColorArea("ring") + case ringOffset extends ColorArea("ring-offset") + case shadow extends ColorArea("shadow") + case accent extends ColorArea("accent") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala new file mode 100644 index 0000000..fab9404 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala @@ -0,0 +1,43 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** A combination of ColorKind and ColorWeight, if applicable. + * + * By applying area we get the full Color definition. + */ +sealed trait ColorDef: + def toCSS: String + + def bg = Color(ColorArea.bg, this) + def text = Color(ColorArea.text, this) + def decoration = Color(ColorArea.decoration, this) + def border = Color(ColorArea.border, this) + def outline = Color(ColorArea.outline, this) + def divide = Color(ColorArea.divide, this) + def ring = Color(ColorArea.ring, this) + def ringOffset = Color(ColorArea.ringOffset, this) + def shadow = Color(ColorArea.shadow, this) + def accent = Color(ColorArea.accent, this) + +// TODO: create a macro that will output the tailwind class as string directly during compilation, where possible. +object ColorDef: + case class WeightedColorDef( + kind: ColorKind, + weight: ColorWeight + ) extends ColorDef: + override def toCSS: String = s"${kind.name}-${weight.value}" + + case class UnweightedColorDef( + kind: ColorKind + ) extends ColorDef: + override def toCSS: String = kind.name + + // TODO: check that the kind is valid unweighted kind + // that means current, inherit, auto, transparent, black, white + // tried using implicit evidence, but the type inference for enumerations + // tends to generalize to the enum, instead of the real type + def apply[T <: ColorKind](kind: T)(using + ev: T <:< ColorKind.Unweighted + ): ColorDef = + UnweightedColorDef(kind) + def apply(kind: ColorKind, weight: ColorWeight): ColorDef = + WeightedColorDef(kind, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala new file mode 100644 index 0000000..865166b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala @@ -0,0 +1,47 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines what color should be used, without specifying the area or weight. + */ +sealed abstract class ColorKind private (val name: String): + def apply(weight: ColorWeight): ColorDef = + ColorDef.WeightedColorDef(this, weight) + +object ColorKind: + trait Unweighted: + self: ColorKind => + override def apply(weight: ColorWeight): ColorDef = + ColorDef.UnweightedColorDef(self) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case object current extends ColorKind("current") with Unweighted + case object inherit extends ColorKind("inherit") with Unweighted + // Not present in for all methods + case object transp extends ColorKind("transparent") with Unweighted + // Seen in accent, not preset otherwise + case object auto extends ColorKind("auto") with Unweighted + // Black and white do not have weight + case object black extends ColorKind("black") with Unweighted + case object white extends ColorKind("white") with Unweighted + case object slate extends ColorKind("slate") + case object gray extends ColorKind("gray") + case object zinc extends ColorKind("zinc") + case object neutral extends ColorKind("neutral") + case object stone extends ColorKind("stone") + case object red extends ColorKind("red") + case object orange extends ColorKind("orange") + case object amber extends ColorKind("amber") + case object yellow extends ColorKind("yellow") + case object lime extends ColorKind("lime") + case object green extends ColorKind("green") + case object emerald extends ColorKind("emerald") + case object teal extends ColorKind("teal") + case object cyan extends ColorKind("cyan") + case object sky extends ColorKind("sky") + case object blue extends ColorKind("blue") + case object indigo extends ColorKind("indigo") + case object violet extends ColorKind("violet") + case object purple extends ColorKind("purple") + case object fuchsia extends ColorKind("fuchsia") + case object pink extends ColorKind("pink") + case object rose extends ColorKind("rose") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala index a2c637c..bfa063a 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala @@ -1,13 +1,11 @@ package works.iterative.ui.components.laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorDef import com.raquo.laminar.tags.HtmlTag case class Bin[Source, +Value]( label: String, description: Option[String | HtmlElement], - color: ColorDef, valueOf: Source => Value ): def map[NewValue]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala deleted file mode 100644 index 3aac0bf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala +++ /dev/null @@ -1,34 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ButtonComponentsModule: - def buttons: ButtonComponents - - trait ButtonComponents: - def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala deleted file mode 100644 index 646e73d..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala +++ /dev/null @@ -1,191 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.color.ColorDef -import com.raquo.laminar.tags.HtmlTag -import org.scalajs.dom.html.UList -import com.raquo.laminar.nodes.ReactiveHtmlElement -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind - -trait DashboardComponentsModule: - def dashboard: DashboardComponents - - // Only methods that create HtmlElements - trait DashboardComponents(using ComponentContext): - def section(label: String, children: Modifier[HtmlElement]): Div - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI - def cardInitials( - initials: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div - def count(label: String, value: Int, color: ColorDef): Span - def counts(children: HtmlElement*): Span - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - -trait DefaultDashboardComponentsModule(using ComponentContext) - extends DashboardComponentsModule: - override val dashboard: DashboardComponents = new DashboardComponents: - def section(label: String, children: Modifier[HtmlElement]): Div = - div( - cls("mt-3"), - h2( - cls("text-gray-500 text-xs font-medium uppercase tracking-wide"), - label - ), - children - ) - - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] = - ul( - cls( - "mt-3 grid gap-5 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" - ), - children - ) - - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI = - li( - div( - cls("col-span-1 flex shadow-sm rounded-md"), - initials, - div( - cls( - "flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate" - ), - div( - cls("flex-1 px-4 py-2 text-sm truncate"), - div( - cls("text-gray-900 font-medium hover:text-gray-600 truncate"), - label - ), - p(cls("text-gray-500"), children) - ) - ) - ) - ) - - def cardInitials( - id: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div = - div( - cls( - "flex-shrink-0 flex flex-col items-center justify-center w-16 text-white text-sm font-medium rounded-l-md" - ), - color.map(_(600).bg), - div(id), - children - ) - - def count(label: String, value: Int, color: ColorDef): Span = - span(color.text, title(label), s"${value}") - - def counts(children: HtmlElement*): Span = - span(interleave(children, span(" / "))) - - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div = - div( - cls("flex flex-col mt-2"), - div( - cls( - "align-middle min-w-full shadow sm:rounded-lg" - ), - L.table( - cls("min-w-full border-separate border-spacing-0"), - styleAttr("border-spacing: 0"), // Tailwind somehow doesn't work - thead( - tr( - cls("border-t border-gray-200"), - headers.map(h => - th( - cls( - "sticky top-0 z-10 px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - h - ) - ) - ) - ), - tbody(cls("bg-white divide-y divide-gray-100"), sections) - ) - ) - ) - - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - td( - cls("bg-gray-50 border-gray-200 border-t border-b"), - cls( - "sticky top-14 z-10 bg-gray-50 px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - colSpan(cs), - label - ), - mods - ) - - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - mods, - td( - cls( - "px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900 truncate" - ), - label - ), - children.map(e => - td( - cls( - "whitespace-nowrap px-2 py-2 text-sm text-gray-500" - ), - e - ) - ) - ) - - private def interleave( - a: Seq[HtmlElement], - separator: => HtmlElement - ): Seq[HtmlElement] = - if a.size < 2 then a - else a.init.flatMap(n => Seq(n, separator)) :+ a.last diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala deleted file mode 100644 index a45beaf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.FileRef - -trait DetailComponentsModule: - - def details: DetailComponents - - trait DetailComponents: - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement - - def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement - - def files(fs: List[FileRef]): HtmlElement - - def file(f: FileRef): HtmlElement - -trait DefaultDetailComponentsModule(using ctx: ComponentContext) - extends DetailComponentsModule: - self: IconsModule => - - override val details: DetailComponents = new DetailComponents: - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement = - div( - div( - cls := "px-4 sm:px-0", - h3( - cls := "text-base font-semibold leading-7 text-gray-900", - title - ), - subtitle.map(st => - p( - cls := "mt-1 max-w-2xl text-sm leading-6 text-gray-500", - st - ) - ) - // TODO: actions - ), - div( - cls := "mt-6 border-t border-gray-100", - dl( - cls := "divide-y divide-gray-100", - fields - ) - ) - ) - - override def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0", - dt( - cls := "text-sm font-medium leading-6 text-gray-900", - title - ), - dd( - cls := "mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0", - content - ) - ) - - override def files(fs: List[FileRef]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-100 rounded-md border border-gray-200", - fs.map(file) - ) - - override def file(f: FileRef): HtmlElement = - li( - cls := "flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6", - div( - cls := "flex w-0 flex-1 items-center", - icons.`paper-clip-solid`(), - div( - cls := "ml-4 flex min-w-0 flex-1 gap-2", - span( - cls := "truncate font-medium", - f.name - ), - f.size.map(size => span(cls := "flex-shrink-0 text-gray-400", size)) - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - "Uložit" - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala new file mode 100644 index 0000000..f6c583f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait FormComponents(using ctx: ComponentContext) extends LocalDateSelectModule: + def searchIcon: SvgElement + + def renderLocalDateSelect( + id: String, + labelText: Option[String], + placeholderText: Option[String], + mods: LocalDateSelect => Modifier[HtmlElement] + ): HtmlElement = + div( + labelText, + input( + idAttr(id), + nameAttr(id), + autoComplete("date"), + tpe("date"), + cls( + "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ), + placeholderText.map(placeholder(_)), + mods(localDateSelect) + ) + ) + + def searchField( + id: String, + placeholderText: Option[String] = None + )( + mods: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls := "flex-1 min-w-0", + label(ctx.messages("forms.search.label"), forId(id), cls("sr-only")), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + searchIcon + ), + input( + tpe := "search", + nameAttr := "search", + idAttr := id, + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholderText + .orElse( + ctx.messages + .opt( + s"forms.search.${id}.placeholder", + s"form.search.placeholder" + ) + ) + .map(placeholder(_)), + mods + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala deleted file mode 100644 index 7714490..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala +++ /dev/null @@ -1,82 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -/** Form components - * - * A form is a collection of sections, each of which contains a collection of - * fields. - * - * Form -> * Section -> * Field - * -> * Action - * - * Each Field has an `id`, `label` and `input` element. The `id` is used to - * link the label to the input. - * - * The `input` element can be a simple text input, or a more complex component - * such as a date picker. The caller is expected to make the input work. - */ -trait FormComponentsModule extends LocalDateSelectModule: - def forms: FormComponents - - trait FormComponents: - - /** Layout the sections and actions of a form */ - def form(mods: Modifier[HtmlElement]*)(sections: Modifier[HtmlElement]*)( - actions: Modifier[HtmlElement]* - ): HtmlElement - - /** Section layout, with a title, optional subtitle and any content. */ - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label and any content */ - def field(label: Modifier[HtmlElement])( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label, input and optional help text */ - def field( - id: String, - label: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement - - def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout an inline form element */ - def inlineForm(mods: Modifier[HtmlElement]*): HtmlElement - - /** An input field with a search icon */ - def searchField(id: String, placeholderText: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement - - /** LocalDate input */ - def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala deleted file mode 100644 index 8acbe7f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala +++ /dev/null @@ -1,28 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.Tag - -trait GenericComponentsModule: - - def generic: GenericComponents - - trait GenericComponents: - def tag(name: String, color: ColorKind): HtmlElement - def tag(t: Tag): HtmlElement = tag(t.value, t.color) - -trait DefaultGenericComponentsModule(using ComponentContext) - extends GenericComponentsModule: - override val generic: GenericComponents = new GenericComponents: - override def tag(name: String, color: ColorKind): HtmlElement = - p( - cls( - "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" - ), - color(800).text, - color(100).bg, - name - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala deleted file mode 100644 index 8cfc1d9..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala +++ /dev/null @@ -1,123 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.CustomAttrs - -trait IconsModule: - def icons: Icons - - trait Icons: - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement - def close(mods: Modifier[SvgElement]*): SvgElement - def `search-solid`(mods: Modifier[SvgElement]*): SvgElement - def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement - def `document-chart-bar-outline`(mods: Modifier[SvgElement]*): SvgElement - def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement - -trait DefaultIconsModule(using ComponentContext) extends IconsModule: - override val icons: Icons = new Icons: - import svg.* - - object aria: - val hidden = CustomAttrs.svg.ariaHidden - - private def withDefault( - mods: Seq[Modifier[SvgElement]], - default: Modifier[SvgElement] - ): Modifier[SvgElement] = - if mods.isEmpty then default else mods - - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-8 w-8"), - 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" - ) - ) - ) - - override def close(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-2 w-2"), - stroke := "currentColor", - fill := "none", - viewBox := "0 0 8 8", - path( - strokeLineCap := "round", - strokeWidth := "1.5", - d := "M1 1l6 6m0-6L1 7" - ) - ) - - override def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 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 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") - ) - ) - - override def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 text-gray-400")), - 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") - ) - ) - - override def `document-chart-bar-outline`( - mods: Modifier[SvgElement]* - ): SvgElement = - svg( - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - strokeWidth := "1.5", - stroke := "currentColor", - withDefault(mods, cls := "h-6 w-6"), - path( - strokeLineCap := "round", - strokeLineJoin := "round", - d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" - ) - ) - - override def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-5 w-5"), - cls := "flex-shrink-0 text-gray-400", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", - clipRule := "evenodd" - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala deleted file mode 100644 index 95ab654..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait LayoutModule: - def layout: LayoutComponents - - trait LayoutComponents: - def card(content: Modifier[HtmlElement]*): HtmlElement - -trait DefaultLayoutModule(using ctx: ComponentContext) extends LayoutModule: - override val layout: LayoutComponents = new LayoutComponents: - override def card(content: Modifier[HtmlElement]*): HtmlElement = - div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala deleted file mode 100644 index 5946974..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom.html.Paragraph -import com.raquo.laminar.nodes.TextNode - -trait ListComponentsModule: - - def list: ListComponents - - trait ListComponents(using ComponentContext): - def label(text: String, color: ColorKind): HtmlElement - def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI - def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] - def listSection(header: String, list: HtmlElement): Div - def navigation(sections: Modifier[HtmlElement]): HtmlElement - -trait DefaultListComponentsModule(using ComponentContext) - extends ListComponentsModule: - self: GenericComponentsModule => - - override val list: ListComponents = new ListComponents: - override def label( - text: String, - color: ColorKind - ): HtmlElement = generic.tag(text, color) - - override def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI = - li( - cls("group"), - div( - cls( - "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" - ), - avatar.map(a => - div( - cls("flex-shrink-0"), - div( - cls( - "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" - ), - a - ) - ) - ), - div( - cls("flex-1 min-w-0"), - p( - cls("text-sm font-medium text-gray-900"), - title, - span(cls("float-right"), right) - ), - subtitle.map(st => - p( - cls("text-sm text-gray-500 truncate"), - st - ) - ) - ) - ) - ) - - override def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = - ul( - cls("relative z-0 divide-y divide-gray-200"), - role("list"), - children - ) - - override def listSection( - header: String, - list: HtmlElement - ): Div = - div( - cls("relative"), - div( - cls( - "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" - ), - header - ), - list - ) - - override def navigation(sections: Modifier[HtmlElement]): HtmlElement = - navTag( - cls("flex-1 min-h-0 overflow-y-auto"), - sections - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala deleted file mode 100644 index 5b5490f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ModalComponentsModule: - - def modal: ModalComponents - - trait ModalComponents: - def modalDialog( - content: HtmlElement, - onClose: Modifier[HtmlElement] - ): HtmlElement - -trait DefaultModalComponentsModule(using ctx: ComponentContext) - extends ModalComponentsModule: - - override val modal: ModalComponents = new ModalComponents: - - override def modalDialog( - content: HtmlElement, - closeMod: Modifier[HtmlElement] - ): 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( - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")), - closeMod - ) - ) - - div( - cls("fixed inset-0 z-20 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - 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-7xl sm:align-middle" - ), - content - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala deleted file mode 100644 index b713d8a..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala +++ /dev/null @@ -1,71 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait PageComponentsModule: - - val page: PageComponents - - trait PageComponents: - def container( - children: Modifier[HtmlElement]* - ): HtmlElement - - def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement - - def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement] = emptyMod, - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement - - /** Visage for clickable text, like links - */ - def clickable: Modifier[HtmlElement] - -trait DefaultPageComponentsModule(using ComponentContext) - extends PageComponentsModule: - - override val page: PageComponents = new PageComponents: - override def container( - children: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), - children - ) - - override def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement = - div( - cls("p-8 bg-gray-100 h-full"), - header, - children - ) - - override def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement = - div( - cls("pb-5 border-b border-gray-200"), - div(cls("float-right"), right), - h1( - cls("text-2xl leading-6 font-medium text-gray-900"), - title - ), - subtitle.map( - p( - cls("text-sm font-medium text-gray-500"), - _ - ) - ) - ) - - override def clickable: Modifier[HtmlElement] = - cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala deleted file mode 100644 index f70ee87..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala +++ /dev/null @@ -1,108 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} - -trait TableComponentsModule: - def tables: TableComponents - - trait TableComponents: - def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )(table: Modifier[HtmlElement]*): HtmlElement - - def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement - - def headerRow(mods: Modifier[HtmlElement]*)( - cells: HtmlElement* - ): HtmlElement - - def dataRow(mods: Modifier[HtmlElement]*)(cells: HtmlElement*): HtmlElement - - def headerCell(content: Modifier[HtmlElement]): HtmlElement - - def dataCell(content: Modifier[HtmlElement]): HtmlElement - -trait DefaultTableComponentsModule extends TableComponentsModule: - - override lazy val tables: TableComponents = new TableComponents: - - override def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )( - table: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("px-4 sm:px-6 lg:px-8"), - div( - cls("sm:flex sm:items-center"), - div( - cls("sm:flex-auto"), - h1(cls("text-base font-semibold leading-6 text-gray-900"), title), - subtitle.map(st => p(cls("mt-2 text-sm text-gray-700"), st)) - ), - div(cls("mt-4 sm:ml-16 sm:mt-0 sm:flex-none"), actions) - ), - div( - cls("mt-8 flow-root"), - div( - cls("-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"), - div( - cls("inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"), - table - ) - ) - ) - ) - - override def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement = - table( - cls("min-w-full divide-y divide-gray-300"), - thead(header), - tbody(cls("divide-y divide-gray-200"), body) - ) - - override def headerRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-3.5 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-3.5 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-3.5 px-3")) - ) - ) - - override def dataRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - mods, - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-4 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-4 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-4 px-3")) - ) - ) - - override def headerCell(content: Modifier[HtmlElement]): HtmlElement = - th( - cls("text-left text-sm font-semibold text-gray-900"), - content - ) - - override def dataCell(content: Modifier[HtmlElement]): HtmlElement = - td( - cls("whitespace-nowrap text-sm text-gray-500"), - content - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala index b937d45..12d01ae 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala @@ -3,7 +3,7 @@ import com.raquo.laminar.api.L.{*, given} import works.iterative.ui.components.laminar.HtmlTabular import works.iterative.ui.model.tables.Tabular -import works.iterative.ui.components.laminar.TableComponentsModule +import works.iterative.ui.components.laminar.tailwind.ui.TableComponentsModule import works.iterative.core.UserMessage import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given import works.iterative.ui.components.tailwind.ComponentContext diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala new file mode 100644 index 0000000..87f3e8d --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala @@ -0,0 +1,54 @@ +package works.iterative.ui.components.laminar.tailwind.color + +import com.raquo.laminar.api.L.{*, given} + +/** Complete color definition that can be rendered to CSS. + * + * Includes the area, kind and weight of the color. + */ +case class Color(area: ColorArea, color: ColorDef): + def toCSS: String = s"${area.name}-${color.toCSS}" + +object Color: + import ColorDef.given + + given colorToCSS: Conversion[Color, HtmlMod] with + def apply(c: Color) = cls(c.toCSS) + + given colorToSVGCSS: Conversion[Color, SvgMod] with + def apply(c: Color) = svg.cls(c.toCSS) + + given colorSignalToCSS: Conversion[Signal[Color], HtmlMod] with + def apply(c: Signal[Color]) = cls <-- c.map(_.toCSS) + + given colorSignalToSVGCSS: Conversion[Signal[Color], SvgMod] with + def apply(c: Signal[Color]) = svg.cls <-- c.map(_.toCSS) + + def current = ColorDef(ColorKind.current) + def inherit = ColorDef(ColorKind.inherit) + def transp = ColorDef(ColorKind.transp) + def auto = ColorDef(ColorKind.auto) + def black = ColorDef(ColorKind.black) + def white = ColorDef(ColorKind.white) + def slate(weight: ColorWeight) = ColorDef(ColorKind.slate, weight) + def gray(weight: ColorWeight) = ColorDef(ColorKind.gray, weight) + def zinc(weight: ColorWeight) = ColorDef(ColorKind.zinc, weight) + def neutral(weight: ColorWeight) = ColorDef(ColorKind.neutral, weight) + def stone(weight: ColorWeight) = ColorDef(ColorKind.stone, weight) + def red(weight: ColorWeight) = ColorDef(ColorKind.red, weight) + def orange(weight: ColorWeight) = ColorDef(ColorKind.orange, weight) + def amber(weight: ColorWeight) = ColorDef(ColorKind.amber, weight) + def yellow(weight: ColorWeight) = ColorDef(ColorKind.yellow, weight) + def lime(weight: ColorWeight) = ColorDef(ColorKind.lime, weight) + def green(weight: ColorWeight) = ColorDef(ColorKind.green, weight) + def emerald(weight: ColorWeight) = ColorDef(ColorKind.emerald, weight) + def teal(weight: ColorWeight) = ColorDef(ColorKind.teal, weight) + def cyan(weight: ColorWeight) = ColorDef(ColorKind.cyan, weight) + def sky(weight: ColorWeight) = ColorDef(ColorKind.sky, weight) + def blue(weight: ColorWeight) = ColorDef(ColorKind.blue, weight) + def indigo(weight: ColorWeight) = ColorDef(ColorKind.indigo, weight) + def violet(weight: ColorWeight) = ColorDef(ColorKind.violet, weight) + def purple(weight: ColorWeight) = ColorDef(ColorKind.purple, weight) + def fuchsia(weight: ColorWeight) = ColorDef(ColorKind.fuchsia, weight) + def pink(weight: ColorWeight) = ColorDef(ColorKind.pink, weight) + def rose(weight: ColorWeight) = ColorDef(ColorKind.rose, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala new file mode 100644 index 0000000..4130840 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala @@ -0,0 +1,16 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines the area the color should apply to, eg. background, text, border, + * etc. + */ +enum ColorArea(val name: String): + case bg extends ColorArea("bg") + case text extends ColorArea("text") + case decoration extends ColorArea("decoration") + case border extends ColorArea("border") + case outline extends ColorArea("outline") + case divide extends ColorArea("divide") + case ring extends ColorArea("ring") + case ringOffset extends ColorArea("ring-offset") + case shadow extends ColorArea("shadow") + case accent extends ColorArea("accent") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala new file mode 100644 index 0000000..fab9404 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala @@ -0,0 +1,43 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** A combination of ColorKind and ColorWeight, if applicable. + * + * By applying area we get the full Color definition. + */ +sealed trait ColorDef: + def toCSS: String + + def bg = Color(ColorArea.bg, this) + def text = Color(ColorArea.text, this) + def decoration = Color(ColorArea.decoration, this) + def border = Color(ColorArea.border, this) + def outline = Color(ColorArea.outline, this) + def divide = Color(ColorArea.divide, this) + def ring = Color(ColorArea.ring, this) + def ringOffset = Color(ColorArea.ringOffset, this) + def shadow = Color(ColorArea.shadow, this) + def accent = Color(ColorArea.accent, this) + +// TODO: create a macro that will output the tailwind class as string directly during compilation, where possible. +object ColorDef: + case class WeightedColorDef( + kind: ColorKind, + weight: ColorWeight + ) extends ColorDef: + override def toCSS: String = s"${kind.name}-${weight.value}" + + case class UnweightedColorDef( + kind: ColorKind + ) extends ColorDef: + override def toCSS: String = kind.name + + // TODO: check that the kind is valid unweighted kind + // that means current, inherit, auto, transparent, black, white + // tried using implicit evidence, but the type inference for enumerations + // tends to generalize to the enum, instead of the real type + def apply[T <: ColorKind](kind: T)(using + ev: T <:< ColorKind.Unweighted + ): ColorDef = + UnweightedColorDef(kind) + def apply(kind: ColorKind, weight: ColorWeight): ColorDef = + WeightedColorDef(kind, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala new file mode 100644 index 0000000..865166b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala @@ -0,0 +1,47 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines what color should be used, without specifying the area or weight. + */ +sealed abstract class ColorKind private (val name: String): + def apply(weight: ColorWeight): ColorDef = + ColorDef.WeightedColorDef(this, weight) + +object ColorKind: + trait Unweighted: + self: ColorKind => + override def apply(weight: ColorWeight): ColorDef = + ColorDef.UnweightedColorDef(self) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case object current extends ColorKind("current") with Unweighted + case object inherit extends ColorKind("inherit") with Unweighted + // Not present in for all methods + case object transp extends ColorKind("transparent") with Unweighted + // Seen in accent, not preset otherwise + case object auto extends ColorKind("auto") with Unweighted + // Black and white do not have weight + case object black extends ColorKind("black") with Unweighted + case object white extends ColorKind("white") with Unweighted + case object slate extends ColorKind("slate") + case object gray extends ColorKind("gray") + case object zinc extends ColorKind("zinc") + case object neutral extends ColorKind("neutral") + case object stone extends ColorKind("stone") + case object red extends ColorKind("red") + case object orange extends ColorKind("orange") + case object amber extends ColorKind("amber") + case object yellow extends ColorKind("yellow") + case object lime extends ColorKind("lime") + case object green extends ColorKind("green") + case object emerald extends ColorKind("emerald") + case object teal extends ColorKind("teal") + case object cyan extends ColorKind("cyan") + case object sky extends ColorKind("sky") + case object blue extends ColorKind("blue") + case object indigo extends ColorKind("indigo") + case object violet extends ColorKind("violet") + case object purple extends ColorKind("purple") + case object fuchsia extends ColorKind("fuchsia") + case object pink extends ColorKind("pink") + case object rose extends ColorKind("rose") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala new file mode 100644 index 0000000..6f1a4cc --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala @@ -0,0 +1,33 @@ +package works.iterative.ui.components.laminar.tailwind.color + +opaque type ColorWeight = String + +extension (c: ColorWeight) def value: String = c + +/** Defines weight of a color, eg. 50, 100, 200, etc. + * + * Tailwind-like. + */ +object ColorWeight: + inline given int50: Conversion[50, ColorWeight] with + inline def apply(i: 50) = "50" + inline given int100: Conversion[100, ColorWeight] with + inline def apply(i: 100) = "100" + inline given int200: Conversion[200, ColorWeight] with + inline def apply(i: 200) = "200" + inline given int300: Conversion[300, ColorWeight] with + inline def apply(i: 300) = "300" + inline given int400: Conversion[400, ColorWeight] with + inline def apply(i: 400) = "400" + inline given int500: Conversion[500, ColorWeight] with + inline def apply(i: 500) = "500" + inline given int600: Conversion[600, ColorWeight] with + inline def apply(i: 600) = "600" + inline given int700: Conversion[700, ColorWeight] with + inline def apply(i: 700) = "700" + inline given int800: Conversion[800, ColorWeight] with + inline def apply(i: 800) = "800" + inline given int900: Conversion[900, ColorWeight] with + inline def apply(i: 900) = "900" + inline given int950: Conversion[950, ColorWeight] with + inline def apply(i: 950) = "950" diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala index a2c637c..bfa063a 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala @@ -1,13 +1,11 @@ package works.iterative.ui.components.laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorDef import com.raquo.laminar.tags.HtmlTag case class Bin[Source, +Value]( label: String, description: Option[String | HtmlElement], - color: ColorDef, valueOf: Source => Value ): def map[NewValue]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala deleted file mode 100644 index 3aac0bf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala +++ /dev/null @@ -1,34 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ButtonComponentsModule: - def buttons: ButtonComponents - - trait ButtonComponents: - def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala deleted file mode 100644 index 646e73d..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala +++ /dev/null @@ -1,191 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.color.ColorDef -import com.raquo.laminar.tags.HtmlTag -import org.scalajs.dom.html.UList -import com.raquo.laminar.nodes.ReactiveHtmlElement -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind - -trait DashboardComponentsModule: - def dashboard: DashboardComponents - - // Only methods that create HtmlElements - trait DashboardComponents(using ComponentContext): - def section(label: String, children: Modifier[HtmlElement]): Div - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI - def cardInitials( - initials: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div - def count(label: String, value: Int, color: ColorDef): Span - def counts(children: HtmlElement*): Span - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - -trait DefaultDashboardComponentsModule(using ComponentContext) - extends DashboardComponentsModule: - override val dashboard: DashboardComponents = new DashboardComponents: - def section(label: String, children: Modifier[HtmlElement]): Div = - div( - cls("mt-3"), - h2( - cls("text-gray-500 text-xs font-medium uppercase tracking-wide"), - label - ), - children - ) - - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] = - ul( - cls( - "mt-3 grid gap-5 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" - ), - children - ) - - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI = - li( - div( - cls("col-span-1 flex shadow-sm rounded-md"), - initials, - div( - cls( - "flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate" - ), - div( - cls("flex-1 px-4 py-2 text-sm truncate"), - div( - cls("text-gray-900 font-medium hover:text-gray-600 truncate"), - label - ), - p(cls("text-gray-500"), children) - ) - ) - ) - ) - - def cardInitials( - id: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div = - div( - cls( - "flex-shrink-0 flex flex-col items-center justify-center w-16 text-white text-sm font-medium rounded-l-md" - ), - color.map(_(600).bg), - div(id), - children - ) - - def count(label: String, value: Int, color: ColorDef): Span = - span(color.text, title(label), s"${value}") - - def counts(children: HtmlElement*): Span = - span(interleave(children, span(" / "))) - - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div = - div( - cls("flex flex-col mt-2"), - div( - cls( - "align-middle min-w-full shadow sm:rounded-lg" - ), - L.table( - cls("min-w-full border-separate border-spacing-0"), - styleAttr("border-spacing: 0"), // Tailwind somehow doesn't work - thead( - tr( - cls("border-t border-gray-200"), - headers.map(h => - th( - cls( - "sticky top-0 z-10 px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - h - ) - ) - ) - ), - tbody(cls("bg-white divide-y divide-gray-100"), sections) - ) - ) - ) - - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - td( - cls("bg-gray-50 border-gray-200 border-t border-b"), - cls( - "sticky top-14 z-10 bg-gray-50 px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - colSpan(cs), - label - ), - mods - ) - - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - mods, - td( - cls( - "px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900 truncate" - ), - label - ), - children.map(e => - td( - cls( - "whitespace-nowrap px-2 py-2 text-sm text-gray-500" - ), - e - ) - ) - ) - - private def interleave( - a: Seq[HtmlElement], - separator: => HtmlElement - ): Seq[HtmlElement] = - if a.size < 2 then a - else a.init.flatMap(n => Seq(n, separator)) :+ a.last diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala deleted file mode 100644 index a45beaf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.FileRef - -trait DetailComponentsModule: - - def details: DetailComponents - - trait DetailComponents: - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement - - def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement - - def files(fs: List[FileRef]): HtmlElement - - def file(f: FileRef): HtmlElement - -trait DefaultDetailComponentsModule(using ctx: ComponentContext) - extends DetailComponentsModule: - self: IconsModule => - - override val details: DetailComponents = new DetailComponents: - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement = - div( - div( - cls := "px-4 sm:px-0", - h3( - cls := "text-base font-semibold leading-7 text-gray-900", - title - ), - subtitle.map(st => - p( - cls := "mt-1 max-w-2xl text-sm leading-6 text-gray-500", - st - ) - ) - // TODO: actions - ), - div( - cls := "mt-6 border-t border-gray-100", - dl( - cls := "divide-y divide-gray-100", - fields - ) - ) - ) - - override def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0", - dt( - cls := "text-sm font-medium leading-6 text-gray-900", - title - ), - dd( - cls := "mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0", - content - ) - ) - - override def files(fs: List[FileRef]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-100 rounded-md border border-gray-200", - fs.map(file) - ) - - override def file(f: FileRef): HtmlElement = - li( - cls := "flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6", - div( - cls := "flex w-0 flex-1 items-center", - icons.`paper-clip-solid`(), - div( - cls := "ml-4 flex min-w-0 flex-1 gap-2", - span( - cls := "truncate font-medium", - f.name - ), - f.size.map(size => span(cls := "flex-shrink-0 text-gray-400", size)) - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - "Uložit" - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala new file mode 100644 index 0000000..f6c583f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait FormComponents(using ctx: ComponentContext) extends LocalDateSelectModule: + def searchIcon: SvgElement + + def renderLocalDateSelect( + id: String, + labelText: Option[String], + placeholderText: Option[String], + mods: LocalDateSelect => Modifier[HtmlElement] + ): HtmlElement = + div( + labelText, + input( + idAttr(id), + nameAttr(id), + autoComplete("date"), + tpe("date"), + cls( + "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ), + placeholderText.map(placeholder(_)), + mods(localDateSelect) + ) + ) + + def searchField( + id: String, + placeholderText: Option[String] = None + )( + mods: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls := "flex-1 min-w-0", + label(ctx.messages("forms.search.label"), forId(id), cls("sr-only")), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + searchIcon + ), + input( + tpe := "search", + nameAttr := "search", + idAttr := id, + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholderText + .orElse( + ctx.messages + .opt( + s"forms.search.${id}.placeholder", + s"form.search.placeholder" + ) + ) + .map(placeholder(_)), + mods + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala deleted file mode 100644 index 7714490..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala +++ /dev/null @@ -1,82 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -/** Form components - * - * A form is a collection of sections, each of which contains a collection of - * fields. - * - * Form -> * Section -> * Field - * -> * Action - * - * Each Field has an `id`, `label` and `input` element. The `id` is used to - * link the label to the input. - * - * The `input` element can be a simple text input, or a more complex component - * such as a date picker. The caller is expected to make the input work. - */ -trait FormComponentsModule extends LocalDateSelectModule: - def forms: FormComponents - - trait FormComponents: - - /** Layout the sections and actions of a form */ - def form(mods: Modifier[HtmlElement]*)(sections: Modifier[HtmlElement]*)( - actions: Modifier[HtmlElement]* - ): HtmlElement - - /** Section layout, with a title, optional subtitle and any content. */ - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label and any content */ - def field(label: Modifier[HtmlElement])( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label, input and optional help text */ - def field( - id: String, - label: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement - - def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout an inline form element */ - def inlineForm(mods: Modifier[HtmlElement]*): HtmlElement - - /** An input field with a search icon */ - def searchField(id: String, placeholderText: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement - - /** LocalDate input */ - def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala deleted file mode 100644 index 8acbe7f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala +++ /dev/null @@ -1,28 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.Tag - -trait GenericComponentsModule: - - def generic: GenericComponents - - trait GenericComponents: - def tag(name: String, color: ColorKind): HtmlElement - def tag(t: Tag): HtmlElement = tag(t.value, t.color) - -trait DefaultGenericComponentsModule(using ComponentContext) - extends GenericComponentsModule: - override val generic: GenericComponents = new GenericComponents: - override def tag(name: String, color: ColorKind): HtmlElement = - p( - cls( - "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" - ), - color(800).text, - color(100).bg, - name - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala deleted file mode 100644 index 8cfc1d9..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala +++ /dev/null @@ -1,123 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.CustomAttrs - -trait IconsModule: - def icons: Icons - - trait Icons: - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement - def close(mods: Modifier[SvgElement]*): SvgElement - def `search-solid`(mods: Modifier[SvgElement]*): SvgElement - def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement - def `document-chart-bar-outline`(mods: Modifier[SvgElement]*): SvgElement - def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement - -trait DefaultIconsModule(using ComponentContext) extends IconsModule: - override val icons: Icons = new Icons: - import svg.* - - object aria: - val hidden = CustomAttrs.svg.ariaHidden - - private def withDefault( - mods: Seq[Modifier[SvgElement]], - default: Modifier[SvgElement] - ): Modifier[SvgElement] = - if mods.isEmpty then default else mods - - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-8 w-8"), - 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" - ) - ) - ) - - override def close(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-2 w-2"), - stroke := "currentColor", - fill := "none", - viewBox := "0 0 8 8", - path( - strokeLineCap := "round", - strokeWidth := "1.5", - d := "M1 1l6 6m0-6L1 7" - ) - ) - - override def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 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 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") - ) - ) - - override def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 text-gray-400")), - 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") - ) - ) - - override def `document-chart-bar-outline`( - mods: Modifier[SvgElement]* - ): SvgElement = - svg( - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - strokeWidth := "1.5", - stroke := "currentColor", - withDefault(mods, cls := "h-6 w-6"), - path( - strokeLineCap := "round", - strokeLineJoin := "round", - d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" - ) - ) - - override def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-5 w-5"), - cls := "flex-shrink-0 text-gray-400", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", - clipRule := "evenodd" - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala deleted file mode 100644 index 95ab654..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait LayoutModule: - def layout: LayoutComponents - - trait LayoutComponents: - def card(content: Modifier[HtmlElement]*): HtmlElement - -trait DefaultLayoutModule(using ctx: ComponentContext) extends LayoutModule: - override val layout: LayoutComponents = new LayoutComponents: - override def card(content: Modifier[HtmlElement]*): HtmlElement = - div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala deleted file mode 100644 index 5946974..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom.html.Paragraph -import com.raquo.laminar.nodes.TextNode - -trait ListComponentsModule: - - def list: ListComponents - - trait ListComponents(using ComponentContext): - def label(text: String, color: ColorKind): HtmlElement - def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI - def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] - def listSection(header: String, list: HtmlElement): Div - def navigation(sections: Modifier[HtmlElement]): HtmlElement - -trait DefaultListComponentsModule(using ComponentContext) - extends ListComponentsModule: - self: GenericComponentsModule => - - override val list: ListComponents = new ListComponents: - override def label( - text: String, - color: ColorKind - ): HtmlElement = generic.tag(text, color) - - override def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI = - li( - cls("group"), - div( - cls( - "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" - ), - avatar.map(a => - div( - cls("flex-shrink-0"), - div( - cls( - "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" - ), - a - ) - ) - ), - div( - cls("flex-1 min-w-0"), - p( - cls("text-sm font-medium text-gray-900"), - title, - span(cls("float-right"), right) - ), - subtitle.map(st => - p( - cls("text-sm text-gray-500 truncate"), - st - ) - ) - ) - ) - ) - - override def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = - ul( - cls("relative z-0 divide-y divide-gray-200"), - role("list"), - children - ) - - override def listSection( - header: String, - list: HtmlElement - ): Div = - div( - cls("relative"), - div( - cls( - "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" - ), - header - ), - list - ) - - override def navigation(sections: Modifier[HtmlElement]): HtmlElement = - navTag( - cls("flex-1 min-h-0 overflow-y-auto"), - sections - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala deleted file mode 100644 index 5b5490f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ModalComponentsModule: - - def modal: ModalComponents - - trait ModalComponents: - def modalDialog( - content: HtmlElement, - onClose: Modifier[HtmlElement] - ): HtmlElement - -trait DefaultModalComponentsModule(using ctx: ComponentContext) - extends ModalComponentsModule: - - override val modal: ModalComponents = new ModalComponents: - - override def modalDialog( - content: HtmlElement, - closeMod: Modifier[HtmlElement] - ): 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( - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")), - closeMod - ) - ) - - div( - cls("fixed inset-0 z-20 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - 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-7xl sm:align-middle" - ), - content - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala deleted file mode 100644 index b713d8a..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala +++ /dev/null @@ -1,71 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait PageComponentsModule: - - val page: PageComponents - - trait PageComponents: - def container( - children: Modifier[HtmlElement]* - ): HtmlElement - - def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement - - def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement] = emptyMod, - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement - - /** Visage for clickable text, like links - */ - def clickable: Modifier[HtmlElement] - -trait DefaultPageComponentsModule(using ComponentContext) - extends PageComponentsModule: - - override val page: PageComponents = new PageComponents: - override def container( - children: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), - children - ) - - override def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement = - div( - cls("p-8 bg-gray-100 h-full"), - header, - children - ) - - override def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement = - div( - cls("pb-5 border-b border-gray-200"), - div(cls("float-right"), right), - h1( - cls("text-2xl leading-6 font-medium text-gray-900"), - title - ), - subtitle.map( - p( - cls("text-sm font-medium text-gray-500"), - _ - ) - ) - ) - - override def clickable: Modifier[HtmlElement] = - cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala deleted file mode 100644 index f70ee87..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala +++ /dev/null @@ -1,108 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} - -trait TableComponentsModule: - def tables: TableComponents - - trait TableComponents: - def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )(table: Modifier[HtmlElement]*): HtmlElement - - def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement - - def headerRow(mods: Modifier[HtmlElement]*)( - cells: HtmlElement* - ): HtmlElement - - def dataRow(mods: Modifier[HtmlElement]*)(cells: HtmlElement*): HtmlElement - - def headerCell(content: Modifier[HtmlElement]): HtmlElement - - def dataCell(content: Modifier[HtmlElement]): HtmlElement - -trait DefaultTableComponentsModule extends TableComponentsModule: - - override lazy val tables: TableComponents = new TableComponents: - - override def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )( - table: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("px-4 sm:px-6 lg:px-8"), - div( - cls("sm:flex sm:items-center"), - div( - cls("sm:flex-auto"), - h1(cls("text-base font-semibold leading-6 text-gray-900"), title), - subtitle.map(st => p(cls("mt-2 text-sm text-gray-700"), st)) - ), - div(cls("mt-4 sm:ml-16 sm:mt-0 sm:flex-none"), actions) - ), - div( - cls("mt-8 flow-root"), - div( - cls("-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"), - div( - cls("inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"), - table - ) - ) - ) - ) - - override def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement = - table( - cls("min-w-full divide-y divide-gray-300"), - thead(header), - tbody(cls("divide-y divide-gray-200"), body) - ) - - override def headerRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-3.5 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-3.5 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-3.5 px-3")) - ) - ) - - override def dataRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - mods, - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-4 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-4 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-4 px-3")) - ) - ) - - override def headerCell(content: Modifier[HtmlElement]): HtmlElement = - th( - cls("text-left text-sm font-semibold text-gray-900"), - content - ) - - override def dataCell(content: Modifier[HtmlElement]): HtmlElement = - td( - cls("whitespace-nowrap text-sm text-gray-500"), - content - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala index b937d45..12d01ae 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala @@ -3,7 +3,7 @@ import com.raquo.laminar.api.L.{*, given} import works.iterative.ui.components.laminar.HtmlTabular import works.iterative.ui.model.tables.Tabular -import works.iterative.ui.components.laminar.TableComponentsModule +import works.iterative.ui.components.laminar.tailwind.ui.TableComponentsModule import works.iterative.core.UserMessage import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given import works.iterative.ui.components.tailwind.ComponentContext diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala new file mode 100644 index 0000000..87f3e8d --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala @@ -0,0 +1,54 @@ +package works.iterative.ui.components.laminar.tailwind.color + +import com.raquo.laminar.api.L.{*, given} + +/** Complete color definition that can be rendered to CSS. + * + * Includes the area, kind and weight of the color. + */ +case class Color(area: ColorArea, color: ColorDef): + def toCSS: String = s"${area.name}-${color.toCSS}" + +object Color: + import ColorDef.given + + given colorToCSS: Conversion[Color, HtmlMod] with + def apply(c: Color) = cls(c.toCSS) + + given colorToSVGCSS: Conversion[Color, SvgMod] with + def apply(c: Color) = svg.cls(c.toCSS) + + given colorSignalToCSS: Conversion[Signal[Color], HtmlMod] with + def apply(c: Signal[Color]) = cls <-- c.map(_.toCSS) + + given colorSignalToSVGCSS: Conversion[Signal[Color], SvgMod] with + def apply(c: Signal[Color]) = svg.cls <-- c.map(_.toCSS) + + def current = ColorDef(ColorKind.current) + def inherit = ColorDef(ColorKind.inherit) + def transp = ColorDef(ColorKind.transp) + def auto = ColorDef(ColorKind.auto) + def black = ColorDef(ColorKind.black) + def white = ColorDef(ColorKind.white) + def slate(weight: ColorWeight) = ColorDef(ColorKind.slate, weight) + def gray(weight: ColorWeight) = ColorDef(ColorKind.gray, weight) + def zinc(weight: ColorWeight) = ColorDef(ColorKind.zinc, weight) + def neutral(weight: ColorWeight) = ColorDef(ColorKind.neutral, weight) + def stone(weight: ColorWeight) = ColorDef(ColorKind.stone, weight) + def red(weight: ColorWeight) = ColorDef(ColorKind.red, weight) + def orange(weight: ColorWeight) = ColorDef(ColorKind.orange, weight) + def amber(weight: ColorWeight) = ColorDef(ColorKind.amber, weight) + def yellow(weight: ColorWeight) = ColorDef(ColorKind.yellow, weight) + def lime(weight: ColorWeight) = ColorDef(ColorKind.lime, weight) + def green(weight: ColorWeight) = ColorDef(ColorKind.green, weight) + def emerald(weight: ColorWeight) = ColorDef(ColorKind.emerald, weight) + def teal(weight: ColorWeight) = ColorDef(ColorKind.teal, weight) + def cyan(weight: ColorWeight) = ColorDef(ColorKind.cyan, weight) + def sky(weight: ColorWeight) = ColorDef(ColorKind.sky, weight) + def blue(weight: ColorWeight) = ColorDef(ColorKind.blue, weight) + def indigo(weight: ColorWeight) = ColorDef(ColorKind.indigo, weight) + def violet(weight: ColorWeight) = ColorDef(ColorKind.violet, weight) + def purple(weight: ColorWeight) = ColorDef(ColorKind.purple, weight) + def fuchsia(weight: ColorWeight) = ColorDef(ColorKind.fuchsia, weight) + def pink(weight: ColorWeight) = ColorDef(ColorKind.pink, weight) + def rose(weight: ColorWeight) = ColorDef(ColorKind.rose, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala new file mode 100644 index 0000000..4130840 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala @@ -0,0 +1,16 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines the area the color should apply to, eg. background, text, border, + * etc. + */ +enum ColorArea(val name: String): + case bg extends ColorArea("bg") + case text extends ColorArea("text") + case decoration extends ColorArea("decoration") + case border extends ColorArea("border") + case outline extends ColorArea("outline") + case divide extends ColorArea("divide") + case ring extends ColorArea("ring") + case ringOffset extends ColorArea("ring-offset") + case shadow extends ColorArea("shadow") + case accent extends ColorArea("accent") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala new file mode 100644 index 0000000..fab9404 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala @@ -0,0 +1,43 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** A combination of ColorKind and ColorWeight, if applicable. + * + * By applying area we get the full Color definition. + */ +sealed trait ColorDef: + def toCSS: String + + def bg = Color(ColorArea.bg, this) + def text = Color(ColorArea.text, this) + def decoration = Color(ColorArea.decoration, this) + def border = Color(ColorArea.border, this) + def outline = Color(ColorArea.outline, this) + def divide = Color(ColorArea.divide, this) + def ring = Color(ColorArea.ring, this) + def ringOffset = Color(ColorArea.ringOffset, this) + def shadow = Color(ColorArea.shadow, this) + def accent = Color(ColorArea.accent, this) + +// TODO: create a macro that will output the tailwind class as string directly during compilation, where possible. +object ColorDef: + case class WeightedColorDef( + kind: ColorKind, + weight: ColorWeight + ) extends ColorDef: + override def toCSS: String = s"${kind.name}-${weight.value}" + + case class UnweightedColorDef( + kind: ColorKind + ) extends ColorDef: + override def toCSS: String = kind.name + + // TODO: check that the kind is valid unweighted kind + // that means current, inherit, auto, transparent, black, white + // tried using implicit evidence, but the type inference for enumerations + // tends to generalize to the enum, instead of the real type + def apply[T <: ColorKind](kind: T)(using + ev: T <:< ColorKind.Unweighted + ): ColorDef = + UnweightedColorDef(kind) + def apply(kind: ColorKind, weight: ColorWeight): ColorDef = + WeightedColorDef(kind, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala new file mode 100644 index 0000000..865166b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala @@ -0,0 +1,47 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines what color should be used, without specifying the area or weight. + */ +sealed abstract class ColorKind private (val name: String): + def apply(weight: ColorWeight): ColorDef = + ColorDef.WeightedColorDef(this, weight) + +object ColorKind: + trait Unweighted: + self: ColorKind => + override def apply(weight: ColorWeight): ColorDef = + ColorDef.UnweightedColorDef(self) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case object current extends ColorKind("current") with Unweighted + case object inherit extends ColorKind("inherit") with Unweighted + // Not present in for all methods + case object transp extends ColorKind("transparent") with Unweighted + // Seen in accent, not preset otherwise + case object auto extends ColorKind("auto") with Unweighted + // Black and white do not have weight + case object black extends ColorKind("black") with Unweighted + case object white extends ColorKind("white") with Unweighted + case object slate extends ColorKind("slate") + case object gray extends ColorKind("gray") + case object zinc extends ColorKind("zinc") + case object neutral extends ColorKind("neutral") + case object stone extends ColorKind("stone") + case object red extends ColorKind("red") + case object orange extends ColorKind("orange") + case object amber extends ColorKind("amber") + case object yellow extends ColorKind("yellow") + case object lime extends ColorKind("lime") + case object green extends ColorKind("green") + case object emerald extends ColorKind("emerald") + case object teal extends ColorKind("teal") + case object cyan extends ColorKind("cyan") + case object sky extends ColorKind("sky") + case object blue extends ColorKind("blue") + case object indigo extends ColorKind("indigo") + case object violet extends ColorKind("violet") + case object purple extends ColorKind("purple") + case object fuchsia extends ColorKind("fuchsia") + case object pink extends ColorKind("pink") + case object rose extends ColorKind("rose") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala new file mode 100644 index 0000000..6f1a4cc --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala @@ -0,0 +1,33 @@ +package works.iterative.ui.components.laminar.tailwind.color + +opaque type ColorWeight = String + +extension (c: ColorWeight) def value: String = c + +/** Defines weight of a color, eg. 50, 100, 200, etc. + * + * Tailwind-like. + */ +object ColorWeight: + inline given int50: Conversion[50, ColorWeight] with + inline def apply(i: 50) = "50" + inline given int100: Conversion[100, ColorWeight] with + inline def apply(i: 100) = "100" + inline given int200: Conversion[200, ColorWeight] with + inline def apply(i: 200) = "200" + inline given int300: Conversion[300, ColorWeight] with + inline def apply(i: 300) = "300" + inline given int400: Conversion[400, ColorWeight] with + inline def apply(i: 400) = "400" + inline given int500: Conversion[500, ColorWeight] with + inline def apply(i: 500) = "500" + inline given int600: Conversion[600, ColorWeight] with + inline def apply(i: 600) = "600" + inline given int700: Conversion[700, ColorWeight] with + inline def apply(i: 700) = "700" + inline given int800: Conversion[800, ColorWeight] with + inline def apply(i: 800) = "800" + inline given int900: Conversion[900, ColorWeight] with + inline def apply(i: 900) = "900" + inline given int950: Conversion[950, ColorWeight] with + inline def apply(i: 950) = "950" diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala new file mode 100644 index 0000000..21e21f0 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala @@ -0,0 +1,12 @@ +package works.iterative.ui.components.laminar.tailwind + +/** We need a generic Color model that can be used both on server and on client. + * + * We have adopted the Tailwind model for now. There is nothing inherently + * Tailwind-specific in the implementation, but all the values are taken from + * their palette and the area model is very HTML biased. + * + * Still, I think that it is a good starting point for exploration and will + * satisfy our current needs. + */ +package object color {} diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala index a2c637c..bfa063a 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala @@ -1,13 +1,11 @@ package works.iterative.ui.components.laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorDef import com.raquo.laminar.tags.HtmlTag case class Bin[Source, +Value]( label: String, description: Option[String | HtmlElement], - color: ColorDef, valueOf: Source => Value ): def map[NewValue]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala deleted file mode 100644 index 3aac0bf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala +++ /dev/null @@ -1,34 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ButtonComponentsModule: - def buttons: ButtonComponents - - trait ButtonComponents: - def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala deleted file mode 100644 index 646e73d..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala +++ /dev/null @@ -1,191 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.color.ColorDef -import com.raquo.laminar.tags.HtmlTag -import org.scalajs.dom.html.UList -import com.raquo.laminar.nodes.ReactiveHtmlElement -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind - -trait DashboardComponentsModule: - def dashboard: DashboardComponents - - // Only methods that create HtmlElements - trait DashboardComponents(using ComponentContext): - def section(label: String, children: Modifier[HtmlElement]): Div - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI - def cardInitials( - initials: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div - def count(label: String, value: Int, color: ColorDef): Span - def counts(children: HtmlElement*): Span - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - -trait DefaultDashboardComponentsModule(using ComponentContext) - extends DashboardComponentsModule: - override val dashboard: DashboardComponents = new DashboardComponents: - def section(label: String, children: Modifier[HtmlElement]): Div = - div( - cls("mt-3"), - h2( - cls("text-gray-500 text-xs font-medium uppercase tracking-wide"), - label - ), - children - ) - - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] = - ul( - cls( - "mt-3 grid gap-5 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" - ), - children - ) - - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI = - li( - div( - cls("col-span-1 flex shadow-sm rounded-md"), - initials, - div( - cls( - "flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate" - ), - div( - cls("flex-1 px-4 py-2 text-sm truncate"), - div( - cls("text-gray-900 font-medium hover:text-gray-600 truncate"), - label - ), - p(cls("text-gray-500"), children) - ) - ) - ) - ) - - def cardInitials( - id: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div = - div( - cls( - "flex-shrink-0 flex flex-col items-center justify-center w-16 text-white text-sm font-medium rounded-l-md" - ), - color.map(_(600).bg), - div(id), - children - ) - - def count(label: String, value: Int, color: ColorDef): Span = - span(color.text, title(label), s"${value}") - - def counts(children: HtmlElement*): Span = - span(interleave(children, span(" / "))) - - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div = - div( - cls("flex flex-col mt-2"), - div( - cls( - "align-middle min-w-full shadow sm:rounded-lg" - ), - L.table( - cls("min-w-full border-separate border-spacing-0"), - styleAttr("border-spacing: 0"), // Tailwind somehow doesn't work - thead( - tr( - cls("border-t border-gray-200"), - headers.map(h => - th( - cls( - "sticky top-0 z-10 px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - h - ) - ) - ) - ), - tbody(cls("bg-white divide-y divide-gray-100"), sections) - ) - ) - ) - - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - td( - cls("bg-gray-50 border-gray-200 border-t border-b"), - cls( - "sticky top-14 z-10 bg-gray-50 px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - colSpan(cs), - label - ), - mods - ) - - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - mods, - td( - cls( - "px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900 truncate" - ), - label - ), - children.map(e => - td( - cls( - "whitespace-nowrap px-2 py-2 text-sm text-gray-500" - ), - e - ) - ) - ) - - private def interleave( - a: Seq[HtmlElement], - separator: => HtmlElement - ): Seq[HtmlElement] = - if a.size < 2 then a - else a.init.flatMap(n => Seq(n, separator)) :+ a.last diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala deleted file mode 100644 index a45beaf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.FileRef - -trait DetailComponentsModule: - - def details: DetailComponents - - trait DetailComponents: - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement - - def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement - - def files(fs: List[FileRef]): HtmlElement - - def file(f: FileRef): HtmlElement - -trait DefaultDetailComponentsModule(using ctx: ComponentContext) - extends DetailComponentsModule: - self: IconsModule => - - override val details: DetailComponents = new DetailComponents: - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement = - div( - div( - cls := "px-4 sm:px-0", - h3( - cls := "text-base font-semibold leading-7 text-gray-900", - title - ), - subtitle.map(st => - p( - cls := "mt-1 max-w-2xl text-sm leading-6 text-gray-500", - st - ) - ) - // TODO: actions - ), - div( - cls := "mt-6 border-t border-gray-100", - dl( - cls := "divide-y divide-gray-100", - fields - ) - ) - ) - - override def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0", - dt( - cls := "text-sm font-medium leading-6 text-gray-900", - title - ), - dd( - cls := "mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0", - content - ) - ) - - override def files(fs: List[FileRef]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-100 rounded-md border border-gray-200", - fs.map(file) - ) - - override def file(f: FileRef): HtmlElement = - li( - cls := "flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6", - div( - cls := "flex w-0 flex-1 items-center", - icons.`paper-clip-solid`(), - div( - cls := "ml-4 flex min-w-0 flex-1 gap-2", - span( - cls := "truncate font-medium", - f.name - ), - f.size.map(size => span(cls := "flex-shrink-0 text-gray-400", size)) - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - "Uložit" - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala new file mode 100644 index 0000000..f6c583f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait FormComponents(using ctx: ComponentContext) extends LocalDateSelectModule: + def searchIcon: SvgElement + + def renderLocalDateSelect( + id: String, + labelText: Option[String], + placeholderText: Option[String], + mods: LocalDateSelect => Modifier[HtmlElement] + ): HtmlElement = + div( + labelText, + input( + idAttr(id), + nameAttr(id), + autoComplete("date"), + tpe("date"), + cls( + "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ), + placeholderText.map(placeholder(_)), + mods(localDateSelect) + ) + ) + + def searchField( + id: String, + placeholderText: Option[String] = None + )( + mods: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls := "flex-1 min-w-0", + label(ctx.messages("forms.search.label"), forId(id), cls("sr-only")), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + searchIcon + ), + input( + tpe := "search", + nameAttr := "search", + idAttr := id, + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholderText + .orElse( + ctx.messages + .opt( + s"forms.search.${id}.placeholder", + s"form.search.placeholder" + ) + ) + .map(placeholder(_)), + mods + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala deleted file mode 100644 index 7714490..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala +++ /dev/null @@ -1,82 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -/** Form components - * - * A form is a collection of sections, each of which contains a collection of - * fields. - * - * Form -> * Section -> * Field - * -> * Action - * - * Each Field has an `id`, `label` and `input` element. The `id` is used to - * link the label to the input. - * - * The `input` element can be a simple text input, or a more complex component - * such as a date picker. The caller is expected to make the input work. - */ -trait FormComponentsModule extends LocalDateSelectModule: - def forms: FormComponents - - trait FormComponents: - - /** Layout the sections and actions of a form */ - def form(mods: Modifier[HtmlElement]*)(sections: Modifier[HtmlElement]*)( - actions: Modifier[HtmlElement]* - ): HtmlElement - - /** Section layout, with a title, optional subtitle and any content. */ - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label and any content */ - def field(label: Modifier[HtmlElement])( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label, input and optional help text */ - def field( - id: String, - label: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement - - def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout an inline form element */ - def inlineForm(mods: Modifier[HtmlElement]*): HtmlElement - - /** An input field with a search icon */ - def searchField(id: String, placeholderText: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement - - /** LocalDate input */ - def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala deleted file mode 100644 index 8acbe7f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala +++ /dev/null @@ -1,28 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.Tag - -trait GenericComponentsModule: - - def generic: GenericComponents - - trait GenericComponents: - def tag(name: String, color: ColorKind): HtmlElement - def tag(t: Tag): HtmlElement = tag(t.value, t.color) - -trait DefaultGenericComponentsModule(using ComponentContext) - extends GenericComponentsModule: - override val generic: GenericComponents = new GenericComponents: - override def tag(name: String, color: ColorKind): HtmlElement = - p( - cls( - "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" - ), - color(800).text, - color(100).bg, - name - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala deleted file mode 100644 index 8cfc1d9..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala +++ /dev/null @@ -1,123 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.CustomAttrs - -trait IconsModule: - def icons: Icons - - trait Icons: - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement - def close(mods: Modifier[SvgElement]*): SvgElement - def `search-solid`(mods: Modifier[SvgElement]*): SvgElement - def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement - def `document-chart-bar-outline`(mods: Modifier[SvgElement]*): SvgElement - def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement - -trait DefaultIconsModule(using ComponentContext) extends IconsModule: - override val icons: Icons = new Icons: - import svg.* - - object aria: - val hidden = CustomAttrs.svg.ariaHidden - - private def withDefault( - mods: Seq[Modifier[SvgElement]], - default: Modifier[SvgElement] - ): Modifier[SvgElement] = - if mods.isEmpty then default else mods - - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-8 w-8"), - 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" - ) - ) - ) - - override def close(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-2 w-2"), - stroke := "currentColor", - fill := "none", - viewBox := "0 0 8 8", - path( - strokeLineCap := "round", - strokeWidth := "1.5", - d := "M1 1l6 6m0-6L1 7" - ) - ) - - override def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 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 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") - ) - ) - - override def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 text-gray-400")), - 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") - ) - ) - - override def `document-chart-bar-outline`( - mods: Modifier[SvgElement]* - ): SvgElement = - svg( - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - strokeWidth := "1.5", - stroke := "currentColor", - withDefault(mods, cls := "h-6 w-6"), - path( - strokeLineCap := "round", - strokeLineJoin := "round", - d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" - ) - ) - - override def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-5 w-5"), - cls := "flex-shrink-0 text-gray-400", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", - clipRule := "evenodd" - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala deleted file mode 100644 index 95ab654..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait LayoutModule: - def layout: LayoutComponents - - trait LayoutComponents: - def card(content: Modifier[HtmlElement]*): HtmlElement - -trait DefaultLayoutModule(using ctx: ComponentContext) extends LayoutModule: - override val layout: LayoutComponents = new LayoutComponents: - override def card(content: Modifier[HtmlElement]*): HtmlElement = - div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala deleted file mode 100644 index 5946974..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom.html.Paragraph -import com.raquo.laminar.nodes.TextNode - -trait ListComponentsModule: - - def list: ListComponents - - trait ListComponents(using ComponentContext): - def label(text: String, color: ColorKind): HtmlElement - def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI - def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] - def listSection(header: String, list: HtmlElement): Div - def navigation(sections: Modifier[HtmlElement]): HtmlElement - -trait DefaultListComponentsModule(using ComponentContext) - extends ListComponentsModule: - self: GenericComponentsModule => - - override val list: ListComponents = new ListComponents: - override def label( - text: String, - color: ColorKind - ): HtmlElement = generic.tag(text, color) - - override def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI = - li( - cls("group"), - div( - cls( - "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" - ), - avatar.map(a => - div( - cls("flex-shrink-0"), - div( - cls( - "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" - ), - a - ) - ) - ), - div( - cls("flex-1 min-w-0"), - p( - cls("text-sm font-medium text-gray-900"), - title, - span(cls("float-right"), right) - ), - subtitle.map(st => - p( - cls("text-sm text-gray-500 truncate"), - st - ) - ) - ) - ) - ) - - override def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = - ul( - cls("relative z-0 divide-y divide-gray-200"), - role("list"), - children - ) - - override def listSection( - header: String, - list: HtmlElement - ): Div = - div( - cls("relative"), - div( - cls( - "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" - ), - header - ), - list - ) - - override def navigation(sections: Modifier[HtmlElement]): HtmlElement = - navTag( - cls("flex-1 min-h-0 overflow-y-auto"), - sections - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala deleted file mode 100644 index 5b5490f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ModalComponentsModule: - - def modal: ModalComponents - - trait ModalComponents: - def modalDialog( - content: HtmlElement, - onClose: Modifier[HtmlElement] - ): HtmlElement - -trait DefaultModalComponentsModule(using ctx: ComponentContext) - extends ModalComponentsModule: - - override val modal: ModalComponents = new ModalComponents: - - override def modalDialog( - content: HtmlElement, - closeMod: Modifier[HtmlElement] - ): 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( - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")), - closeMod - ) - ) - - div( - cls("fixed inset-0 z-20 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - 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-7xl sm:align-middle" - ), - content - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala deleted file mode 100644 index b713d8a..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala +++ /dev/null @@ -1,71 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait PageComponentsModule: - - val page: PageComponents - - trait PageComponents: - def container( - children: Modifier[HtmlElement]* - ): HtmlElement - - def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement - - def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement] = emptyMod, - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement - - /** Visage for clickable text, like links - */ - def clickable: Modifier[HtmlElement] - -trait DefaultPageComponentsModule(using ComponentContext) - extends PageComponentsModule: - - override val page: PageComponents = new PageComponents: - override def container( - children: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), - children - ) - - override def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement = - div( - cls("p-8 bg-gray-100 h-full"), - header, - children - ) - - override def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement = - div( - cls("pb-5 border-b border-gray-200"), - div(cls("float-right"), right), - h1( - cls("text-2xl leading-6 font-medium text-gray-900"), - title - ), - subtitle.map( - p( - cls("text-sm font-medium text-gray-500"), - _ - ) - ) - ) - - override def clickable: Modifier[HtmlElement] = - cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala deleted file mode 100644 index f70ee87..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala +++ /dev/null @@ -1,108 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} - -trait TableComponentsModule: - def tables: TableComponents - - trait TableComponents: - def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )(table: Modifier[HtmlElement]*): HtmlElement - - def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement - - def headerRow(mods: Modifier[HtmlElement]*)( - cells: HtmlElement* - ): HtmlElement - - def dataRow(mods: Modifier[HtmlElement]*)(cells: HtmlElement*): HtmlElement - - def headerCell(content: Modifier[HtmlElement]): HtmlElement - - def dataCell(content: Modifier[HtmlElement]): HtmlElement - -trait DefaultTableComponentsModule extends TableComponentsModule: - - override lazy val tables: TableComponents = new TableComponents: - - override def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )( - table: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("px-4 sm:px-6 lg:px-8"), - div( - cls("sm:flex sm:items-center"), - div( - cls("sm:flex-auto"), - h1(cls("text-base font-semibold leading-6 text-gray-900"), title), - subtitle.map(st => p(cls("mt-2 text-sm text-gray-700"), st)) - ), - div(cls("mt-4 sm:ml-16 sm:mt-0 sm:flex-none"), actions) - ), - div( - cls("mt-8 flow-root"), - div( - cls("-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"), - div( - cls("inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"), - table - ) - ) - ) - ) - - override def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement = - table( - cls("min-w-full divide-y divide-gray-300"), - thead(header), - tbody(cls("divide-y divide-gray-200"), body) - ) - - override def headerRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-3.5 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-3.5 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-3.5 px-3")) - ) - ) - - override def dataRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - mods, - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-4 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-4 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-4 px-3")) - ) - ) - - override def headerCell(content: Modifier[HtmlElement]): HtmlElement = - th( - cls("text-left text-sm font-semibold text-gray-900"), - content - ) - - override def dataCell(content: Modifier[HtmlElement]): HtmlElement = - td( - cls("whitespace-nowrap text-sm text-gray-500"), - content - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala index b937d45..12d01ae 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala @@ -3,7 +3,7 @@ import com.raquo.laminar.api.L.{*, given} import works.iterative.ui.components.laminar.HtmlTabular import works.iterative.ui.model.tables.Tabular -import works.iterative.ui.components.laminar.TableComponentsModule +import works.iterative.ui.components.laminar.tailwind.ui.TableComponentsModule import works.iterative.core.UserMessage import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given import works.iterative.ui.components.tailwind.ComponentContext diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala new file mode 100644 index 0000000..87f3e8d --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala @@ -0,0 +1,54 @@ +package works.iterative.ui.components.laminar.tailwind.color + +import com.raquo.laminar.api.L.{*, given} + +/** Complete color definition that can be rendered to CSS. + * + * Includes the area, kind and weight of the color. + */ +case class Color(area: ColorArea, color: ColorDef): + def toCSS: String = s"${area.name}-${color.toCSS}" + +object Color: + import ColorDef.given + + given colorToCSS: Conversion[Color, HtmlMod] with + def apply(c: Color) = cls(c.toCSS) + + given colorToSVGCSS: Conversion[Color, SvgMod] with + def apply(c: Color) = svg.cls(c.toCSS) + + given colorSignalToCSS: Conversion[Signal[Color], HtmlMod] with + def apply(c: Signal[Color]) = cls <-- c.map(_.toCSS) + + given colorSignalToSVGCSS: Conversion[Signal[Color], SvgMod] with + def apply(c: Signal[Color]) = svg.cls <-- c.map(_.toCSS) + + def current = ColorDef(ColorKind.current) + def inherit = ColorDef(ColorKind.inherit) + def transp = ColorDef(ColorKind.transp) + def auto = ColorDef(ColorKind.auto) + def black = ColorDef(ColorKind.black) + def white = ColorDef(ColorKind.white) + def slate(weight: ColorWeight) = ColorDef(ColorKind.slate, weight) + def gray(weight: ColorWeight) = ColorDef(ColorKind.gray, weight) + def zinc(weight: ColorWeight) = ColorDef(ColorKind.zinc, weight) + def neutral(weight: ColorWeight) = ColorDef(ColorKind.neutral, weight) + def stone(weight: ColorWeight) = ColorDef(ColorKind.stone, weight) + def red(weight: ColorWeight) = ColorDef(ColorKind.red, weight) + def orange(weight: ColorWeight) = ColorDef(ColorKind.orange, weight) + def amber(weight: ColorWeight) = ColorDef(ColorKind.amber, weight) + def yellow(weight: ColorWeight) = ColorDef(ColorKind.yellow, weight) + def lime(weight: ColorWeight) = ColorDef(ColorKind.lime, weight) + def green(weight: ColorWeight) = ColorDef(ColorKind.green, weight) + def emerald(weight: ColorWeight) = ColorDef(ColorKind.emerald, weight) + def teal(weight: ColorWeight) = ColorDef(ColorKind.teal, weight) + def cyan(weight: ColorWeight) = ColorDef(ColorKind.cyan, weight) + def sky(weight: ColorWeight) = ColorDef(ColorKind.sky, weight) + def blue(weight: ColorWeight) = ColorDef(ColorKind.blue, weight) + def indigo(weight: ColorWeight) = ColorDef(ColorKind.indigo, weight) + def violet(weight: ColorWeight) = ColorDef(ColorKind.violet, weight) + def purple(weight: ColorWeight) = ColorDef(ColorKind.purple, weight) + def fuchsia(weight: ColorWeight) = ColorDef(ColorKind.fuchsia, weight) + def pink(weight: ColorWeight) = ColorDef(ColorKind.pink, weight) + def rose(weight: ColorWeight) = ColorDef(ColorKind.rose, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala new file mode 100644 index 0000000..4130840 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala @@ -0,0 +1,16 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines the area the color should apply to, eg. background, text, border, + * etc. + */ +enum ColorArea(val name: String): + case bg extends ColorArea("bg") + case text extends ColorArea("text") + case decoration extends ColorArea("decoration") + case border extends ColorArea("border") + case outline extends ColorArea("outline") + case divide extends ColorArea("divide") + case ring extends ColorArea("ring") + case ringOffset extends ColorArea("ring-offset") + case shadow extends ColorArea("shadow") + case accent extends ColorArea("accent") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala new file mode 100644 index 0000000..fab9404 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala @@ -0,0 +1,43 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** A combination of ColorKind and ColorWeight, if applicable. + * + * By applying area we get the full Color definition. + */ +sealed trait ColorDef: + def toCSS: String + + def bg = Color(ColorArea.bg, this) + def text = Color(ColorArea.text, this) + def decoration = Color(ColorArea.decoration, this) + def border = Color(ColorArea.border, this) + def outline = Color(ColorArea.outline, this) + def divide = Color(ColorArea.divide, this) + def ring = Color(ColorArea.ring, this) + def ringOffset = Color(ColorArea.ringOffset, this) + def shadow = Color(ColorArea.shadow, this) + def accent = Color(ColorArea.accent, this) + +// TODO: create a macro that will output the tailwind class as string directly during compilation, where possible. +object ColorDef: + case class WeightedColorDef( + kind: ColorKind, + weight: ColorWeight + ) extends ColorDef: + override def toCSS: String = s"${kind.name}-${weight.value}" + + case class UnweightedColorDef( + kind: ColorKind + ) extends ColorDef: + override def toCSS: String = kind.name + + // TODO: check that the kind is valid unweighted kind + // that means current, inherit, auto, transparent, black, white + // tried using implicit evidence, but the type inference for enumerations + // tends to generalize to the enum, instead of the real type + def apply[T <: ColorKind](kind: T)(using + ev: T <:< ColorKind.Unweighted + ): ColorDef = + UnweightedColorDef(kind) + def apply(kind: ColorKind, weight: ColorWeight): ColorDef = + WeightedColorDef(kind, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala new file mode 100644 index 0000000..865166b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala @@ -0,0 +1,47 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines what color should be used, without specifying the area or weight. + */ +sealed abstract class ColorKind private (val name: String): + def apply(weight: ColorWeight): ColorDef = + ColorDef.WeightedColorDef(this, weight) + +object ColorKind: + trait Unweighted: + self: ColorKind => + override def apply(weight: ColorWeight): ColorDef = + ColorDef.UnweightedColorDef(self) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case object current extends ColorKind("current") with Unweighted + case object inherit extends ColorKind("inherit") with Unweighted + // Not present in for all methods + case object transp extends ColorKind("transparent") with Unweighted + // Seen in accent, not preset otherwise + case object auto extends ColorKind("auto") with Unweighted + // Black and white do not have weight + case object black extends ColorKind("black") with Unweighted + case object white extends ColorKind("white") with Unweighted + case object slate extends ColorKind("slate") + case object gray extends ColorKind("gray") + case object zinc extends ColorKind("zinc") + case object neutral extends ColorKind("neutral") + case object stone extends ColorKind("stone") + case object red extends ColorKind("red") + case object orange extends ColorKind("orange") + case object amber extends ColorKind("amber") + case object yellow extends ColorKind("yellow") + case object lime extends ColorKind("lime") + case object green extends ColorKind("green") + case object emerald extends ColorKind("emerald") + case object teal extends ColorKind("teal") + case object cyan extends ColorKind("cyan") + case object sky extends ColorKind("sky") + case object blue extends ColorKind("blue") + case object indigo extends ColorKind("indigo") + case object violet extends ColorKind("violet") + case object purple extends ColorKind("purple") + case object fuchsia extends ColorKind("fuchsia") + case object pink extends ColorKind("pink") + case object rose extends ColorKind("rose") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala new file mode 100644 index 0000000..6f1a4cc --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala @@ -0,0 +1,33 @@ +package works.iterative.ui.components.laminar.tailwind.color + +opaque type ColorWeight = String + +extension (c: ColorWeight) def value: String = c + +/** Defines weight of a color, eg. 50, 100, 200, etc. + * + * Tailwind-like. + */ +object ColorWeight: + inline given int50: Conversion[50, ColorWeight] with + inline def apply(i: 50) = "50" + inline given int100: Conversion[100, ColorWeight] with + inline def apply(i: 100) = "100" + inline given int200: Conversion[200, ColorWeight] with + inline def apply(i: 200) = "200" + inline given int300: Conversion[300, ColorWeight] with + inline def apply(i: 300) = "300" + inline given int400: Conversion[400, ColorWeight] with + inline def apply(i: 400) = "400" + inline given int500: Conversion[500, ColorWeight] with + inline def apply(i: 500) = "500" + inline given int600: Conversion[600, ColorWeight] with + inline def apply(i: 600) = "600" + inline given int700: Conversion[700, ColorWeight] with + inline def apply(i: 700) = "700" + inline given int800: Conversion[800, ColorWeight] with + inline def apply(i: 800) = "800" + inline given int900: Conversion[900, ColorWeight] with + inline def apply(i: 900) = "900" + inline given int950: Conversion[950, ColorWeight] with + inline def apply(i: 950) = "950" diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala new file mode 100644 index 0000000..21e21f0 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala @@ -0,0 +1,12 @@ +package works.iterative.ui.components.laminar.tailwind + +/** We need a generic Color model that can be used both on server and on client. + * + * We have adopted the Tailwind model for now. There is nothing inherently + * Tailwind-specific in the implementation, but all the values are taken from + * their palette and the area model is very HTML biased. + * + * Still, I think that it is a good starting point for exploration and will + * satisfy our current needs. + */ +package object color {} diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" new file mode 100644 index 0000000..5c4e218 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" @@ -0,0 +1,21 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.model.color.ColorKind +import works.iterative.ui.components.tailwind.ComponentContext +import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given +import works.iterative.ui.model.Tag + +trait TailwindUIBadgesComponentsModule(using ComponentContext): + + object badges: + override def tag(name: String, color: ColorKind): HtmlElement = + p( + cls( + "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" + ), + color(800).text, + color(100).bg, + name + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala index a2c637c..bfa063a 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala @@ -1,13 +1,11 @@ package works.iterative.ui.components.laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorDef import com.raquo.laminar.tags.HtmlTag case class Bin[Source, +Value]( label: String, description: Option[String | HtmlElement], - color: ColorDef, valueOf: Source => Value ): def map[NewValue]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala deleted file mode 100644 index 3aac0bf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala +++ /dev/null @@ -1,34 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ButtonComponentsModule: - def buttons: ButtonComponents - - trait ButtonComponents: - def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala deleted file mode 100644 index 646e73d..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala +++ /dev/null @@ -1,191 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.color.ColorDef -import com.raquo.laminar.tags.HtmlTag -import org.scalajs.dom.html.UList -import com.raquo.laminar.nodes.ReactiveHtmlElement -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind - -trait DashboardComponentsModule: - def dashboard: DashboardComponents - - // Only methods that create HtmlElements - trait DashboardComponents(using ComponentContext): - def section(label: String, children: Modifier[HtmlElement]): Div - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI - def cardInitials( - initials: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div - def count(label: String, value: Int, color: ColorDef): Span - def counts(children: HtmlElement*): Span - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - -trait DefaultDashboardComponentsModule(using ComponentContext) - extends DashboardComponentsModule: - override val dashboard: DashboardComponents = new DashboardComponents: - def section(label: String, children: Modifier[HtmlElement]): Div = - div( - cls("mt-3"), - h2( - cls("text-gray-500 text-xs font-medium uppercase tracking-wide"), - label - ), - children - ) - - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] = - ul( - cls( - "mt-3 grid gap-5 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" - ), - children - ) - - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI = - li( - div( - cls("col-span-1 flex shadow-sm rounded-md"), - initials, - div( - cls( - "flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate" - ), - div( - cls("flex-1 px-4 py-2 text-sm truncate"), - div( - cls("text-gray-900 font-medium hover:text-gray-600 truncate"), - label - ), - p(cls("text-gray-500"), children) - ) - ) - ) - ) - - def cardInitials( - id: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div = - div( - cls( - "flex-shrink-0 flex flex-col items-center justify-center w-16 text-white text-sm font-medium rounded-l-md" - ), - color.map(_(600).bg), - div(id), - children - ) - - def count(label: String, value: Int, color: ColorDef): Span = - span(color.text, title(label), s"${value}") - - def counts(children: HtmlElement*): Span = - span(interleave(children, span(" / "))) - - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div = - div( - cls("flex flex-col mt-2"), - div( - cls( - "align-middle min-w-full shadow sm:rounded-lg" - ), - L.table( - cls("min-w-full border-separate border-spacing-0"), - styleAttr("border-spacing: 0"), // Tailwind somehow doesn't work - thead( - tr( - cls("border-t border-gray-200"), - headers.map(h => - th( - cls( - "sticky top-0 z-10 px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - h - ) - ) - ) - ), - tbody(cls("bg-white divide-y divide-gray-100"), sections) - ) - ) - ) - - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - td( - cls("bg-gray-50 border-gray-200 border-t border-b"), - cls( - "sticky top-14 z-10 bg-gray-50 px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - colSpan(cs), - label - ), - mods - ) - - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - mods, - td( - cls( - "px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900 truncate" - ), - label - ), - children.map(e => - td( - cls( - "whitespace-nowrap px-2 py-2 text-sm text-gray-500" - ), - e - ) - ) - ) - - private def interleave( - a: Seq[HtmlElement], - separator: => HtmlElement - ): Seq[HtmlElement] = - if a.size < 2 then a - else a.init.flatMap(n => Seq(n, separator)) :+ a.last diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala deleted file mode 100644 index a45beaf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.FileRef - -trait DetailComponentsModule: - - def details: DetailComponents - - trait DetailComponents: - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement - - def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement - - def files(fs: List[FileRef]): HtmlElement - - def file(f: FileRef): HtmlElement - -trait DefaultDetailComponentsModule(using ctx: ComponentContext) - extends DetailComponentsModule: - self: IconsModule => - - override val details: DetailComponents = new DetailComponents: - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement = - div( - div( - cls := "px-4 sm:px-0", - h3( - cls := "text-base font-semibold leading-7 text-gray-900", - title - ), - subtitle.map(st => - p( - cls := "mt-1 max-w-2xl text-sm leading-6 text-gray-500", - st - ) - ) - // TODO: actions - ), - div( - cls := "mt-6 border-t border-gray-100", - dl( - cls := "divide-y divide-gray-100", - fields - ) - ) - ) - - override def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0", - dt( - cls := "text-sm font-medium leading-6 text-gray-900", - title - ), - dd( - cls := "mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0", - content - ) - ) - - override def files(fs: List[FileRef]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-100 rounded-md border border-gray-200", - fs.map(file) - ) - - override def file(f: FileRef): HtmlElement = - li( - cls := "flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6", - div( - cls := "flex w-0 flex-1 items-center", - icons.`paper-clip-solid`(), - div( - cls := "ml-4 flex min-w-0 flex-1 gap-2", - span( - cls := "truncate font-medium", - f.name - ), - f.size.map(size => span(cls := "flex-shrink-0 text-gray-400", size)) - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - "Uložit" - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala new file mode 100644 index 0000000..f6c583f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait FormComponents(using ctx: ComponentContext) extends LocalDateSelectModule: + def searchIcon: SvgElement + + def renderLocalDateSelect( + id: String, + labelText: Option[String], + placeholderText: Option[String], + mods: LocalDateSelect => Modifier[HtmlElement] + ): HtmlElement = + div( + labelText, + input( + idAttr(id), + nameAttr(id), + autoComplete("date"), + tpe("date"), + cls( + "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ), + placeholderText.map(placeholder(_)), + mods(localDateSelect) + ) + ) + + def searchField( + id: String, + placeholderText: Option[String] = None + )( + mods: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls := "flex-1 min-w-0", + label(ctx.messages("forms.search.label"), forId(id), cls("sr-only")), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + searchIcon + ), + input( + tpe := "search", + nameAttr := "search", + idAttr := id, + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholderText + .orElse( + ctx.messages + .opt( + s"forms.search.${id}.placeholder", + s"form.search.placeholder" + ) + ) + .map(placeholder(_)), + mods + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala deleted file mode 100644 index 7714490..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala +++ /dev/null @@ -1,82 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -/** Form components - * - * A form is a collection of sections, each of which contains a collection of - * fields. - * - * Form -> * Section -> * Field - * -> * Action - * - * Each Field has an `id`, `label` and `input` element. The `id` is used to - * link the label to the input. - * - * The `input` element can be a simple text input, or a more complex component - * such as a date picker. The caller is expected to make the input work. - */ -trait FormComponentsModule extends LocalDateSelectModule: - def forms: FormComponents - - trait FormComponents: - - /** Layout the sections and actions of a form */ - def form(mods: Modifier[HtmlElement]*)(sections: Modifier[HtmlElement]*)( - actions: Modifier[HtmlElement]* - ): HtmlElement - - /** Section layout, with a title, optional subtitle and any content. */ - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label and any content */ - def field(label: Modifier[HtmlElement])( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label, input and optional help text */ - def field( - id: String, - label: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement - - def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout an inline form element */ - def inlineForm(mods: Modifier[HtmlElement]*): HtmlElement - - /** An input field with a search icon */ - def searchField(id: String, placeholderText: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement - - /** LocalDate input */ - def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala deleted file mode 100644 index 8acbe7f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala +++ /dev/null @@ -1,28 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.Tag - -trait GenericComponentsModule: - - def generic: GenericComponents - - trait GenericComponents: - def tag(name: String, color: ColorKind): HtmlElement - def tag(t: Tag): HtmlElement = tag(t.value, t.color) - -trait DefaultGenericComponentsModule(using ComponentContext) - extends GenericComponentsModule: - override val generic: GenericComponents = new GenericComponents: - override def tag(name: String, color: ColorKind): HtmlElement = - p( - cls( - "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" - ), - color(800).text, - color(100).bg, - name - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala deleted file mode 100644 index 8cfc1d9..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala +++ /dev/null @@ -1,123 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.CustomAttrs - -trait IconsModule: - def icons: Icons - - trait Icons: - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement - def close(mods: Modifier[SvgElement]*): SvgElement - def `search-solid`(mods: Modifier[SvgElement]*): SvgElement - def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement - def `document-chart-bar-outline`(mods: Modifier[SvgElement]*): SvgElement - def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement - -trait DefaultIconsModule(using ComponentContext) extends IconsModule: - override val icons: Icons = new Icons: - import svg.* - - object aria: - val hidden = CustomAttrs.svg.ariaHidden - - private def withDefault( - mods: Seq[Modifier[SvgElement]], - default: Modifier[SvgElement] - ): Modifier[SvgElement] = - if mods.isEmpty then default else mods - - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-8 w-8"), - 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" - ) - ) - ) - - override def close(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-2 w-2"), - stroke := "currentColor", - fill := "none", - viewBox := "0 0 8 8", - path( - strokeLineCap := "round", - strokeWidth := "1.5", - d := "M1 1l6 6m0-6L1 7" - ) - ) - - override def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 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 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") - ) - ) - - override def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 text-gray-400")), - 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") - ) - ) - - override def `document-chart-bar-outline`( - mods: Modifier[SvgElement]* - ): SvgElement = - svg( - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - strokeWidth := "1.5", - stroke := "currentColor", - withDefault(mods, cls := "h-6 w-6"), - path( - strokeLineCap := "round", - strokeLineJoin := "round", - d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" - ) - ) - - override def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-5 w-5"), - cls := "flex-shrink-0 text-gray-400", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", - clipRule := "evenodd" - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala deleted file mode 100644 index 95ab654..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait LayoutModule: - def layout: LayoutComponents - - trait LayoutComponents: - def card(content: Modifier[HtmlElement]*): HtmlElement - -trait DefaultLayoutModule(using ctx: ComponentContext) extends LayoutModule: - override val layout: LayoutComponents = new LayoutComponents: - override def card(content: Modifier[HtmlElement]*): HtmlElement = - div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala deleted file mode 100644 index 5946974..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom.html.Paragraph -import com.raquo.laminar.nodes.TextNode - -trait ListComponentsModule: - - def list: ListComponents - - trait ListComponents(using ComponentContext): - def label(text: String, color: ColorKind): HtmlElement - def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI - def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] - def listSection(header: String, list: HtmlElement): Div - def navigation(sections: Modifier[HtmlElement]): HtmlElement - -trait DefaultListComponentsModule(using ComponentContext) - extends ListComponentsModule: - self: GenericComponentsModule => - - override val list: ListComponents = new ListComponents: - override def label( - text: String, - color: ColorKind - ): HtmlElement = generic.tag(text, color) - - override def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI = - li( - cls("group"), - div( - cls( - "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" - ), - avatar.map(a => - div( - cls("flex-shrink-0"), - div( - cls( - "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" - ), - a - ) - ) - ), - div( - cls("flex-1 min-w-0"), - p( - cls("text-sm font-medium text-gray-900"), - title, - span(cls("float-right"), right) - ), - subtitle.map(st => - p( - cls("text-sm text-gray-500 truncate"), - st - ) - ) - ) - ) - ) - - override def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = - ul( - cls("relative z-0 divide-y divide-gray-200"), - role("list"), - children - ) - - override def listSection( - header: String, - list: HtmlElement - ): Div = - div( - cls("relative"), - div( - cls( - "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" - ), - header - ), - list - ) - - override def navigation(sections: Modifier[HtmlElement]): HtmlElement = - navTag( - cls("flex-1 min-h-0 overflow-y-auto"), - sections - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala deleted file mode 100644 index 5b5490f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ModalComponentsModule: - - def modal: ModalComponents - - trait ModalComponents: - def modalDialog( - content: HtmlElement, - onClose: Modifier[HtmlElement] - ): HtmlElement - -trait DefaultModalComponentsModule(using ctx: ComponentContext) - extends ModalComponentsModule: - - override val modal: ModalComponents = new ModalComponents: - - override def modalDialog( - content: HtmlElement, - closeMod: Modifier[HtmlElement] - ): 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( - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")), - closeMod - ) - ) - - div( - cls("fixed inset-0 z-20 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - 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-7xl sm:align-middle" - ), - content - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala deleted file mode 100644 index b713d8a..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala +++ /dev/null @@ -1,71 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait PageComponentsModule: - - val page: PageComponents - - trait PageComponents: - def container( - children: Modifier[HtmlElement]* - ): HtmlElement - - def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement - - def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement] = emptyMod, - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement - - /** Visage for clickable text, like links - */ - def clickable: Modifier[HtmlElement] - -trait DefaultPageComponentsModule(using ComponentContext) - extends PageComponentsModule: - - override val page: PageComponents = new PageComponents: - override def container( - children: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), - children - ) - - override def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement = - div( - cls("p-8 bg-gray-100 h-full"), - header, - children - ) - - override def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement = - div( - cls("pb-5 border-b border-gray-200"), - div(cls("float-right"), right), - h1( - cls("text-2xl leading-6 font-medium text-gray-900"), - title - ), - subtitle.map( - p( - cls("text-sm font-medium text-gray-500"), - _ - ) - ) - ) - - override def clickable: Modifier[HtmlElement] = - cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala deleted file mode 100644 index f70ee87..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala +++ /dev/null @@ -1,108 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} - -trait TableComponentsModule: - def tables: TableComponents - - trait TableComponents: - def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )(table: Modifier[HtmlElement]*): HtmlElement - - def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement - - def headerRow(mods: Modifier[HtmlElement]*)( - cells: HtmlElement* - ): HtmlElement - - def dataRow(mods: Modifier[HtmlElement]*)(cells: HtmlElement*): HtmlElement - - def headerCell(content: Modifier[HtmlElement]): HtmlElement - - def dataCell(content: Modifier[HtmlElement]): HtmlElement - -trait DefaultTableComponentsModule extends TableComponentsModule: - - override lazy val tables: TableComponents = new TableComponents: - - override def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )( - table: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("px-4 sm:px-6 lg:px-8"), - div( - cls("sm:flex sm:items-center"), - div( - cls("sm:flex-auto"), - h1(cls("text-base font-semibold leading-6 text-gray-900"), title), - subtitle.map(st => p(cls("mt-2 text-sm text-gray-700"), st)) - ), - div(cls("mt-4 sm:ml-16 sm:mt-0 sm:flex-none"), actions) - ), - div( - cls("mt-8 flow-root"), - div( - cls("-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"), - div( - cls("inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"), - table - ) - ) - ) - ) - - override def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement = - table( - cls("min-w-full divide-y divide-gray-300"), - thead(header), - tbody(cls("divide-y divide-gray-200"), body) - ) - - override def headerRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-3.5 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-3.5 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-3.5 px-3")) - ) - ) - - override def dataRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - mods, - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-4 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-4 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-4 px-3")) - ) - ) - - override def headerCell(content: Modifier[HtmlElement]): HtmlElement = - th( - cls("text-left text-sm font-semibold text-gray-900"), - content - ) - - override def dataCell(content: Modifier[HtmlElement]): HtmlElement = - td( - cls("whitespace-nowrap text-sm text-gray-500"), - content - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala index b937d45..12d01ae 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala @@ -3,7 +3,7 @@ import com.raquo.laminar.api.L.{*, given} import works.iterative.ui.components.laminar.HtmlTabular import works.iterative.ui.model.tables.Tabular -import works.iterative.ui.components.laminar.TableComponentsModule +import works.iterative.ui.components.laminar.tailwind.ui.TableComponentsModule import works.iterative.core.UserMessage import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given import works.iterative.ui.components.tailwind.ComponentContext diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala new file mode 100644 index 0000000..87f3e8d --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala @@ -0,0 +1,54 @@ +package works.iterative.ui.components.laminar.tailwind.color + +import com.raquo.laminar.api.L.{*, given} + +/** Complete color definition that can be rendered to CSS. + * + * Includes the area, kind and weight of the color. + */ +case class Color(area: ColorArea, color: ColorDef): + def toCSS: String = s"${area.name}-${color.toCSS}" + +object Color: + import ColorDef.given + + given colorToCSS: Conversion[Color, HtmlMod] with + def apply(c: Color) = cls(c.toCSS) + + given colorToSVGCSS: Conversion[Color, SvgMod] with + def apply(c: Color) = svg.cls(c.toCSS) + + given colorSignalToCSS: Conversion[Signal[Color], HtmlMod] with + def apply(c: Signal[Color]) = cls <-- c.map(_.toCSS) + + given colorSignalToSVGCSS: Conversion[Signal[Color], SvgMod] with + def apply(c: Signal[Color]) = svg.cls <-- c.map(_.toCSS) + + def current = ColorDef(ColorKind.current) + def inherit = ColorDef(ColorKind.inherit) + def transp = ColorDef(ColorKind.transp) + def auto = ColorDef(ColorKind.auto) + def black = ColorDef(ColorKind.black) + def white = ColorDef(ColorKind.white) + def slate(weight: ColorWeight) = ColorDef(ColorKind.slate, weight) + def gray(weight: ColorWeight) = ColorDef(ColorKind.gray, weight) + def zinc(weight: ColorWeight) = ColorDef(ColorKind.zinc, weight) + def neutral(weight: ColorWeight) = ColorDef(ColorKind.neutral, weight) + def stone(weight: ColorWeight) = ColorDef(ColorKind.stone, weight) + def red(weight: ColorWeight) = ColorDef(ColorKind.red, weight) + def orange(weight: ColorWeight) = ColorDef(ColorKind.orange, weight) + def amber(weight: ColorWeight) = ColorDef(ColorKind.amber, weight) + def yellow(weight: ColorWeight) = ColorDef(ColorKind.yellow, weight) + def lime(weight: ColorWeight) = ColorDef(ColorKind.lime, weight) + def green(weight: ColorWeight) = ColorDef(ColorKind.green, weight) + def emerald(weight: ColorWeight) = ColorDef(ColorKind.emerald, weight) + def teal(weight: ColorWeight) = ColorDef(ColorKind.teal, weight) + def cyan(weight: ColorWeight) = ColorDef(ColorKind.cyan, weight) + def sky(weight: ColorWeight) = ColorDef(ColorKind.sky, weight) + def blue(weight: ColorWeight) = ColorDef(ColorKind.blue, weight) + def indigo(weight: ColorWeight) = ColorDef(ColorKind.indigo, weight) + def violet(weight: ColorWeight) = ColorDef(ColorKind.violet, weight) + def purple(weight: ColorWeight) = ColorDef(ColorKind.purple, weight) + def fuchsia(weight: ColorWeight) = ColorDef(ColorKind.fuchsia, weight) + def pink(weight: ColorWeight) = ColorDef(ColorKind.pink, weight) + def rose(weight: ColorWeight) = ColorDef(ColorKind.rose, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala new file mode 100644 index 0000000..4130840 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala @@ -0,0 +1,16 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines the area the color should apply to, eg. background, text, border, + * etc. + */ +enum ColorArea(val name: String): + case bg extends ColorArea("bg") + case text extends ColorArea("text") + case decoration extends ColorArea("decoration") + case border extends ColorArea("border") + case outline extends ColorArea("outline") + case divide extends ColorArea("divide") + case ring extends ColorArea("ring") + case ringOffset extends ColorArea("ring-offset") + case shadow extends ColorArea("shadow") + case accent extends ColorArea("accent") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala new file mode 100644 index 0000000..fab9404 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala @@ -0,0 +1,43 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** A combination of ColorKind and ColorWeight, if applicable. + * + * By applying area we get the full Color definition. + */ +sealed trait ColorDef: + def toCSS: String + + def bg = Color(ColorArea.bg, this) + def text = Color(ColorArea.text, this) + def decoration = Color(ColorArea.decoration, this) + def border = Color(ColorArea.border, this) + def outline = Color(ColorArea.outline, this) + def divide = Color(ColorArea.divide, this) + def ring = Color(ColorArea.ring, this) + def ringOffset = Color(ColorArea.ringOffset, this) + def shadow = Color(ColorArea.shadow, this) + def accent = Color(ColorArea.accent, this) + +// TODO: create a macro that will output the tailwind class as string directly during compilation, where possible. +object ColorDef: + case class WeightedColorDef( + kind: ColorKind, + weight: ColorWeight + ) extends ColorDef: + override def toCSS: String = s"${kind.name}-${weight.value}" + + case class UnweightedColorDef( + kind: ColorKind + ) extends ColorDef: + override def toCSS: String = kind.name + + // TODO: check that the kind is valid unweighted kind + // that means current, inherit, auto, transparent, black, white + // tried using implicit evidence, but the type inference for enumerations + // tends to generalize to the enum, instead of the real type + def apply[T <: ColorKind](kind: T)(using + ev: T <:< ColorKind.Unweighted + ): ColorDef = + UnweightedColorDef(kind) + def apply(kind: ColorKind, weight: ColorWeight): ColorDef = + WeightedColorDef(kind, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala new file mode 100644 index 0000000..865166b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala @@ -0,0 +1,47 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines what color should be used, without specifying the area or weight. + */ +sealed abstract class ColorKind private (val name: String): + def apply(weight: ColorWeight): ColorDef = + ColorDef.WeightedColorDef(this, weight) + +object ColorKind: + trait Unweighted: + self: ColorKind => + override def apply(weight: ColorWeight): ColorDef = + ColorDef.UnweightedColorDef(self) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case object current extends ColorKind("current") with Unweighted + case object inherit extends ColorKind("inherit") with Unweighted + // Not present in for all methods + case object transp extends ColorKind("transparent") with Unweighted + // Seen in accent, not preset otherwise + case object auto extends ColorKind("auto") with Unweighted + // Black and white do not have weight + case object black extends ColorKind("black") with Unweighted + case object white extends ColorKind("white") with Unweighted + case object slate extends ColorKind("slate") + case object gray extends ColorKind("gray") + case object zinc extends ColorKind("zinc") + case object neutral extends ColorKind("neutral") + case object stone extends ColorKind("stone") + case object red extends ColorKind("red") + case object orange extends ColorKind("orange") + case object amber extends ColorKind("amber") + case object yellow extends ColorKind("yellow") + case object lime extends ColorKind("lime") + case object green extends ColorKind("green") + case object emerald extends ColorKind("emerald") + case object teal extends ColorKind("teal") + case object cyan extends ColorKind("cyan") + case object sky extends ColorKind("sky") + case object blue extends ColorKind("blue") + case object indigo extends ColorKind("indigo") + case object violet extends ColorKind("violet") + case object purple extends ColorKind("purple") + case object fuchsia extends ColorKind("fuchsia") + case object pink extends ColorKind("pink") + case object rose extends ColorKind("rose") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala new file mode 100644 index 0000000..6f1a4cc --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala @@ -0,0 +1,33 @@ +package works.iterative.ui.components.laminar.tailwind.color + +opaque type ColorWeight = String + +extension (c: ColorWeight) def value: String = c + +/** Defines weight of a color, eg. 50, 100, 200, etc. + * + * Tailwind-like. + */ +object ColorWeight: + inline given int50: Conversion[50, ColorWeight] with + inline def apply(i: 50) = "50" + inline given int100: Conversion[100, ColorWeight] with + inline def apply(i: 100) = "100" + inline given int200: Conversion[200, ColorWeight] with + inline def apply(i: 200) = "200" + inline given int300: Conversion[300, ColorWeight] with + inline def apply(i: 300) = "300" + inline given int400: Conversion[400, ColorWeight] with + inline def apply(i: 400) = "400" + inline given int500: Conversion[500, ColorWeight] with + inline def apply(i: 500) = "500" + inline given int600: Conversion[600, ColorWeight] with + inline def apply(i: 600) = "600" + inline given int700: Conversion[700, ColorWeight] with + inline def apply(i: 700) = "700" + inline given int800: Conversion[800, ColorWeight] with + inline def apply(i: 800) = "800" + inline given int900: Conversion[900, ColorWeight] with + inline def apply(i: 900) = "900" + inline given int950: Conversion[950, ColorWeight] with + inline def apply(i: 950) = "950" diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala new file mode 100644 index 0000000..21e21f0 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala @@ -0,0 +1,12 @@ +package works.iterative.ui.components.laminar.tailwind + +/** We need a generic Color model that can be used both on server and on client. + * + * We have adopted the Tailwind model for now. There is nothing inherently + * Tailwind-specific in the implementation, but all the values are taken from + * their palette and the area model is very HTML biased. + * + * Still, I think that it is a good starting point for exploration and will + * satisfy our current needs. + */ +package object color {} diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" new file mode 100644 index 0000000..5c4e218 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" @@ -0,0 +1,21 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.model.color.ColorKind +import works.iterative.ui.components.tailwind.ComponentContext +import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given +import works.iterative.ui.model.Tag + +trait TailwindUIBadgesComponentsModule(using ComponentContext): + + object badges: + override def tag(name: String, color: ColorKind): HtmlElement = + p( + cls( + "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" + ), + color(800).text, + color(100).bg, + name + ) diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" new file mode 100644 index 0000000..0c9ca25 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" @@ -0,0 +1,24 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} + +trait TailwindUIHeadingsComponentsModule: + val headings: HeadingsComponents = new HeadingsComponents + + class HeadingsComponents: + def section(title: Node, actions: HtmlElement*): HtmlElement = + div( + cls("border-b border-gray-200 pb-5"), + div( + cls("sm:flex sm:items-center sm:justify-between"), + h3(cls("text-base font-semibold leading-6 text-gray-900"), title), + div( + cls("mt-3 flex sm:ml-4 sm:mt-0"), + actions match + case Nil => emptyMod + case first :: rest => + first :: rest.map(_.amend(cls("ml-3"))) + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala index a2c637c..bfa063a 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala @@ -1,13 +1,11 @@ package works.iterative.ui.components.laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorDef import com.raquo.laminar.tags.HtmlTag case class Bin[Source, +Value]( label: String, description: Option[String | HtmlElement], - color: ColorDef, valueOf: Source => Value ): def map[NewValue]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala deleted file mode 100644 index 3aac0bf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala +++ /dev/null @@ -1,34 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ButtonComponentsModule: - def buttons: ButtonComponents - - trait ButtonComponents: - def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala deleted file mode 100644 index 646e73d..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala +++ /dev/null @@ -1,191 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.color.ColorDef -import com.raquo.laminar.tags.HtmlTag -import org.scalajs.dom.html.UList -import com.raquo.laminar.nodes.ReactiveHtmlElement -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind - -trait DashboardComponentsModule: - def dashboard: DashboardComponents - - // Only methods that create HtmlElements - trait DashboardComponents(using ComponentContext): - def section(label: String, children: Modifier[HtmlElement]): Div - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI - def cardInitials( - initials: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div - def count(label: String, value: Int, color: ColorDef): Span - def counts(children: HtmlElement*): Span - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - -trait DefaultDashboardComponentsModule(using ComponentContext) - extends DashboardComponentsModule: - override val dashboard: DashboardComponents = new DashboardComponents: - def section(label: String, children: Modifier[HtmlElement]): Div = - div( - cls("mt-3"), - h2( - cls("text-gray-500 text-xs font-medium uppercase tracking-wide"), - label - ), - children - ) - - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] = - ul( - cls( - "mt-3 grid gap-5 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" - ), - children - ) - - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI = - li( - div( - cls("col-span-1 flex shadow-sm rounded-md"), - initials, - div( - cls( - "flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate" - ), - div( - cls("flex-1 px-4 py-2 text-sm truncate"), - div( - cls("text-gray-900 font-medium hover:text-gray-600 truncate"), - label - ), - p(cls("text-gray-500"), children) - ) - ) - ) - ) - - def cardInitials( - id: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div = - div( - cls( - "flex-shrink-0 flex flex-col items-center justify-center w-16 text-white text-sm font-medium rounded-l-md" - ), - color.map(_(600).bg), - div(id), - children - ) - - def count(label: String, value: Int, color: ColorDef): Span = - span(color.text, title(label), s"${value}") - - def counts(children: HtmlElement*): Span = - span(interleave(children, span(" / "))) - - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div = - div( - cls("flex flex-col mt-2"), - div( - cls( - "align-middle min-w-full shadow sm:rounded-lg" - ), - L.table( - cls("min-w-full border-separate border-spacing-0"), - styleAttr("border-spacing: 0"), // Tailwind somehow doesn't work - thead( - tr( - cls("border-t border-gray-200"), - headers.map(h => - th( - cls( - "sticky top-0 z-10 px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - h - ) - ) - ) - ), - tbody(cls("bg-white divide-y divide-gray-100"), sections) - ) - ) - ) - - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - td( - cls("bg-gray-50 border-gray-200 border-t border-b"), - cls( - "sticky top-14 z-10 bg-gray-50 px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - colSpan(cs), - label - ), - mods - ) - - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - mods, - td( - cls( - "px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900 truncate" - ), - label - ), - children.map(e => - td( - cls( - "whitespace-nowrap px-2 py-2 text-sm text-gray-500" - ), - e - ) - ) - ) - - private def interleave( - a: Seq[HtmlElement], - separator: => HtmlElement - ): Seq[HtmlElement] = - if a.size < 2 then a - else a.init.flatMap(n => Seq(n, separator)) :+ a.last diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala deleted file mode 100644 index a45beaf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.FileRef - -trait DetailComponentsModule: - - def details: DetailComponents - - trait DetailComponents: - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement - - def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement - - def files(fs: List[FileRef]): HtmlElement - - def file(f: FileRef): HtmlElement - -trait DefaultDetailComponentsModule(using ctx: ComponentContext) - extends DetailComponentsModule: - self: IconsModule => - - override val details: DetailComponents = new DetailComponents: - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement = - div( - div( - cls := "px-4 sm:px-0", - h3( - cls := "text-base font-semibold leading-7 text-gray-900", - title - ), - subtitle.map(st => - p( - cls := "mt-1 max-w-2xl text-sm leading-6 text-gray-500", - st - ) - ) - // TODO: actions - ), - div( - cls := "mt-6 border-t border-gray-100", - dl( - cls := "divide-y divide-gray-100", - fields - ) - ) - ) - - override def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0", - dt( - cls := "text-sm font-medium leading-6 text-gray-900", - title - ), - dd( - cls := "mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0", - content - ) - ) - - override def files(fs: List[FileRef]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-100 rounded-md border border-gray-200", - fs.map(file) - ) - - override def file(f: FileRef): HtmlElement = - li( - cls := "flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6", - div( - cls := "flex w-0 flex-1 items-center", - icons.`paper-clip-solid`(), - div( - cls := "ml-4 flex min-w-0 flex-1 gap-2", - span( - cls := "truncate font-medium", - f.name - ), - f.size.map(size => span(cls := "flex-shrink-0 text-gray-400", size)) - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - "Uložit" - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala new file mode 100644 index 0000000..f6c583f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait FormComponents(using ctx: ComponentContext) extends LocalDateSelectModule: + def searchIcon: SvgElement + + def renderLocalDateSelect( + id: String, + labelText: Option[String], + placeholderText: Option[String], + mods: LocalDateSelect => Modifier[HtmlElement] + ): HtmlElement = + div( + labelText, + input( + idAttr(id), + nameAttr(id), + autoComplete("date"), + tpe("date"), + cls( + "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ), + placeholderText.map(placeholder(_)), + mods(localDateSelect) + ) + ) + + def searchField( + id: String, + placeholderText: Option[String] = None + )( + mods: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls := "flex-1 min-w-0", + label(ctx.messages("forms.search.label"), forId(id), cls("sr-only")), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + searchIcon + ), + input( + tpe := "search", + nameAttr := "search", + idAttr := id, + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholderText + .orElse( + ctx.messages + .opt( + s"forms.search.${id}.placeholder", + s"form.search.placeholder" + ) + ) + .map(placeholder(_)), + mods + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala deleted file mode 100644 index 7714490..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala +++ /dev/null @@ -1,82 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -/** Form components - * - * A form is a collection of sections, each of which contains a collection of - * fields. - * - * Form -> * Section -> * Field - * -> * Action - * - * Each Field has an `id`, `label` and `input` element. The `id` is used to - * link the label to the input. - * - * The `input` element can be a simple text input, or a more complex component - * such as a date picker. The caller is expected to make the input work. - */ -trait FormComponentsModule extends LocalDateSelectModule: - def forms: FormComponents - - trait FormComponents: - - /** Layout the sections and actions of a form */ - def form(mods: Modifier[HtmlElement]*)(sections: Modifier[HtmlElement]*)( - actions: Modifier[HtmlElement]* - ): HtmlElement - - /** Section layout, with a title, optional subtitle and any content. */ - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label and any content */ - def field(label: Modifier[HtmlElement])( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label, input and optional help text */ - def field( - id: String, - label: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement - - def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout an inline form element */ - def inlineForm(mods: Modifier[HtmlElement]*): HtmlElement - - /** An input field with a search icon */ - def searchField(id: String, placeholderText: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement - - /** LocalDate input */ - def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala deleted file mode 100644 index 8acbe7f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala +++ /dev/null @@ -1,28 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.Tag - -trait GenericComponentsModule: - - def generic: GenericComponents - - trait GenericComponents: - def tag(name: String, color: ColorKind): HtmlElement - def tag(t: Tag): HtmlElement = tag(t.value, t.color) - -trait DefaultGenericComponentsModule(using ComponentContext) - extends GenericComponentsModule: - override val generic: GenericComponents = new GenericComponents: - override def tag(name: String, color: ColorKind): HtmlElement = - p( - cls( - "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" - ), - color(800).text, - color(100).bg, - name - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala deleted file mode 100644 index 8cfc1d9..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala +++ /dev/null @@ -1,123 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.CustomAttrs - -trait IconsModule: - def icons: Icons - - trait Icons: - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement - def close(mods: Modifier[SvgElement]*): SvgElement - def `search-solid`(mods: Modifier[SvgElement]*): SvgElement - def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement - def `document-chart-bar-outline`(mods: Modifier[SvgElement]*): SvgElement - def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement - -trait DefaultIconsModule(using ComponentContext) extends IconsModule: - override val icons: Icons = new Icons: - import svg.* - - object aria: - val hidden = CustomAttrs.svg.ariaHidden - - private def withDefault( - mods: Seq[Modifier[SvgElement]], - default: Modifier[SvgElement] - ): Modifier[SvgElement] = - if mods.isEmpty then default else mods - - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-8 w-8"), - 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" - ) - ) - ) - - override def close(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-2 w-2"), - stroke := "currentColor", - fill := "none", - viewBox := "0 0 8 8", - path( - strokeLineCap := "round", - strokeWidth := "1.5", - d := "M1 1l6 6m0-6L1 7" - ) - ) - - override def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 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 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") - ) - ) - - override def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 text-gray-400")), - 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") - ) - ) - - override def `document-chart-bar-outline`( - mods: Modifier[SvgElement]* - ): SvgElement = - svg( - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - strokeWidth := "1.5", - stroke := "currentColor", - withDefault(mods, cls := "h-6 w-6"), - path( - strokeLineCap := "round", - strokeLineJoin := "round", - d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" - ) - ) - - override def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-5 w-5"), - cls := "flex-shrink-0 text-gray-400", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", - clipRule := "evenodd" - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala deleted file mode 100644 index 95ab654..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait LayoutModule: - def layout: LayoutComponents - - trait LayoutComponents: - def card(content: Modifier[HtmlElement]*): HtmlElement - -trait DefaultLayoutModule(using ctx: ComponentContext) extends LayoutModule: - override val layout: LayoutComponents = new LayoutComponents: - override def card(content: Modifier[HtmlElement]*): HtmlElement = - div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala deleted file mode 100644 index 5946974..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom.html.Paragraph -import com.raquo.laminar.nodes.TextNode - -trait ListComponentsModule: - - def list: ListComponents - - trait ListComponents(using ComponentContext): - def label(text: String, color: ColorKind): HtmlElement - def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI - def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] - def listSection(header: String, list: HtmlElement): Div - def navigation(sections: Modifier[HtmlElement]): HtmlElement - -trait DefaultListComponentsModule(using ComponentContext) - extends ListComponentsModule: - self: GenericComponentsModule => - - override val list: ListComponents = new ListComponents: - override def label( - text: String, - color: ColorKind - ): HtmlElement = generic.tag(text, color) - - override def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI = - li( - cls("group"), - div( - cls( - "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" - ), - avatar.map(a => - div( - cls("flex-shrink-0"), - div( - cls( - "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" - ), - a - ) - ) - ), - div( - cls("flex-1 min-w-0"), - p( - cls("text-sm font-medium text-gray-900"), - title, - span(cls("float-right"), right) - ), - subtitle.map(st => - p( - cls("text-sm text-gray-500 truncate"), - st - ) - ) - ) - ) - ) - - override def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = - ul( - cls("relative z-0 divide-y divide-gray-200"), - role("list"), - children - ) - - override def listSection( - header: String, - list: HtmlElement - ): Div = - div( - cls("relative"), - div( - cls( - "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" - ), - header - ), - list - ) - - override def navigation(sections: Modifier[HtmlElement]): HtmlElement = - navTag( - cls("flex-1 min-h-0 overflow-y-auto"), - sections - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala deleted file mode 100644 index 5b5490f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ModalComponentsModule: - - def modal: ModalComponents - - trait ModalComponents: - def modalDialog( - content: HtmlElement, - onClose: Modifier[HtmlElement] - ): HtmlElement - -trait DefaultModalComponentsModule(using ctx: ComponentContext) - extends ModalComponentsModule: - - override val modal: ModalComponents = new ModalComponents: - - override def modalDialog( - content: HtmlElement, - closeMod: Modifier[HtmlElement] - ): 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( - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")), - closeMod - ) - ) - - div( - cls("fixed inset-0 z-20 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - 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-7xl sm:align-middle" - ), - content - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala deleted file mode 100644 index b713d8a..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala +++ /dev/null @@ -1,71 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait PageComponentsModule: - - val page: PageComponents - - trait PageComponents: - def container( - children: Modifier[HtmlElement]* - ): HtmlElement - - def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement - - def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement] = emptyMod, - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement - - /** Visage for clickable text, like links - */ - def clickable: Modifier[HtmlElement] - -trait DefaultPageComponentsModule(using ComponentContext) - extends PageComponentsModule: - - override val page: PageComponents = new PageComponents: - override def container( - children: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), - children - ) - - override def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement = - div( - cls("p-8 bg-gray-100 h-full"), - header, - children - ) - - override def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement = - div( - cls("pb-5 border-b border-gray-200"), - div(cls("float-right"), right), - h1( - cls("text-2xl leading-6 font-medium text-gray-900"), - title - ), - subtitle.map( - p( - cls("text-sm font-medium text-gray-500"), - _ - ) - ) - ) - - override def clickable: Modifier[HtmlElement] = - cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala deleted file mode 100644 index f70ee87..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala +++ /dev/null @@ -1,108 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} - -trait TableComponentsModule: - def tables: TableComponents - - trait TableComponents: - def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )(table: Modifier[HtmlElement]*): HtmlElement - - def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement - - def headerRow(mods: Modifier[HtmlElement]*)( - cells: HtmlElement* - ): HtmlElement - - def dataRow(mods: Modifier[HtmlElement]*)(cells: HtmlElement*): HtmlElement - - def headerCell(content: Modifier[HtmlElement]): HtmlElement - - def dataCell(content: Modifier[HtmlElement]): HtmlElement - -trait DefaultTableComponentsModule extends TableComponentsModule: - - override lazy val tables: TableComponents = new TableComponents: - - override def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )( - table: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("px-4 sm:px-6 lg:px-8"), - div( - cls("sm:flex sm:items-center"), - div( - cls("sm:flex-auto"), - h1(cls("text-base font-semibold leading-6 text-gray-900"), title), - subtitle.map(st => p(cls("mt-2 text-sm text-gray-700"), st)) - ), - div(cls("mt-4 sm:ml-16 sm:mt-0 sm:flex-none"), actions) - ), - div( - cls("mt-8 flow-root"), - div( - cls("-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"), - div( - cls("inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"), - table - ) - ) - ) - ) - - override def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement = - table( - cls("min-w-full divide-y divide-gray-300"), - thead(header), - tbody(cls("divide-y divide-gray-200"), body) - ) - - override def headerRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-3.5 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-3.5 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-3.5 px-3")) - ) - ) - - override def dataRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - mods, - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-4 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-4 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-4 px-3")) - ) - ) - - override def headerCell(content: Modifier[HtmlElement]): HtmlElement = - th( - cls("text-left text-sm font-semibold text-gray-900"), - content - ) - - override def dataCell(content: Modifier[HtmlElement]): HtmlElement = - td( - cls("whitespace-nowrap text-sm text-gray-500"), - content - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala index b937d45..12d01ae 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala @@ -3,7 +3,7 @@ import com.raquo.laminar.api.L.{*, given} import works.iterative.ui.components.laminar.HtmlTabular import works.iterative.ui.model.tables.Tabular -import works.iterative.ui.components.laminar.TableComponentsModule +import works.iterative.ui.components.laminar.tailwind.ui.TableComponentsModule import works.iterative.core.UserMessage import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given import works.iterative.ui.components.tailwind.ComponentContext diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala new file mode 100644 index 0000000..87f3e8d --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala @@ -0,0 +1,54 @@ +package works.iterative.ui.components.laminar.tailwind.color + +import com.raquo.laminar.api.L.{*, given} + +/** Complete color definition that can be rendered to CSS. + * + * Includes the area, kind and weight of the color. + */ +case class Color(area: ColorArea, color: ColorDef): + def toCSS: String = s"${area.name}-${color.toCSS}" + +object Color: + import ColorDef.given + + given colorToCSS: Conversion[Color, HtmlMod] with + def apply(c: Color) = cls(c.toCSS) + + given colorToSVGCSS: Conversion[Color, SvgMod] with + def apply(c: Color) = svg.cls(c.toCSS) + + given colorSignalToCSS: Conversion[Signal[Color], HtmlMod] with + def apply(c: Signal[Color]) = cls <-- c.map(_.toCSS) + + given colorSignalToSVGCSS: Conversion[Signal[Color], SvgMod] with + def apply(c: Signal[Color]) = svg.cls <-- c.map(_.toCSS) + + def current = ColorDef(ColorKind.current) + def inherit = ColorDef(ColorKind.inherit) + def transp = ColorDef(ColorKind.transp) + def auto = ColorDef(ColorKind.auto) + def black = ColorDef(ColorKind.black) + def white = ColorDef(ColorKind.white) + def slate(weight: ColorWeight) = ColorDef(ColorKind.slate, weight) + def gray(weight: ColorWeight) = ColorDef(ColorKind.gray, weight) + def zinc(weight: ColorWeight) = ColorDef(ColorKind.zinc, weight) + def neutral(weight: ColorWeight) = ColorDef(ColorKind.neutral, weight) + def stone(weight: ColorWeight) = ColorDef(ColorKind.stone, weight) + def red(weight: ColorWeight) = ColorDef(ColorKind.red, weight) + def orange(weight: ColorWeight) = ColorDef(ColorKind.orange, weight) + def amber(weight: ColorWeight) = ColorDef(ColorKind.amber, weight) + def yellow(weight: ColorWeight) = ColorDef(ColorKind.yellow, weight) + def lime(weight: ColorWeight) = ColorDef(ColorKind.lime, weight) + def green(weight: ColorWeight) = ColorDef(ColorKind.green, weight) + def emerald(weight: ColorWeight) = ColorDef(ColorKind.emerald, weight) + def teal(weight: ColorWeight) = ColorDef(ColorKind.teal, weight) + def cyan(weight: ColorWeight) = ColorDef(ColorKind.cyan, weight) + def sky(weight: ColorWeight) = ColorDef(ColorKind.sky, weight) + def blue(weight: ColorWeight) = ColorDef(ColorKind.blue, weight) + def indigo(weight: ColorWeight) = ColorDef(ColorKind.indigo, weight) + def violet(weight: ColorWeight) = ColorDef(ColorKind.violet, weight) + def purple(weight: ColorWeight) = ColorDef(ColorKind.purple, weight) + def fuchsia(weight: ColorWeight) = ColorDef(ColorKind.fuchsia, weight) + def pink(weight: ColorWeight) = ColorDef(ColorKind.pink, weight) + def rose(weight: ColorWeight) = ColorDef(ColorKind.rose, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala new file mode 100644 index 0000000..4130840 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala @@ -0,0 +1,16 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines the area the color should apply to, eg. background, text, border, + * etc. + */ +enum ColorArea(val name: String): + case bg extends ColorArea("bg") + case text extends ColorArea("text") + case decoration extends ColorArea("decoration") + case border extends ColorArea("border") + case outline extends ColorArea("outline") + case divide extends ColorArea("divide") + case ring extends ColorArea("ring") + case ringOffset extends ColorArea("ring-offset") + case shadow extends ColorArea("shadow") + case accent extends ColorArea("accent") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala new file mode 100644 index 0000000..fab9404 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala @@ -0,0 +1,43 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** A combination of ColorKind and ColorWeight, if applicable. + * + * By applying area we get the full Color definition. + */ +sealed trait ColorDef: + def toCSS: String + + def bg = Color(ColorArea.bg, this) + def text = Color(ColorArea.text, this) + def decoration = Color(ColorArea.decoration, this) + def border = Color(ColorArea.border, this) + def outline = Color(ColorArea.outline, this) + def divide = Color(ColorArea.divide, this) + def ring = Color(ColorArea.ring, this) + def ringOffset = Color(ColorArea.ringOffset, this) + def shadow = Color(ColorArea.shadow, this) + def accent = Color(ColorArea.accent, this) + +// TODO: create a macro that will output the tailwind class as string directly during compilation, where possible. +object ColorDef: + case class WeightedColorDef( + kind: ColorKind, + weight: ColorWeight + ) extends ColorDef: + override def toCSS: String = s"${kind.name}-${weight.value}" + + case class UnweightedColorDef( + kind: ColorKind + ) extends ColorDef: + override def toCSS: String = kind.name + + // TODO: check that the kind is valid unweighted kind + // that means current, inherit, auto, transparent, black, white + // tried using implicit evidence, but the type inference for enumerations + // tends to generalize to the enum, instead of the real type + def apply[T <: ColorKind](kind: T)(using + ev: T <:< ColorKind.Unweighted + ): ColorDef = + UnweightedColorDef(kind) + def apply(kind: ColorKind, weight: ColorWeight): ColorDef = + WeightedColorDef(kind, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala new file mode 100644 index 0000000..865166b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala @@ -0,0 +1,47 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines what color should be used, without specifying the area or weight. + */ +sealed abstract class ColorKind private (val name: String): + def apply(weight: ColorWeight): ColorDef = + ColorDef.WeightedColorDef(this, weight) + +object ColorKind: + trait Unweighted: + self: ColorKind => + override def apply(weight: ColorWeight): ColorDef = + ColorDef.UnweightedColorDef(self) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case object current extends ColorKind("current") with Unweighted + case object inherit extends ColorKind("inherit") with Unweighted + // Not present in for all methods + case object transp extends ColorKind("transparent") with Unweighted + // Seen in accent, not preset otherwise + case object auto extends ColorKind("auto") with Unweighted + // Black and white do not have weight + case object black extends ColorKind("black") with Unweighted + case object white extends ColorKind("white") with Unweighted + case object slate extends ColorKind("slate") + case object gray extends ColorKind("gray") + case object zinc extends ColorKind("zinc") + case object neutral extends ColorKind("neutral") + case object stone extends ColorKind("stone") + case object red extends ColorKind("red") + case object orange extends ColorKind("orange") + case object amber extends ColorKind("amber") + case object yellow extends ColorKind("yellow") + case object lime extends ColorKind("lime") + case object green extends ColorKind("green") + case object emerald extends ColorKind("emerald") + case object teal extends ColorKind("teal") + case object cyan extends ColorKind("cyan") + case object sky extends ColorKind("sky") + case object blue extends ColorKind("blue") + case object indigo extends ColorKind("indigo") + case object violet extends ColorKind("violet") + case object purple extends ColorKind("purple") + case object fuchsia extends ColorKind("fuchsia") + case object pink extends ColorKind("pink") + case object rose extends ColorKind("rose") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala new file mode 100644 index 0000000..6f1a4cc --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala @@ -0,0 +1,33 @@ +package works.iterative.ui.components.laminar.tailwind.color + +opaque type ColorWeight = String + +extension (c: ColorWeight) def value: String = c + +/** Defines weight of a color, eg. 50, 100, 200, etc. + * + * Tailwind-like. + */ +object ColorWeight: + inline given int50: Conversion[50, ColorWeight] with + inline def apply(i: 50) = "50" + inline given int100: Conversion[100, ColorWeight] with + inline def apply(i: 100) = "100" + inline given int200: Conversion[200, ColorWeight] with + inline def apply(i: 200) = "200" + inline given int300: Conversion[300, ColorWeight] with + inline def apply(i: 300) = "300" + inline given int400: Conversion[400, ColorWeight] with + inline def apply(i: 400) = "400" + inline given int500: Conversion[500, ColorWeight] with + inline def apply(i: 500) = "500" + inline given int600: Conversion[600, ColorWeight] with + inline def apply(i: 600) = "600" + inline given int700: Conversion[700, ColorWeight] with + inline def apply(i: 700) = "700" + inline given int800: Conversion[800, ColorWeight] with + inline def apply(i: 800) = "800" + inline given int900: Conversion[900, ColorWeight] with + inline def apply(i: 900) = "900" + inline given int950: Conversion[950, ColorWeight] with + inline def apply(i: 950) = "950" diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala new file mode 100644 index 0000000..21e21f0 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala @@ -0,0 +1,12 @@ +package works.iterative.ui.components.laminar.tailwind + +/** We need a generic Color model that can be used both on server and on client. + * + * We have adopted the Tailwind model for now. There is nothing inherently + * Tailwind-specific in the implementation, but all the values are taken from + * their palette and the area model is very HTML biased. + * + * Still, I think that it is a good starting point for exploration and will + * satisfy our current needs. + */ +package object color {} diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" new file mode 100644 index 0000000..5c4e218 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" @@ -0,0 +1,21 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.model.color.ColorKind +import works.iterative.ui.components.tailwind.ComponentContext +import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given +import works.iterative.ui.model.Tag + +trait TailwindUIBadgesComponentsModule(using ComponentContext): + + object badges: + override def tag(name: String, color: ColorKind): HtmlElement = + p( + cls( + "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" + ), + color(800).text, + color(100).bg, + name + ) diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" new file mode 100644 index 0000000..0c9ca25 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" @@ -0,0 +1,24 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} + +trait TailwindUIHeadingsComponentsModule: + val headings: HeadingsComponents = new HeadingsComponents + + class HeadingsComponents: + def section(title: Node, actions: HtmlElement*): HtmlElement = + div( + cls("border-b border-gray-200 pb-5"), + div( + cls("sm:flex sm:items-center sm:justify-between"), + h3(cls("text-base font-semibold leading-6 text-gray-900"), title), + div( + cls("mt-3 flex sm:ml-4 sm:mt-0"), + actions match + case Nil => emptyMod + case first :: rest => + first :: rest.map(_.amend(cls("ml-3"))) + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala new file mode 100644 index 0000000..16ca01b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala @@ -0,0 +1,19 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.laminar.tailwind.color.ColorKind + +trait BadgeComponentsModule: + + object badges: + def pill(name: String, color: ColorKind): HtmlElement = + p( + cls( + "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" + ), + color(800).text, + color(100).bg, + name + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala index a2c637c..bfa063a 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala @@ -1,13 +1,11 @@ package works.iterative.ui.components.laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorDef import com.raquo.laminar.tags.HtmlTag case class Bin[Source, +Value]( label: String, description: Option[String | HtmlElement], - color: ColorDef, valueOf: Source => Value ): def map[NewValue]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala deleted file mode 100644 index 3aac0bf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala +++ /dev/null @@ -1,34 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ButtonComponentsModule: - def buttons: ButtonComponents - - trait ButtonComponents: - def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala deleted file mode 100644 index 646e73d..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala +++ /dev/null @@ -1,191 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.color.ColorDef -import com.raquo.laminar.tags.HtmlTag -import org.scalajs.dom.html.UList -import com.raquo.laminar.nodes.ReactiveHtmlElement -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind - -trait DashboardComponentsModule: - def dashboard: DashboardComponents - - // Only methods that create HtmlElements - trait DashboardComponents(using ComponentContext): - def section(label: String, children: Modifier[HtmlElement]): Div - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI - def cardInitials( - initials: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div - def count(label: String, value: Int, color: ColorDef): Span - def counts(children: HtmlElement*): Span - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - -trait DefaultDashboardComponentsModule(using ComponentContext) - extends DashboardComponentsModule: - override val dashboard: DashboardComponents = new DashboardComponents: - def section(label: String, children: Modifier[HtmlElement]): Div = - div( - cls("mt-3"), - h2( - cls("text-gray-500 text-xs font-medium uppercase tracking-wide"), - label - ), - children - ) - - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] = - ul( - cls( - "mt-3 grid gap-5 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" - ), - children - ) - - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI = - li( - div( - cls("col-span-1 flex shadow-sm rounded-md"), - initials, - div( - cls( - "flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate" - ), - div( - cls("flex-1 px-4 py-2 text-sm truncate"), - div( - cls("text-gray-900 font-medium hover:text-gray-600 truncate"), - label - ), - p(cls("text-gray-500"), children) - ) - ) - ) - ) - - def cardInitials( - id: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div = - div( - cls( - "flex-shrink-0 flex flex-col items-center justify-center w-16 text-white text-sm font-medium rounded-l-md" - ), - color.map(_(600).bg), - div(id), - children - ) - - def count(label: String, value: Int, color: ColorDef): Span = - span(color.text, title(label), s"${value}") - - def counts(children: HtmlElement*): Span = - span(interleave(children, span(" / "))) - - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div = - div( - cls("flex flex-col mt-2"), - div( - cls( - "align-middle min-w-full shadow sm:rounded-lg" - ), - L.table( - cls("min-w-full border-separate border-spacing-0"), - styleAttr("border-spacing: 0"), // Tailwind somehow doesn't work - thead( - tr( - cls("border-t border-gray-200"), - headers.map(h => - th( - cls( - "sticky top-0 z-10 px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - h - ) - ) - ) - ), - tbody(cls("bg-white divide-y divide-gray-100"), sections) - ) - ) - ) - - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - td( - cls("bg-gray-50 border-gray-200 border-t border-b"), - cls( - "sticky top-14 z-10 bg-gray-50 px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - colSpan(cs), - label - ), - mods - ) - - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - mods, - td( - cls( - "px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900 truncate" - ), - label - ), - children.map(e => - td( - cls( - "whitespace-nowrap px-2 py-2 text-sm text-gray-500" - ), - e - ) - ) - ) - - private def interleave( - a: Seq[HtmlElement], - separator: => HtmlElement - ): Seq[HtmlElement] = - if a.size < 2 then a - else a.init.flatMap(n => Seq(n, separator)) :+ a.last diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala deleted file mode 100644 index a45beaf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.FileRef - -trait DetailComponentsModule: - - def details: DetailComponents - - trait DetailComponents: - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement - - def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement - - def files(fs: List[FileRef]): HtmlElement - - def file(f: FileRef): HtmlElement - -trait DefaultDetailComponentsModule(using ctx: ComponentContext) - extends DetailComponentsModule: - self: IconsModule => - - override val details: DetailComponents = new DetailComponents: - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement = - div( - div( - cls := "px-4 sm:px-0", - h3( - cls := "text-base font-semibold leading-7 text-gray-900", - title - ), - subtitle.map(st => - p( - cls := "mt-1 max-w-2xl text-sm leading-6 text-gray-500", - st - ) - ) - // TODO: actions - ), - div( - cls := "mt-6 border-t border-gray-100", - dl( - cls := "divide-y divide-gray-100", - fields - ) - ) - ) - - override def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0", - dt( - cls := "text-sm font-medium leading-6 text-gray-900", - title - ), - dd( - cls := "mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0", - content - ) - ) - - override def files(fs: List[FileRef]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-100 rounded-md border border-gray-200", - fs.map(file) - ) - - override def file(f: FileRef): HtmlElement = - li( - cls := "flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6", - div( - cls := "flex w-0 flex-1 items-center", - icons.`paper-clip-solid`(), - div( - cls := "ml-4 flex min-w-0 flex-1 gap-2", - span( - cls := "truncate font-medium", - f.name - ), - f.size.map(size => span(cls := "flex-shrink-0 text-gray-400", size)) - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - "Uložit" - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala new file mode 100644 index 0000000..f6c583f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait FormComponents(using ctx: ComponentContext) extends LocalDateSelectModule: + def searchIcon: SvgElement + + def renderLocalDateSelect( + id: String, + labelText: Option[String], + placeholderText: Option[String], + mods: LocalDateSelect => Modifier[HtmlElement] + ): HtmlElement = + div( + labelText, + input( + idAttr(id), + nameAttr(id), + autoComplete("date"), + tpe("date"), + cls( + "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ), + placeholderText.map(placeholder(_)), + mods(localDateSelect) + ) + ) + + def searchField( + id: String, + placeholderText: Option[String] = None + )( + mods: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls := "flex-1 min-w-0", + label(ctx.messages("forms.search.label"), forId(id), cls("sr-only")), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + searchIcon + ), + input( + tpe := "search", + nameAttr := "search", + idAttr := id, + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholderText + .orElse( + ctx.messages + .opt( + s"forms.search.${id}.placeholder", + s"form.search.placeholder" + ) + ) + .map(placeholder(_)), + mods + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala deleted file mode 100644 index 7714490..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala +++ /dev/null @@ -1,82 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -/** Form components - * - * A form is a collection of sections, each of which contains a collection of - * fields. - * - * Form -> * Section -> * Field - * -> * Action - * - * Each Field has an `id`, `label` and `input` element. The `id` is used to - * link the label to the input. - * - * The `input` element can be a simple text input, or a more complex component - * such as a date picker. The caller is expected to make the input work. - */ -trait FormComponentsModule extends LocalDateSelectModule: - def forms: FormComponents - - trait FormComponents: - - /** Layout the sections and actions of a form */ - def form(mods: Modifier[HtmlElement]*)(sections: Modifier[HtmlElement]*)( - actions: Modifier[HtmlElement]* - ): HtmlElement - - /** Section layout, with a title, optional subtitle and any content. */ - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label and any content */ - def field(label: Modifier[HtmlElement])( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label, input and optional help text */ - def field( - id: String, - label: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement - - def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout an inline form element */ - def inlineForm(mods: Modifier[HtmlElement]*): HtmlElement - - /** An input field with a search icon */ - def searchField(id: String, placeholderText: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement - - /** LocalDate input */ - def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala deleted file mode 100644 index 8acbe7f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala +++ /dev/null @@ -1,28 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.Tag - -trait GenericComponentsModule: - - def generic: GenericComponents - - trait GenericComponents: - def tag(name: String, color: ColorKind): HtmlElement - def tag(t: Tag): HtmlElement = tag(t.value, t.color) - -trait DefaultGenericComponentsModule(using ComponentContext) - extends GenericComponentsModule: - override val generic: GenericComponents = new GenericComponents: - override def tag(name: String, color: ColorKind): HtmlElement = - p( - cls( - "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" - ), - color(800).text, - color(100).bg, - name - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala deleted file mode 100644 index 8cfc1d9..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala +++ /dev/null @@ -1,123 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.CustomAttrs - -trait IconsModule: - def icons: Icons - - trait Icons: - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement - def close(mods: Modifier[SvgElement]*): SvgElement - def `search-solid`(mods: Modifier[SvgElement]*): SvgElement - def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement - def `document-chart-bar-outline`(mods: Modifier[SvgElement]*): SvgElement - def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement - -trait DefaultIconsModule(using ComponentContext) extends IconsModule: - override val icons: Icons = new Icons: - import svg.* - - object aria: - val hidden = CustomAttrs.svg.ariaHidden - - private def withDefault( - mods: Seq[Modifier[SvgElement]], - default: Modifier[SvgElement] - ): Modifier[SvgElement] = - if mods.isEmpty then default else mods - - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-8 w-8"), - 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" - ) - ) - ) - - override def close(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-2 w-2"), - stroke := "currentColor", - fill := "none", - viewBox := "0 0 8 8", - path( - strokeLineCap := "round", - strokeWidth := "1.5", - d := "M1 1l6 6m0-6L1 7" - ) - ) - - override def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 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 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") - ) - ) - - override def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 text-gray-400")), - 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") - ) - ) - - override def `document-chart-bar-outline`( - mods: Modifier[SvgElement]* - ): SvgElement = - svg( - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - strokeWidth := "1.5", - stroke := "currentColor", - withDefault(mods, cls := "h-6 w-6"), - path( - strokeLineCap := "round", - strokeLineJoin := "round", - d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" - ) - ) - - override def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-5 w-5"), - cls := "flex-shrink-0 text-gray-400", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", - clipRule := "evenodd" - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala deleted file mode 100644 index 95ab654..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait LayoutModule: - def layout: LayoutComponents - - trait LayoutComponents: - def card(content: Modifier[HtmlElement]*): HtmlElement - -trait DefaultLayoutModule(using ctx: ComponentContext) extends LayoutModule: - override val layout: LayoutComponents = new LayoutComponents: - override def card(content: Modifier[HtmlElement]*): HtmlElement = - div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala deleted file mode 100644 index 5946974..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom.html.Paragraph -import com.raquo.laminar.nodes.TextNode - -trait ListComponentsModule: - - def list: ListComponents - - trait ListComponents(using ComponentContext): - def label(text: String, color: ColorKind): HtmlElement - def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI - def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] - def listSection(header: String, list: HtmlElement): Div - def navigation(sections: Modifier[HtmlElement]): HtmlElement - -trait DefaultListComponentsModule(using ComponentContext) - extends ListComponentsModule: - self: GenericComponentsModule => - - override val list: ListComponents = new ListComponents: - override def label( - text: String, - color: ColorKind - ): HtmlElement = generic.tag(text, color) - - override def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI = - li( - cls("group"), - div( - cls( - "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" - ), - avatar.map(a => - div( - cls("flex-shrink-0"), - div( - cls( - "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" - ), - a - ) - ) - ), - div( - cls("flex-1 min-w-0"), - p( - cls("text-sm font-medium text-gray-900"), - title, - span(cls("float-right"), right) - ), - subtitle.map(st => - p( - cls("text-sm text-gray-500 truncate"), - st - ) - ) - ) - ) - ) - - override def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = - ul( - cls("relative z-0 divide-y divide-gray-200"), - role("list"), - children - ) - - override def listSection( - header: String, - list: HtmlElement - ): Div = - div( - cls("relative"), - div( - cls( - "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" - ), - header - ), - list - ) - - override def navigation(sections: Modifier[HtmlElement]): HtmlElement = - navTag( - cls("flex-1 min-h-0 overflow-y-auto"), - sections - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala deleted file mode 100644 index 5b5490f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ModalComponentsModule: - - def modal: ModalComponents - - trait ModalComponents: - def modalDialog( - content: HtmlElement, - onClose: Modifier[HtmlElement] - ): HtmlElement - -trait DefaultModalComponentsModule(using ctx: ComponentContext) - extends ModalComponentsModule: - - override val modal: ModalComponents = new ModalComponents: - - override def modalDialog( - content: HtmlElement, - closeMod: Modifier[HtmlElement] - ): 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( - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")), - closeMod - ) - ) - - div( - cls("fixed inset-0 z-20 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - 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-7xl sm:align-middle" - ), - content - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala deleted file mode 100644 index b713d8a..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala +++ /dev/null @@ -1,71 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait PageComponentsModule: - - val page: PageComponents - - trait PageComponents: - def container( - children: Modifier[HtmlElement]* - ): HtmlElement - - def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement - - def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement] = emptyMod, - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement - - /** Visage for clickable text, like links - */ - def clickable: Modifier[HtmlElement] - -trait DefaultPageComponentsModule(using ComponentContext) - extends PageComponentsModule: - - override val page: PageComponents = new PageComponents: - override def container( - children: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), - children - ) - - override def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement = - div( - cls("p-8 bg-gray-100 h-full"), - header, - children - ) - - override def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement = - div( - cls("pb-5 border-b border-gray-200"), - div(cls("float-right"), right), - h1( - cls("text-2xl leading-6 font-medium text-gray-900"), - title - ), - subtitle.map( - p( - cls("text-sm font-medium text-gray-500"), - _ - ) - ) - ) - - override def clickable: Modifier[HtmlElement] = - cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala deleted file mode 100644 index f70ee87..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala +++ /dev/null @@ -1,108 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} - -trait TableComponentsModule: - def tables: TableComponents - - trait TableComponents: - def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )(table: Modifier[HtmlElement]*): HtmlElement - - def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement - - def headerRow(mods: Modifier[HtmlElement]*)( - cells: HtmlElement* - ): HtmlElement - - def dataRow(mods: Modifier[HtmlElement]*)(cells: HtmlElement*): HtmlElement - - def headerCell(content: Modifier[HtmlElement]): HtmlElement - - def dataCell(content: Modifier[HtmlElement]): HtmlElement - -trait DefaultTableComponentsModule extends TableComponentsModule: - - override lazy val tables: TableComponents = new TableComponents: - - override def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )( - table: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("px-4 sm:px-6 lg:px-8"), - div( - cls("sm:flex sm:items-center"), - div( - cls("sm:flex-auto"), - h1(cls("text-base font-semibold leading-6 text-gray-900"), title), - subtitle.map(st => p(cls("mt-2 text-sm text-gray-700"), st)) - ), - div(cls("mt-4 sm:ml-16 sm:mt-0 sm:flex-none"), actions) - ), - div( - cls("mt-8 flow-root"), - div( - cls("-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"), - div( - cls("inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"), - table - ) - ) - ) - ) - - override def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement = - table( - cls("min-w-full divide-y divide-gray-300"), - thead(header), - tbody(cls("divide-y divide-gray-200"), body) - ) - - override def headerRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-3.5 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-3.5 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-3.5 px-3")) - ) - ) - - override def dataRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - mods, - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-4 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-4 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-4 px-3")) - ) - ) - - override def headerCell(content: Modifier[HtmlElement]): HtmlElement = - th( - cls("text-left text-sm font-semibold text-gray-900"), - content - ) - - override def dataCell(content: Modifier[HtmlElement]): HtmlElement = - td( - cls("whitespace-nowrap text-sm text-gray-500"), - content - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala index b937d45..12d01ae 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala @@ -3,7 +3,7 @@ import com.raquo.laminar.api.L.{*, given} import works.iterative.ui.components.laminar.HtmlTabular import works.iterative.ui.model.tables.Tabular -import works.iterative.ui.components.laminar.TableComponentsModule +import works.iterative.ui.components.laminar.tailwind.ui.TableComponentsModule import works.iterative.core.UserMessage import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given import works.iterative.ui.components.tailwind.ComponentContext diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala new file mode 100644 index 0000000..87f3e8d --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala @@ -0,0 +1,54 @@ +package works.iterative.ui.components.laminar.tailwind.color + +import com.raquo.laminar.api.L.{*, given} + +/** Complete color definition that can be rendered to CSS. + * + * Includes the area, kind and weight of the color. + */ +case class Color(area: ColorArea, color: ColorDef): + def toCSS: String = s"${area.name}-${color.toCSS}" + +object Color: + import ColorDef.given + + given colorToCSS: Conversion[Color, HtmlMod] with + def apply(c: Color) = cls(c.toCSS) + + given colorToSVGCSS: Conversion[Color, SvgMod] with + def apply(c: Color) = svg.cls(c.toCSS) + + given colorSignalToCSS: Conversion[Signal[Color], HtmlMod] with + def apply(c: Signal[Color]) = cls <-- c.map(_.toCSS) + + given colorSignalToSVGCSS: Conversion[Signal[Color], SvgMod] with + def apply(c: Signal[Color]) = svg.cls <-- c.map(_.toCSS) + + def current = ColorDef(ColorKind.current) + def inherit = ColorDef(ColorKind.inherit) + def transp = ColorDef(ColorKind.transp) + def auto = ColorDef(ColorKind.auto) + def black = ColorDef(ColorKind.black) + def white = ColorDef(ColorKind.white) + def slate(weight: ColorWeight) = ColorDef(ColorKind.slate, weight) + def gray(weight: ColorWeight) = ColorDef(ColorKind.gray, weight) + def zinc(weight: ColorWeight) = ColorDef(ColorKind.zinc, weight) + def neutral(weight: ColorWeight) = ColorDef(ColorKind.neutral, weight) + def stone(weight: ColorWeight) = ColorDef(ColorKind.stone, weight) + def red(weight: ColorWeight) = ColorDef(ColorKind.red, weight) + def orange(weight: ColorWeight) = ColorDef(ColorKind.orange, weight) + def amber(weight: ColorWeight) = ColorDef(ColorKind.amber, weight) + def yellow(weight: ColorWeight) = ColorDef(ColorKind.yellow, weight) + def lime(weight: ColorWeight) = ColorDef(ColorKind.lime, weight) + def green(weight: ColorWeight) = ColorDef(ColorKind.green, weight) + def emerald(weight: ColorWeight) = ColorDef(ColorKind.emerald, weight) + def teal(weight: ColorWeight) = ColorDef(ColorKind.teal, weight) + def cyan(weight: ColorWeight) = ColorDef(ColorKind.cyan, weight) + def sky(weight: ColorWeight) = ColorDef(ColorKind.sky, weight) + def blue(weight: ColorWeight) = ColorDef(ColorKind.blue, weight) + def indigo(weight: ColorWeight) = ColorDef(ColorKind.indigo, weight) + def violet(weight: ColorWeight) = ColorDef(ColorKind.violet, weight) + def purple(weight: ColorWeight) = ColorDef(ColorKind.purple, weight) + def fuchsia(weight: ColorWeight) = ColorDef(ColorKind.fuchsia, weight) + def pink(weight: ColorWeight) = ColorDef(ColorKind.pink, weight) + def rose(weight: ColorWeight) = ColorDef(ColorKind.rose, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala new file mode 100644 index 0000000..4130840 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala @@ -0,0 +1,16 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines the area the color should apply to, eg. background, text, border, + * etc. + */ +enum ColorArea(val name: String): + case bg extends ColorArea("bg") + case text extends ColorArea("text") + case decoration extends ColorArea("decoration") + case border extends ColorArea("border") + case outline extends ColorArea("outline") + case divide extends ColorArea("divide") + case ring extends ColorArea("ring") + case ringOffset extends ColorArea("ring-offset") + case shadow extends ColorArea("shadow") + case accent extends ColorArea("accent") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala new file mode 100644 index 0000000..fab9404 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala @@ -0,0 +1,43 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** A combination of ColorKind and ColorWeight, if applicable. + * + * By applying area we get the full Color definition. + */ +sealed trait ColorDef: + def toCSS: String + + def bg = Color(ColorArea.bg, this) + def text = Color(ColorArea.text, this) + def decoration = Color(ColorArea.decoration, this) + def border = Color(ColorArea.border, this) + def outline = Color(ColorArea.outline, this) + def divide = Color(ColorArea.divide, this) + def ring = Color(ColorArea.ring, this) + def ringOffset = Color(ColorArea.ringOffset, this) + def shadow = Color(ColorArea.shadow, this) + def accent = Color(ColorArea.accent, this) + +// TODO: create a macro that will output the tailwind class as string directly during compilation, where possible. +object ColorDef: + case class WeightedColorDef( + kind: ColorKind, + weight: ColorWeight + ) extends ColorDef: + override def toCSS: String = s"${kind.name}-${weight.value}" + + case class UnweightedColorDef( + kind: ColorKind + ) extends ColorDef: + override def toCSS: String = kind.name + + // TODO: check that the kind is valid unweighted kind + // that means current, inherit, auto, transparent, black, white + // tried using implicit evidence, but the type inference for enumerations + // tends to generalize to the enum, instead of the real type + def apply[T <: ColorKind](kind: T)(using + ev: T <:< ColorKind.Unweighted + ): ColorDef = + UnweightedColorDef(kind) + def apply(kind: ColorKind, weight: ColorWeight): ColorDef = + WeightedColorDef(kind, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala new file mode 100644 index 0000000..865166b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala @@ -0,0 +1,47 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines what color should be used, without specifying the area or weight. + */ +sealed abstract class ColorKind private (val name: String): + def apply(weight: ColorWeight): ColorDef = + ColorDef.WeightedColorDef(this, weight) + +object ColorKind: + trait Unweighted: + self: ColorKind => + override def apply(weight: ColorWeight): ColorDef = + ColorDef.UnweightedColorDef(self) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case object current extends ColorKind("current") with Unweighted + case object inherit extends ColorKind("inherit") with Unweighted + // Not present in for all methods + case object transp extends ColorKind("transparent") with Unweighted + // Seen in accent, not preset otherwise + case object auto extends ColorKind("auto") with Unweighted + // Black and white do not have weight + case object black extends ColorKind("black") with Unweighted + case object white extends ColorKind("white") with Unweighted + case object slate extends ColorKind("slate") + case object gray extends ColorKind("gray") + case object zinc extends ColorKind("zinc") + case object neutral extends ColorKind("neutral") + case object stone extends ColorKind("stone") + case object red extends ColorKind("red") + case object orange extends ColorKind("orange") + case object amber extends ColorKind("amber") + case object yellow extends ColorKind("yellow") + case object lime extends ColorKind("lime") + case object green extends ColorKind("green") + case object emerald extends ColorKind("emerald") + case object teal extends ColorKind("teal") + case object cyan extends ColorKind("cyan") + case object sky extends ColorKind("sky") + case object blue extends ColorKind("blue") + case object indigo extends ColorKind("indigo") + case object violet extends ColorKind("violet") + case object purple extends ColorKind("purple") + case object fuchsia extends ColorKind("fuchsia") + case object pink extends ColorKind("pink") + case object rose extends ColorKind("rose") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala new file mode 100644 index 0000000..6f1a4cc --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala @@ -0,0 +1,33 @@ +package works.iterative.ui.components.laminar.tailwind.color + +opaque type ColorWeight = String + +extension (c: ColorWeight) def value: String = c + +/** Defines weight of a color, eg. 50, 100, 200, etc. + * + * Tailwind-like. + */ +object ColorWeight: + inline given int50: Conversion[50, ColorWeight] with + inline def apply(i: 50) = "50" + inline given int100: Conversion[100, ColorWeight] with + inline def apply(i: 100) = "100" + inline given int200: Conversion[200, ColorWeight] with + inline def apply(i: 200) = "200" + inline given int300: Conversion[300, ColorWeight] with + inline def apply(i: 300) = "300" + inline given int400: Conversion[400, ColorWeight] with + inline def apply(i: 400) = "400" + inline given int500: Conversion[500, ColorWeight] with + inline def apply(i: 500) = "500" + inline given int600: Conversion[600, ColorWeight] with + inline def apply(i: 600) = "600" + inline given int700: Conversion[700, ColorWeight] with + inline def apply(i: 700) = "700" + inline given int800: Conversion[800, ColorWeight] with + inline def apply(i: 800) = "800" + inline given int900: Conversion[900, ColorWeight] with + inline def apply(i: 900) = "900" + inline given int950: Conversion[950, ColorWeight] with + inline def apply(i: 950) = "950" diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala new file mode 100644 index 0000000..21e21f0 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala @@ -0,0 +1,12 @@ +package works.iterative.ui.components.laminar.tailwind + +/** We need a generic Color model that can be used both on server and on client. + * + * We have adopted the Tailwind model for now. There is nothing inherently + * Tailwind-specific in the implementation, but all the values are taken from + * their palette and the area model is very HTML biased. + * + * Still, I think that it is a good starting point for exploration and will + * satisfy our current needs. + */ +package object color {} diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" new file mode 100644 index 0000000..5c4e218 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" @@ -0,0 +1,21 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.model.color.ColorKind +import works.iterative.ui.components.tailwind.ComponentContext +import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given +import works.iterative.ui.model.Tag + +trait TailwindUIBadgesComponentsModule(using ComponentContext): + + object badges: + override def tag(name: String, color: ColorKind): HtmlElement = + p( + cls( + "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" + ), + color(800).text, + color(100).bg, + name + ) diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" new file mode 100644 index 0000000..0c9ca25 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" @@ -0,0 +1,24 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} + +trait TailwindUIHeadingsComponentsModule: + val headings: HeadingsComponents = new HeadingsComponents + + class HeadingsComponents: + def section(title: Node, actions: HtmlElement*): HtmlElement = + div( + cls("border-b border-gray-200 pb-5"), + div( + cls("sm:flex sm:items-center sm:justify-between"), + h3(cls("text-base font-semibold leading-6 text-gray-900"), title), + div( + cls("mt-3 flex sm:ml-4 sm:mt-0"), + actions match + case Nil => emptyMod + case first :: rest => + first :: rest.map(_.amend(cls("ml-3"))) + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala new file mode 100644 index 0000000..16ca01b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala @@ -0,0 +1,19 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.laminar.tailwind.color.ColorKind + +trait BadgeComponentsModule: + + object badges: + def pill(name: String, color: ColorKind): HtmlElement = + p( + cls( + "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" + ), + color(800).text, + color(100).bg, + name + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala new file mode 100644 index 0000000..ebb2968 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} + +trait ButtonComponentsModule: + + object buttons: + + private inline def srHelp(text: String): Modifier[HtmlElement] = + span(cls := "sr-only", text) + + def primaryButton( + id: String, + text: Modifier[HtmlElement], + icon: Option[SvgElement] = None, + buttonType: String = "submit" + )(mods: Modifier[HtmlElement]*): HtmlElement = + button( + tpe(buttonType), + cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + icon, + text, + mods + ) + + def secondaryButton( + id: String, + text: Modifier[HtmlElement], + icon: Option[SvgElement] = None, + buttonType: String = "button" + )(mods: Modifier[HtmlElement]*): HtmlElement = + button( + tpe(buttonType), + cls( + "ml-2 bg-white inline-flex justify-center 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" + ), + icon, + text, + mods + ) + + def inlineButton(id: String, icon: SvgElement)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + button( + tpe := "button", + cls := "ml-1 inline-flex h-4 w-4 flex-shrink-0 rounded-full p-1 text-gray-400 hover:bg-gray-200 hover:text-gray-500", + srHelp(id), + icon + ) + + def iconButton(id: String, icon: SvgElement, srText: Option[String] = None)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + button( + tpe := "button", + cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", + icon, + srText.map(srHelp(_)) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala index a2c637c..bfa063a 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala @@ -1,13 +1,11 @@ package works.iterative.ui.components.laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorDef import com.raquo.laminar.tags.HtmlTag case class Bin[Source, +Value]( label: String, description: Option[String | HtmlElement], - color: ColorDef, valueOf: Source => Value ): def map[NewValue]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala deleted file mode 100644 index 3aac0bf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala +++ /dev/null @@ -1,34 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ButtonComponentsModule: - def buttons: ButtonComponents - - trait ButtonComponents: - def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala deleted file mode 100644 index 646e73d..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala +++ /dev/null @@ -1,191 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.color.ColorDef -import com.raquo.laminar.tags.HtmlTag -import org.scalajs.dom.html.UList -import com.raquo.laminar.nodes.ReactiveHtmlElement -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind - -trait DashboardComponentsModule: - def dashboard: DashboardComponents - - // Only methods that create HtmlElements - trait DashboardComponents(using ComponentContext): - def section(label: String, children: Modifier[HtmlElement]): Div - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI - def cardInitials( - initials: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div - def count(label: String, value: Int, color: ColorDef): Span - def counts(children: HtmlElement*): Span - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - -trait DefaultDashboardComponentsModule(using ComponentContext) - extends DashboardComponentsModule: - override val dashboard: DashboardComponents = new DashboardComponents: - def section(label: String, children: Modifier[HtmlElement]): Div = - div( - cls("mt-3"), - h2( - cls("text-gray-500 text-xs font-medium uppercase tracking-wide"), - label - ), - children - ) - - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] = - ul( - cls( - "mt-3 grid gap-5 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" - ), - children - ) - - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI = - li( - div( - cls("col-span-1 flex shadow-sm rounded-md"), - initials, - div( - cls( - "flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate" - ), - div( - cls("flex-1 px-4 py-2 text-sm truncate"), - div( - cls("text-gray-900 font-medium hover:text-gray-600 truncate"), - label - ), - p(cls("text-gray-500"), children) - ) - ) - ) - ) - - def cardInitials( - id: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div = - div( - cls( - "flex-shrink-0 flex flex-col items-center justify-center w-16 text-white text-sm font-medium rounded-l-md" - ), - color.map(_(600).bg), - div(id), - children - ) - - def count(label: String, value: Int, color: ColorDef): Span = - span(color.text, title(label), s"${value}") - - def counts(children: HtmlElement*): Span = - span(interleave(children, span(" / "))) - - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div = - div( - cls("flex flex-col mt-2"), - div( - cls( - "align-middle min-w-full shadow sm:rounded-lg" - ), - L.table( - cls("min-w-full border-separate border-spacing-0"), - styleAttr("border-spacing: 0"), // Tailwind somehow doesn't work - thead( - tr( - cls("border-t border-gray-200"), - headers.map(h => - th( - cls( - "sticky top-0 z-10 px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - h - ) - ) - ) - ), - tbody(cls("bg-white divide-y divide-gray-100"), sections) - ) - ) - ) - - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - td( - cls("bg-gray-50 border-gray-200 border-t border-b"), - cls( - "sticky top-14 z-10 bg-gray-50 px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - colSpan(cs), - label - ), - mods - ) - - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - mods, - td( - cls( - "px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900 truncate" - ), - label - ), - children.map(e => - td( - cls( - "whitespace-nowrap px-2 py-2 text-sm text-gray-500" - ), - e - ) - ) - ) - - private def interleave( - a: Seq[HtmlElement], - separator: => HtmlElement - ): Seq[HtmlElement] = - if a.size < 2 then a - else a.init.flatMap(n => Seq(n, separator)) :+ a.last diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala deleted file mode 100644 index a45beaf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.FileRef - -trait DetailComponentsModule: - - def details: DetailComponents - - trait DetailComponents: - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement - - def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement - - def files(fs: List[FileRef]): HtmlElement - - def file(f: FileRef): HtmlElement - -trait DefaultDetailComponentsModule(using ctx: ComponentContext) - extends DetailComponentsModule: - self: IconsModule => - - override val details: DetailComponents = new DetailComponents: - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement = - div( - div( - cls := "px-4 sm:px-0", - h3( - cls := "text-base font-semibold leading-7 text-gray-900", - title - ), - subtitle.map(st => - p( - cls := "mt-1 max-w-2xl text-sm leading-6 text-gray-500", - st - ) - ) - // TODO: actions - ), - div( - cls := "mt-6 border-t border-gray-100", - dl( - cls := "divide-y divide-gray-100", - fields - ) - ) - ) - - override def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0", - dt( - cls := "text-sm font-medium leading-6 text-gray-900", - title - ), - dd( - cls := "mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0", - content - ) - ) - - override def files(fs: List[FileRef]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-100 rounded-md border border-gray-200", - fs.map(file) - ) - - override def file(f: FileRef): HtmlElement = - li( - cls := "flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6", - div( - cls := "flex w-0 flex-1 items-center", - icons.`paper-clip-solid`(), - div( - cls := "ml-4 flex min-w-0 flex-1 gap-2", - span( - cls := "truncate font-medium", - f.name - ), - f.size.map(size => span(cls := "flex-shrink-0 text-gray-400", size)) - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - "Uložit" - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala new file mode 100644 index 0000000..f6c583f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait FormComponents(using ctx: ComponentContext) extends LocalDateSelectModule: + def searchIcon: SvgElement + + def renderLocalDateSelect( + id: String, + labelText: Option[String], + placeholderText: Option[String], + mods: LocalDateSelect => Modifier[HtmlElement] + ): HtmlElement = + div( + labelText, + input( + idAttr(id), + nameAttr(id), + autoComplete("date"), + tpe("date"), + cls( + "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ), + placeholderText.map(placeholder(_)), + mods(localDateSelect) + ) + ) + + def searchField( + id: String, + placeholderText: Option[String] = None + )( + mods: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls := "flex-1 min-w-0", + label(ctx.messages("forms.search.label"), forId(id), cls("sr-only")), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + searchIcon + ), + input( + tpe := "search", + nameAttr := "search", + idAttr := id, + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholderText + .orElse( + ctx.messages + .opt( + s"forms.search.${id}.placeholder", + s"form.search.placeholder" + ) + ) + .map(placeholder(_)), + mods + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala deleted file mode 100644 index 7714490..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala +++ /dev/null @@ -1,82 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -/** Form components - * - * A form is a collection of sections, each of which contains a collection of - * fields. - * - * Form -> * Section -> * Field - * -> * Action - * - * Each Field has an `id`, `label` and `input` element. The `id` is used to - * link the label to the input. - * - * The `input` element can be a simple text input, or a more complex component - * such as a date picker. The caller is expected to make the input work. - */ -trait FormComponentsModule extends LocalDateSelectModule: - def forms: FormComponents - - trait FormComponents: - - /** Layout the sections and actions of a form */ - def form(mods: Modifier[HtmlElement]*)(sections: Modifier[HtmlElement]*)( - actions: Modifier[HtmlElement]* - ): HtmlElement - - /** Section layout, with a title, optional subtitle and any content. */ - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label and any content */ - def field(label: Modifier[HtmlElement])( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label, input and optional help text */ - def field( - id: String, - label: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement - - def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout an inline form element */ - def inlineForm(mods: Modifier[HtmlElement]*): HtmlElement - - /** An input field with a search icon */ - def searchField(id: String, placeholderText: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement - - /** LocalDate input */ - def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala deleted file mode 100644 index 8acbe7f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala +++ /dev/null @@ -1,28 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.Tag - -trait GenericComponentsModule: - - def generic: GenericComponents - - trait GenericComponents: - def tag(name: String, color: ColorKind): HtmlElement - def tag(t: Tag): HtmlElement = tag(t.value, t.color) - -trait DefaultGenericComponentsModule(using ComponentContext) - extends GenericComponentsModule: - override val generic: GenericComponents = new GenericComponents: - override def tag(name: String, color: ColorKind): HtmlElement = - p( - cls( - "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" - ), - color(800).text, - color(100).bg, - name - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala deleted file mode 100644 index 8cfc1d9..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala +++ /dev/null @@ -1,123 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.CustomAttrs - -trait IconsModule: - def icons: Icons - - trait Icons: - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement - def close(mods: Modifier[SvgElement]*): SvgElement - def `search-solid`(mods: Modifier[SvgElement]*): SvgElement - def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement - def `document-chart-bar-outline`(mods: Modifier[SvgElement]*): SvgElement - def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement - -trait DefaultIconsModule(using ComponentContext) extends IconsModule: - override val icons: Icons = new Icons: - import svg.* - - object aria: - val hidden = CustomAttrs.svg.ariaHidden - - private def withDefault( - mods: Seq[Modifier[SvgElement]], - default: Modifier[SvgElement] - ): Modifier[SvgElement] = - if mods.isEmpty then default else mods - - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-8 w-8"), - 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" - ) - ) - ) - - override def close(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-2 w-2"), - stroke := "currentColor", - fill := "none", - viewBox := "0 0 8 8", - path( - strokeLineCap := "round", - strokeWidth := "1.5", - d := "M1 1l6 6m0-6L1 7" - ) - ) - - override def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 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 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") - ) - ) - - override def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 text-gray-400")), - 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") - ) - ) - - override def `document-chart-bar-outline`( - mods: Modifier[SvgElement]* - ): SvgElement = - svg( - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - strokeWidth := "1.5", - stroke := "currentColor", - withDefault(mods, cls := "h-6 w-6"), - path( - strokeLineCap := "round", - strokeLineJoin := "round", - d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" - ) - ) - - override def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-5 w-5"), - cls := "flex-shrink-0 text-gray-400", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", - clipRule := "evenodd" - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala deleted file mode 100644 index 95ab654..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait LayoutModule: - def layout: LayoutComponents - - trait LayoutComponents: - def card(content: Modifier[HtmlElement]*): HtmlElement - -trait DefaultLayoutModule(using ctx: ComponentContext) extends LayoutModule: - override val layout: LayoutComponents = new LayoutComponents: - override def card(content: Modifier[HtmlElement]*): HtmlElement = - div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala deleted file mode 100644 index 5946974..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom.html.Paragraph -import com.raquo.laminar.nodes.TextNode - -trait ListComponentsModule: - - def list: ListComponents - - trait ListComponents(using ComponentContext): - def label(text: String, color: ColorKind): HtmlElement - def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI - def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] - def listSection(header: String, list: HtmlElement): Div - def navigation(sections: Modifier[HtmlElement]): HtmlElement - -trait DefaultListComponentsModule(using ComponentContext) - extends ListComponentsModule: - self: GenericComponentsModule => - - override val list: ListComponents = new ListComponents: - override def label( - text: String, - color: ColorKind - ): HtmlElement = generic.tag(text, color) - - override def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI = - li( - cls("group"), - div( - cls( - "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" - ), - avatar.map(a => - div( - cls("flex-shrink-0"), - div( - cls( - "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" - ), - a - ) - ) - ), - div( - cls("flex-1 min-w-0"), - p( - cls("text-sm font-medium text-gray-900"), - title, - span(cls("float-right"), right) - ), - subtitle.map(st => - p( - cls("text-sm text-gray-500 truncate"), - st - ) - ) - ) - ) - ) - - override def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = - ul( - cls("relative z-0 divide-y divide-gray-200"), - role("list"), - children - ) - - override def listSection( - header: String, - list: HtmlElement - ): Div = - div( - cls("relative"), - div( - cls( - "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" - ), - header - ), - list - ) - - override def navigation(sections: Modifier[HtmlElement]): HtmlElement = - navTag( - cls("flex-1 min-h-0 overflow-y-auto"), - sections - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala deleted file mode 100644 index 5b5490f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ModalComponentsModule: - - def modal: ModalComponents - - trait ModalComponents: - def modalDialog( - content: HtmlElement, - onClose: Modifier[HtmlElement] - ): HtmlElement - -trait DefaultModalComponentsModule(using ctx: ComponentContext) - extends ModalComponentsModule: - - override val modal: ModalComponents = new ModalComponents: - - override def modalDialog( - content: HtmlElement, - closeMod: Modifier[HtmlElement] - ): 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( - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")), - closeMod - ) - ) - - div( - cls("fixed inset-0 z-20 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - 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-7xl sm:align-middle" - ), - content - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala deleted file mode 100644 index b713d8a..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala +++ /dev/null @@ -1,71 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait PageComponentsModule: - - val page: PageComponents - - trait PageComponents: - def container( - children: Modifier[HtmlElement]* - ): HtmlElement - - def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement - - def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement] = emptyMod, - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement - - /** Visage for clickable text, like links - */ - def clickable: Modifier[HtmlElement] - -trait DefaultPageComponentsModule(using ComponentContext) - extends PageComponentsModule: - - override val page: PageComponents = new PageComponents: - override def container( - children: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), - children - ) - - override def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement = - div( - cls("p-8 bg-gray-100 h-full"), - header, - children - ) - - override def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement = - div( - cls("pb-5 border-b border-gray-200"), - div(cls("float-right"), right), - h1( - cls("text-2xl leading-6 font-medium text-gray-900"), - title - ), - subtitle.map( - p( - cls("text-sm font-medium text-gray-500"), - _ - ) - ) - ) - - override def clickable: Modifier[HtmlElement] = - cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala deleted file mode 100644 index f70ee87..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala +++ /dev/null @@ -1,108 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} - -trait TableComponentsModule: - def tables: TableComponents - - trait TableComponents: - def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )(table: Modifier[HtmlElement]*): HtmlElement - - def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement - - def headerRow(mods: Modifier[HtmlElement]*)( - cells: HtmlElement* - ): HtmlElement - - def dataRow(mods: Modifier[HtmlElement]*)(cells: HtmlElement*): HtmlElement - - def headerCell(content: Modifier[HtmlElement]): HtmlElement - - def dataCell(content: Modifier[HtmlElement]): HtmlElement - -trait DefaultTableComponentsModule extends TableComponentsModule: - - override lazy val tables: TableComponents = new TableComponents: - - override def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )( - table: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("px-4 sm:px-6 lg:px-8"), - div( - cls("sm:flex sm:items-center"), - div( - cls("sm:flex-auto"), - h1(cls("text-base font-semibold leading-6 text-gray-900"), title), - subtitle.map(st => p(cls("mt-2 text-sm text-gray-700"), st)) - ), - div(cls("mt-4 sm:ml-16 sm:mt-0 sm:flex-none"), actions) - ), - div( - cls("mt-8 flow-root"), - div( - cls("-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"), - div( - cls("inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"), - table - ) - ) - ) - ) - - override def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement = - table( - cls("min-w-full divide-y divide-gray-300"), - thead(header), - tbody(cls("divide-y divide-gray-200"), body) - ) - - override def headerRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-3.5 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-3.5 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-3.5 px-3")) - ) - ) - - override def dataRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - mods, - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-4 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-4 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-4 px-3")) - ) - ) - - override def headerCell(content: Modifier[HtmlElement]): HtmlElement = - th( - cls("text-left text-sm font-semibold text-gray-900"), - content - ) - - override def dataCell(content: Modifier[HtmlElement]): HtmlElement = - td( - cls("whitespace-nowrap text-sm text-gray-500"), - content - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala index b937d45..12d01ae 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala @@ -3,7 +3,7 @@ import com.raquo.laminar.api.L.{*, given} import works.iterative.ui.components.laminar.HtmlTabular import works.iterative.ui.model.tables.Tabular -import works.iterative.ui.components.laminar.TableComponentsModule +import works.iterative.ui.components.laminar.tailwind.ui.TableComponentsModule import works.iterative.core.UserMessage import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given import works.iterative.ui.components.tailwind.ComponentContext diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala new file mode 100644 index 0000000..87f3e8d --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala @@ -0,0 +1,54 @@ +package works.iterative.ui.components.laminar.tailwind.color + +import com.raquo.laminar.api.L.{*, given} + +/** Complete color definition that can be rendered to CSS. + * + * Includes the area, kind and weight of the color. + */ +case class Color(area: ColorArea, color: ColorDef): + def toCSS: String = s"${area.name}-${color.toCSS}" + +object Color: + import ColorDef.given + + given colorToCSS: Conversion[Color, HtmlMod] with + def apply(c: Color) = cls(c.toCSS) + + given colorToSVGCSS: Conversion[Color, SvgMod] with + def apply(c: Color) = svg.cls(c.toCSS) + + given colorSignalToCSS: Conversion[Signal[Color], HtmlMod] with + def apply(c: Signal[Color]) = cls <-- c.map(_.toCSS) + + given colorSignalToSVGCSS: Conversion[Signal[Color], SvgMod] with + def apply(c: Signal[Color]) = svg.cls <-- c.map(_.toCSS) + + def current = ColorDef(ColorKind.current) + def inherit = ColorDef(ColorKind.inherit) + def transp = ColorDef(ColorKind.transp) + def auto = ColorDef(ColorKind.auto) + def black = ColorDef(ColorKind.black) + def white = ColorDef(ColorKind.white) + def slate(weight: ColorWeight) = ColorDef(ColorKind.slate, weight) + def gray(weight: ColorWeight) = ColorDef(ColorKind.gray, weight) + def zinc(weight: ColorWeight) = ColorDef(ColorKind.zinc, weight) + def neutral(weight: ColorWeight) = ColorDef(ColorKind.neutral, weight) + def stone(weight: ColorWeight) = ColorDef(ColorKind.stone, weight) + def red(weight: ColorWeight) = ColorDef(ColorKind.red, weight) + def orange(weight: ColorWeight) = ColorDef(ColorKind.orange, weight) + def amber(weight: ColorWeight) = ColorDef(ColorKind.amber, weight) + def yellow(weight: ColorWeight) = ColorDef(ColorKind.yellow, weight) + def lime(weight: ColorWeight) = ColorDef(ColorKind.lime, weight) + def green(weight: ColorWeight) = ColorDef(ColorKind.green, weight) + def emerald(weight: ColorWeight) = ColorDef(ColorKind.emerald, weight) + def teal(weight: ColorWeight) = ColorDef(ColorKind.teal, weight) + def cyan(weight: ColorWeight) = ColorDef(ColorKind.cyan, weight) + def sky(weight: ColorWeight) = ColorDef(ColorKind.sky, weight) + def blue(weight: ColorWeight) = ColorDef(ColorKind.blue, weight) + def indigo(weight: ColorWeight) = ColorDef(ColorKind.indigo, weight) + def violet(weight: ColorWeight) = ColorDef(ColorKind.violet, weight) + def purple(weight: ColorWeight) = ColorDef(ColorKind.purple, weight) + def fuchsia(weight: ColorWeight) = ColorDef(ColorKind.fuchsia, weight) + def pink(weight: ColorWeight) = ColorDef(ColorKind.pink, weight) + def rose(weight: ColorWeight) = ColorDef(ColorKind.rose, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala new file mode 100644 index 0000000..4130840 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala @@ -0,0 +1,16 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines the area the color should apply to, eg. background, text, border, + * etc. + */ +enum ColorArea(val name: String): + case bg extends ColorArea("bg") + case text extends ColorArea("text") + case decoration extends ColorArea("decoration") + case border extends ColorArea("border") + case outline extends ColorArea("outline") + case divide extends ColorArea("divide") + case ring extends ColorArea("ring") + case ringOffset extends ColorArea("ring-offset") + case shadow extends ColorArea("shadow") + case accent extends ColorArea("accent") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala new file mode 100644 index 0000000..fab9404 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala @@ -0,0 +1,43 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** A combination of ColorKind and ColorWeight, if applicable. + * + * By applying area we get the full Color definition. + */ +sealed trait ColorDef: + def toCSS: String + + def bg = Color(ColorArea.bg, this) + def text = Color(ColorArea.text, this) + def decoration = Color(ColorArea.decoration, this) + def border = Color(ColorArea.border, this) + def outline = Color(ColorArea.outline, this) + def divide = Color(ColorArea.divide, this) + def ring = Color(ColorArea.ring, this) + def ringOffset = Color(ColorArea.ringOffset, this) + def shadow = Color(ColorArea.shadow, this) + def accent = Color(ColorArea.accent, this) + +// TODO: create a macro that will output the tailwind class as string directly during compilation, where possible. +object ColorDef: + case class WeightedColorDef( + kind: ColorKind, + weight: ColorWeight + ) extends ColorDef: + override def toCSS: String = s"${kind.name}-${weight.value}" + + case class UnweightedColorDef( + kind: ColorKind + ) extends ColorDef: + override def toCSS: String = kind.name + + // TODO: check that the kind is valid unweighted kind + // that means current, inherit, auto, transparent, black, white + // tried using implicit evidence, but the type inference for enumerations + // tends to generalize to the enum, instead of the real type + def apply[T <: ColorKind](kind: T)(using + ev: T <:< ColorKind.Unweighted + ): ColorDef = + UnweightedColorDef(kind) + def apply(kind: ColorKind, weight: ColorWeight): ColorDef = + WeightedColorDef(kind, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala new file mode 100644 index 0000000..865166b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala @@ -0,0 +1,47 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines what color should be used, without specifying the area or weight. + */ +sealed abstract class ColorKind private (val name: String): + def apply(weight: ColorWeight): ColorDef = + ColorDef.WeightedColorDef(this, weight) + +object ColorKind: + trait Unweighted: + self: ColorKind => + override def apply(weight: ColorWeight): ColorDef = + ColorDef.UnweightedColorDef(self) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case object current extends ColorKind("current") with Unweighted + case object inherit extends ColorKind("inherit") with Unweighted + // Not present in for all methods + case object transp extends ColorKind("transparent") with Unweighted + // Seen in accent, not preset otherwise + case object auto extends ColorKind("auto") with Unweighted + // Black and white do not have weight + case object black extends ColorKind("black") with Unweighted + case object white extends ColorKind("white") with Unweighted + case object slate extends ColorKind("slate") + case object gray extends ColorKind("gray") + case object zinc extends ColorKind("zinc") + case object neutral extends ColorKind("neutral") + case object stone extends ColorKind("stone") + case object red extends ColorKind("red") + case object orange extends ColorKind("orange") + case object amber extends ColorKind("amber") + case object yellow extends ColorKind("yellow") + case object lime extends ColorKind("lime") + case object green extends ColorKind("green") + case object emerald extends ColorKind("emerald") + case object teal extends ColorKind("teal") + case object cyan extends ColorKind("cyan") + case object sky extends ColorKind("sky") + case object blue extends ColorKind("blue") + case object indigo extends ColorKind("indigo") + case object violet extends ColorKind("violet") + case object purple extends ColorKind("purple") + case object fuchsia extends ColorKind("fuchsia") + case object pink extends ColorKind("pink") + case object rose extends ColorKind("rose") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala new file mode 100644 index 0000000..6f1a4cc --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala @@ -0,0 +1,33 @@ +package works.iterative.ui.components.laminar.tailwind.color + +opaque type ColorWeight = String + +extension (c: ColorWeight) def value: String = c + +/** Defines weight of a color, eg. 50, 100, 200, etc. + * + * Tailwind-like. + */ +object ColorWeight: + inline given int50: Conversion[50, ColorWeight] with + inline def apply(i: 50) = "50" + inline given int100: Conversion[100, ColorWeight] with + inline def apply(i: 100) = "100" + inline given int200: Conversion[200, ColorWeight] with + inline def apply(i: 200) = "200" + inline given int300: Conversion[300, ColorWeight] with + inline def apply(i: 300) = "300" + inline given int400: Conversion[400, ColorWeight] with + inline def apply(i: 400) = "400" + inline given int500: Conversion[500, ColorWeight] with + inline def apply(i: 500) = "500" + inline given int600: Conversion[600, ColorWeight] with + inline def apply(i: 600) = "600" + inline given int700: Conversion[700, ColorWeight] with + inline def apply(i: 700) = "700" + inline given int800: Conversion[800, ColorWeight] with + inline def apply(i: 800) = "800" + inline given int900: Conversion[900, ColorWeight] with + inline def apply(i: 900) = "900" + inline given int950: Conversion[950, ColorWeight] with + inline def apply(i: 950) = "950" diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala new file mode 100644 index 0000000..21e21f0 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala @@ -0,0 +1,12 @@ +package works.iterative.ui.components.laminar.tailwind + +/** We need a generic Color model that can be used both on server and on client. + * + * We have adopted the Tailwind model for now. There is nothing inherently + * Tailwind-specific in the implementation, but all the values are taken from + * their palette and the area model is very HTML biased. + * + * Still, I think that it is a good starting point for exploration and will + * satisfy our current needs. + */ +package object color {} diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" new file mode 100644 index 0000000..5c4e218 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" @@ -0,0 +1,21 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.model.color.ColorKind +import works.iterative.ui.components.tailwind.ComponentContext +import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given +import works.iterative.ui.model.Tag + +trait TailwindUIBadgesComponentsModule(using ComponentContext): + + object badges: + override def tag(name: String, color: ColorKind): HtmlElement = + p( + cls( + "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" + ), + color(800).text, + color(100).bg, + name + ) diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" new file mode 100644 index 0000000..0c9ca25 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" @@ -0,0 +1,24 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} + +trait TailwindUIHeadingsComponentsModule: + val headings: HeadingsComponents = new HeadingsComponents + + class HeadingsComponents: + def section(title: Node, actions: HtmlElement*): HtmlElement = + div( + cls("border-b border-gray-200 pb-5"), + div( + cls("sm:flex sm:items-center sm:justify-between"), + h3(cls("text-base font-semibold leading-6 text-gray-900"), title), + div( + cls("mt-3 flex sm:ml-4 sm:mt-0"), + actions match + case Nil => emptyMod + case first :: rest => + first :: rest.map(_.amend(cls("ml-3"))) + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala new file mode 100644 index 0000000..16ca01b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala @@ -0,0 +1,19 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.laminar.tailwind.color.ColorKind + +trait BadgeComponentsModule: + + object badges: + def pill(name: String, color: ColorKind): HtmlElement = + p( + cls( + "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" + ), + color(800).text, + color(100).bg, + name + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala new file mode 100644 index 0000000..ebb2968 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} + +trait ButtonComponentsModule: + + object buttons: + + private inline def srHelp(text: String): Modifier[HtmlElement] = + span(cls := "sr-only", text) + + def primaryButton( + id: String, + text: Modifier[HtmlElement], + icon: Option[SvgElement] = None, + buttonType: String = "submit" + )(mods: Modifier[HtmlElement]*): HtmlElement = + button( + tpe(buttonType), + cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + icon, + text, + mods + ) + + def secondaryButton( + id: String, + text: Modifier[HtmlElement], + icon: Option[SvgElement] = None, + buttonType: String = "button" + )(mods: Modifier[HtmlElement]*): HtmlElement = + button( + tpe(buttonType), + cls( + "ml-2 bg-white inline-flex justify-center 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" + ), + icon, + text, + mods + ) + + def inlineButton(id: String, icon: SvgElement)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + button( + tpe := "button", + cls := "ml-1 inline-flex h-4 w-4 flex-shrink-0 rounded-full p-1 text-gray-400 hover:bg-gray-200 hover:text-gray-500", + srHelp(id), + icon + ) + + def iconButton(id: String, icon: SvgElement, srText: Option[String] = None)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + button( + tpe := "button", + cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", + icon, + srText.map(srHelp(_)) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala new file mode 100644 index 0000000..eb6dd5f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala @@ -0,0 +1,101 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import org.scalajs.dom.html +import com.raquo.laminar.modifiers.KeyUpdater + +trait FormComponentsModule: + self: IconsModule => + + object forms: + + def section( + title: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] + )(content: Modifier[HtmlElement]*): HtmlElement = + div( + cls("space-y-6 sm:space-y-5"), + div( + h3(cls("text-lg leading-6 font-medium text-gray-900"), title), + subtitle.map(st => p(cls("mt-1 max-w-2xl text-sm text-gray-500"), st)) + ), + div(cls("mt-6 sm:mt-5 space-y-6 sm:space-y-5"), content) + ) + + def label(labelText: String, forId: Option[String] = None)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + L.label( + cls("block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"), + forId.map(id => L.forId(id)), + labelText, + mods + ) + + def field( + label: Modifier[HtmlElement] + )(content: Modifier[HtmlElement]*): 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, + div(cls("mt-1 sm:mt-0 sm:col-span-2"), content) + ) + + def field( + id: String, + labelText: String, + input: HtmlElement, + help: Option[String] + ): HtmlElement = + field( + label(labelText, Some(id))() + )( + input.amend(idAttr(id)), + help.map(h => p(cls("mt-2 text-sm text-gray-500"), h)) + ) + + def form(mods: Modifier[HtmlElement]*)( + sections: Modifier[HtmlElement]* + )(actions: Modifier[HtmlElement]*): HtmlElement = + L.form( + cls("space-y-8 divide-y divide-gray-200"), + mods, + sections, + div( + cls("pt-5"), + div(cls("flex justify-end"), actions) + ) + ) + + def inlineForm( + mods: Modifier[HtmlElement]* + ): HtmlElement = + L.form(cls("flex space-x-4"), mods) + + def inputField( + id: String, + labelText: String, + placeholderText: Option[String] = None, + inputType: String = "text", + helpText: Option[String] = None + ): HtmlElement = + field( + id, + labelText, + input( + tpe(inputType), + cls( + "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:max-w-xs sm:text-sm sm:leading-6" + ), + placeholderText.map(placeholder(_)) + ), + helpText + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala index a2c637c..bfa063a 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala @@ -1,13 +1,11 @@ package works.iterative.ui.components.laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorDef import com.raquo.laminar.tags.HtmlTag case class Bin[Source, +Value]( label: String, description: Option[String | HtmlElement], - color: ColorDef, valueOf: Source => Value ): def map[NewValue]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala deleted file mode 100644 index 3aac0bf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala +++ /dev/null @@ -1,34 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ButtonComponentsModule: - def buttons: ButtonComponents - - trait ButtonComponents: - def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala deleted file mode 100644 index 646e73d..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala +++ /dev/null @@ -1,191 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.color.ColorDef -import com.raquo.laminar.tags.HtmlTag -import org.scalajs.dom.html.UList -import com.raquo.laminar.nodes.ReactiveHtmlElement -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind - -trait DashboardComponentsModule: - def dashboard: DashboardComponents - - // Only methods that create HtmlElements - trait DashboardComponents(using ComponentContext): - def section(label: String, children: Modifier[HtmlElement]): Div - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI - def cardInitials( - initials: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div - def count(label: String, value: Int, color: ColorDef): Span - def counts(children: HtmlElement*): Span - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - -trait DefaultDashboardComponentsModule(using ComponentContext) - extends DashboardComponentsModule: - override val dashboard: DashboardComponents = new DashboardComponents: - def section(label: String, children: Modifier[HtmlElement]): Div = - div( - cls("mt-3"), - h2( - cls("text-gray-500 text-xs font-medium uppercase tracking-wide"), - label - ), - children - ) - - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] = - ul( - cls( - "mt-3 grid gap-5 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" - ), - children - ) - - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI = - li( - div( - cls("col-span-1 flex shadow-sm rounded-md"), - initials, - div( - cls( - "flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate" - ), - div( - cls("flex-1 px-4 py-2 text-sm truncate"), - div( - cls("text-gray-900 font-medium hover:text-gray-600 truncate"), - label - ), - p(cls("text-gray-500"), children) - ) - ) - ) - ) - - def cardInitials( - id: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div = - div( - cls( - "flex-shrink-0 flex flex-col items-center justify-center w-16 text-white text-sm font-medium rounded-l-md" - ), - color.map(_(600).bg), - div(id), - children - ) - - def count(label: String, value: Int, color: ColorDef): Span = - span(color.text, title(label), s"${value}") - - def counts(children: HtmlElement*): Span = - span(interleave(children, span(" / "))) - - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div = - div( - cls("flex flex-col mt-2"), - div( - cls( - "align-middle min-w-full shadow sm:rounded-lg" - ), - L.table( - cls("min-w-full border-separate border-spacing-0"), - styleAttr("border-spacing: 0"), // Tailwind somehow doesn't work - thead( - tr( - cls("border-t border-gray-200"), - headers.map(h => - th( - cls( - "sticky top-0 z-10 px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - h - ) - ) - ) - ), - tbody(cls("bg-white divide-y divide-gray-100"), sections) - ) - ) - ) - - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - td( - cls("bg-gray-50 border-gray-200 border-t border-b"), - cls( - "sticky top-14 z-10 bg-gray-50 px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - colSpan(cs), - label - ), - mods - ) - - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - mods, - td( - cls( - "px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900 truncate" - ), - label - ), - children.map(e => - td( - cls( - "whitespace-nowrap px-2 py-2 text-sm text-gray-500" - ), - e - ) - ) - ) - - private def interleave( - a: Seq[HtmlElement], - separator: => HtmlElement - ): Seq[HtmlElement] = - if a.size < 2 then a - else a.init.flatMap(n => Seq(n, separator)) :+ a.last diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala deleted file mode 100644 index a45beaf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.FileRef - -trait DetailComponentsModule: - - def details: DetailComponents - - trait DetailComponents: - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement - - def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement - - def files(fs: List[FileRef]): HtmlElement - - def file(f: FileRef): HtmlElement - -trait DefaultDetailComponentsModule(using ctx: ComponentContext) - extends DetailComponentsModule: - self: IconsModule => - - override val details: DetailComponents = new DetailComponents: - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement = - div( - div( - cls := "px-4 sm:px-0", - h3( - cls := "text-base font-semibold leading-7 text-gray-900", - title - ), - subtitle.map(st => - p( - cls := "mt-1 max-w-2xl text-sm leading-6 text-gray-500", - st - ) - ) - // TODO: actions - ), - div( - cls := "mt-6 border-t border-gray-100", - dl( - cls := "divide-y divide-gray-100", - fields - ) - ) - ) - - override def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0", - dt( - cls := "text-sm font-medium leading-6 text-gray-900", - title - ), - dd( - cls := "mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0", - content - ) - ) - - override def files(fs: List[FileRef]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-100 rounded-md border border-gray-200", - fs.map(file) - ) - - override def file(f: FileRef): HtmlElement = - li( - cls := "flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6", - div( - cls := "flex w-0 flex-1 items-center", - icons.`paper-clip-solid`(), - div( - cls := "ml-4 flex min-w-0 flex-1 gap-2", - span( - cls := "truncate font-medium", - f.name - ), - f.size.map(size => span(cls := "flex-shrink-0 text-gray-400", size)) - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - "Uložit" - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala new file mode 100644 index 0000000..f6c583f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait FormComponents(using ctx: ComponentContext) extends LocalDateSelectModule: + def searchIcon: SvgElement + + def renderLocalDateSelect( + id: String, + labelText: Option[String], + placeholderText: Option[String], + mods: LocalDateSelect => Modifier[HtmlElement] + ): HtmlElement = + div( + labelText, + input( + idAttr(id), + nameAttr(id), + autoComplete("date"), + tpe("date"), + cls( + "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ), + placeholderText.map(placeholder(_)), + mods(localDateSelect) + ) + ) + + def searchField( + id: String, + placeholderText: Option[String] = None + )( + mods: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls := "flex-1 min-w-0", + label(ctx.messages("forms.search.label"), forId(id), cls("sr-only")), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + searchIcon + ), + input( + tpe := "search", + nameAttr := "search", + idAttr := id, + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholderText + .orElse( + ctx.messages + .opt( + s"forms.search.${id}.placeholder", + s"form.search.placeholder" + ) + ) + .map(placeholder(_)), + mods + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala deleted file mode 100644 index 7714490..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala +++ /dev/null @@ -1,82 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -/** Form components - * - * A form is a collection of sections, each of which contains a collection of - * fields. - * - * Form -> * Section -> * Field - * -> * Action - * - * Each Field has an `id`, `label` and `input` element. The `id` is used to - * link the label to the input. - * - * The `input` element can be a simple text input, or a more complex component - * such as a date picker. The caller is expected to make the input work. - */ -trait FormComponentsModule extends LocalDateSelectModule: - def forms: FormComponents - - trait FormComponents: - - /** Layout the sections and actions of a form */ - def form(mods: Modifier[HtmlElement]*)(sections: Modifier[HtmlElement]*)( - actions: Modifier[HtmlElement]* - ): HtmlElement - - /** Section layout, with a title, optional subtitle and any content. */ - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label and any content */ - def field(label: Modifier[HtmlElement])( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label, input and optional help text */ - def field( - id: String, - label: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement - - def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout an inline form element */ - def inlineForm(mods: Modifier[HtmlElement]*): HtmlElement - - /** An input field with a search icon */ - def searchField(id: String, placeholderText: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement - - /** LocalDate input */ - def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala deleted file mode 100644 index 8acbe7f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala +++ /dev/null @@ -1,28 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.Tag - -trait GenericComponentsModule: - - def generic: GenericComponents - - trait GenericComponents: - def tag(name: String, color: ColorKind): HtmlElement - def tag(t: Tag): HtmlElement = tag(t.value, t.color) - -trait DefaultGenericComponentsModule(using ComponentContext) - extends GenericComponentsModule: - override val generic: GenericComponents = new GenericComponents: - override def tag(name: String, color: ColorKind): HtmlElement = - p( - cls( - "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" - ), - color(800).text, - color(100).bg, - name - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala deleted file mode 100644 index 8cfc1d9..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala +++ /dev/null @@ -1,123 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.CustomAttrs - -trait IconsModule: - def icons: Icons - - trait Icons: - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement - def close(mods: Modifier[SvgElement]*): SvgElement - def `search-solid`(mods: Modifier[SvgElement]*): SvgElement - def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement - def `document-chart-bar-outline`(mods: Modifier[SvgElement]*): SvgElement - def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement - -trait DefaultIconsModule(using ComponentContext) extends IconsModule: - override val icons: Icons = new Icons: - import svg.* - - object aria: - val hidden = CustomAttrs.svg.ariaHidden - - private def withDefault( - mods: Seq[Modifier[SvgElement]], - default: Modifier[SvgElement] - ): Modifier[SvgElement] = - if mods.isEmpty then default else mods - - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-8 w-8"), - 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" - ) - ) - ) - - override def close(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-2 w-2"), - stroke := "currentColor", - fill := "none", - viewBox := "0 0 8 8", - path( - strokeLineCap := "round", - strokeWidth := "1.5", - d := "M1 1l6 6m0-6L1 7" - ) - ) - - override def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 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 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") - ) - ) - - override def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 text-gray-400")), - 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") - ) - ) - - override def `document-chart-bar-outline`( - mods: Modifier[SvgElement]* - ): SvgElement = - svg( - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - strokeWidth := "1.5", - stroke := "currentColor", - withDefault(mods, cls := "h-6 w-6"), - path( - strokeLineCap := "round", - strokeLineJoin := "round", - d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" - ) - ) - - override def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-5 w-5"), - cls := "flex-shrink-0 text-gray-400", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", - clipRule := "evenodd" - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala deleted file mode 100644 index 95ab654..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait LayoutModule: - def layout: LayoutComponents - - trait LayoutComponents: - def card(content: Modifier[HtmlElement]*): HtmlElement - -trait DefaultLayoutModule(using ctx: ComponentContext) extends LayoutModule: - override val layout: LayoutComponents = new LayoutComponents: - override def card(content: Modifier[HtmlElement]*): HtmlElement = - div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala deleted file mode 100644 index 5946974..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom.html.Paragraph -import com.raquo.laminar.nodes.TextNode - -trait ListComponentsModule: - - def list: ListComponents - - trait ListComponents(using ComponentContext): - def label(text: String, color: ColorKind): HtmlElement - def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI - def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] - def listSection(header: String, list: HtmlElement): Div - def navigation(sections: Modifier[HtmlElement]): HtmlElement - -trait DefaultListComponentsModule(using ComponentContext) - extends ListComponentsModule: - self: GenericComponentsModule => - - override val list: ListComponents = new ListComponents: - override def label( - text: String, - color: ColorKind - ): HtmlElement = generic.tag(text, color) - - override def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI = - li( - cls("group"), - div( - cls( - "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" - ), - avatar.map(a => - div( - cls("flex-shrink-0"), - div( - cls( - "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" - ), - a - ) - ) - ), - div( - cls("flex-1 min-w-0"), - p( - cls("text-sm font-medium text-gray-900"), - title, - span(cls("float-right"), right) - ), - subtitle.map(st => - p( - cls("text-sm text-gray-500 truncate"), - st - ) - ) - ) - ) - ) - - override def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = - ul( - cls("relative z-0 divide-y divide-gray-200"), - role("list"), - children - ) - - override def listSection( - header: String, - list: HtmlElement - ): Div = - div( - cls("relative"), - div( - cls( - "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" - ), - header - ), - list - ) - - override def navigation(sections: Modifier[HtmlElement]): HtmlElement = - navTag( - cls("flex-1 min-h-0 overflow-y-auto"), - sections - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala deleted file mode 100644 index 5b5490f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ModalComponentsModule: - - def modal: ModalComponents - - trait ModalComponents: - def modalDialog( - content: HtmlElement, - onClose: Modifier[HtmlElement] - ): HtmlElement - -trait DefaultModalComponentsModule(using ctx: ComponentContext) - extends ModalComponentsModule: - - override val modal: ModalComponents = new ModalComponents: - - override def modalDialog( - content: HtmlElement, - closeMod: Modifier[HtmlElement] - ): 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( - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")), - closeMod - ) - ) - - div( - cls("fixed inset-0 z-20 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - 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-7xl sm:align-middle" - ), - content - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala deleted file mode 100644 index b713d8a..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala +++ /dev/null @@ -1,71 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait PageComponentsModule: - - val page: PageComponents - - trait PageComponents: - def container( - children: Modifier[HtmlElement]* - ): HtmlElement - - def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement - - def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement] = emptyMod, - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement - - /** Visage for clickable text, like links - */ - def clickable: Modifier[HtmlElement] - -trait DefaultPageComponentsModule(using ComponentContext) - extends PageComponentsModule: - - override val page: PageComponents = new PageComponents: - override def container( - children: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), - children - ) - - override def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement = - div( - cls("p-8 bg-gray-100 h-full"), - header, - children - ) - - override def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement = - div( - cls("pb-5 border-b border-gray-200"), - div(cls("float-right"), right), - h1( - cls("text-2xl leading-6 font-medium text-gray-900"), - title - ), - subtitle.map( - p( - cls("text-sm font-medium text-gray-500"), - _ - ) - ) - ) - - override def clickable: Modifier[HtmlElement] = - cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala deleted file mode 100644 index f70ee87..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala +++ /dev/null @@ -1,108 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} - -trait TableComponentsModule: - def tables: TableComponents - - trait TableComponents: - def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )(table: Modifier[HtmlElement]*): HtmlElement - - def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement - - def headerRow(mods: Modifier[HtmlElement]*)( - cells: HtmlElement* - ): HtmlElement - - def dataRow(mods: Modifier[HtmlElement]*)(cells: HtmlElement*): HtmlElement - - def headerCell(content: Modifier[HtmlElement]): HtmlElement - - def dataCell(content: Modifier[HtmlElement]): HtmlElement - -trait DefaultTableComponentsModule extends TableComponentsModule: - - override lazy val tables: TableComponents = new TableComponents: - - override def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )( - table: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("px-4 sm:px-6 lg:px-8"), - div( - cls("sm:flex sm:items-center"), - div( - cls("sm:flex-auto"), - h1(cls("text-base font-semibold leading-6 text-gray-900"), title), - subtitle.map(st => p(cls("mt-2 text-sm text-gray-700"), st)) - ), - div(cls("mt-4 sm:ml-16 sm:mt-0 sm:flex-none"), actions) - ), - div( - cls("mt-8 flow-root"), - div( - cls("-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"), - div( - cls("inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"), - table - ) - ) - ) - ) - - override def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement = - table( - cls("min-w-full divide-y divide-gray-300"), - thead(header), - tbody(cls("divide-y divide-gray-200"), body) - ) - - override def headerRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-3.5 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-3.5 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-3.5 px-3")) - ) - ) - - override def dataRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - mods, - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-4 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-4 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-4 px-3")) - ) - ) - - override def headerCell(content: Modifier[HtmlElement]): HtmlElement = - th( - cls("text-left text-sm font-semibold text-gray-900"), - content - ) - - override def dataCell(content: Modifier[HtmlElement]): HtmlElement = - td( - cls("whitespace-nowrap text-sm text-gray-500"), - content - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala index b937d45..12d01ae 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala @@ -3,7 +3,7 @@ import com.raquo.laminar.api.L.{*, given} import works.iterative.ui.components.laminar.HtmlTabular import works.iterative.ui.model.tables.Tabular -import works.iterative.ui.components.laminar.TableComponentsModule +import works.iterative.ui.components.laminar.tailwind.ui.TableComponentsModule import works.iterative.core.UserMessage import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given import works.iterative.ui.components.tailwind.ComponentContext diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala new file mode 100644 index 0000000..87f3e8d --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala @@ -0,0 +1,54 @@ +package works.iterative.ui.components.laminar.tailwind.color + +import com.raquo.laminar.api.L.{*, given} + +/** Complete color definition that can be rendered to CSS. + * + * Includes the area, kind and weight of the color. + */ +case class Color(area: ColorArea, color: ColorDef): + def toCSS: String = s"${area.name}-${color.toCSS}" + +object Color: + import ColorDef.given + + given colorToCSS: Conversion[Color, HtmlMod] with + def apply(c: Color) = cls(c.toCSS) + + given colorToSVGCSS: Conversion[Color, SvgMod] with + def apply(c: Color) = svg.cls(c.toCSS) + + given colorSignalToCSS: Conversion[Signal[Color], HtmlMod] with + def apply(c: Signal[Color]) = cls <-- c.map(_.toCSS) + + given colorSignalToSVGCSS: Conversion[Signal[Color], SvgMod] with + def apply(c: Signal[Color]) = svg.cls <-- c.map(_.toCSS) + + def current = ColorDef(ColorKind.current) + def inherit = ColorDef(ColorKind.inherit) + def transp = ColorDef(ColorKind.transp) + def auto = ColorDef(ColorKind.auto) + def black = ColorDef(ColorKind.black) + def white = ColorDef(ColorKind.white) + def slate(weight: ColorWeight) = ColorDef(ColorKind.slate, weight) + def gray(weight: ColorWeight) = ColorDef(ColorKind.gray, weight) + def zinc(weight: ColorWeight) = ColorDef(ColorKind.zinc, weight) + def neutral(weight: ColorWeight) = ColorDef(ColorKind.neutral, weight) + def stone(weight: ColorWeight) = ColorDef(ColorKind.stone, weight) + def red(weight: ColorWeight) = ColorDef(ColorKind.red, weight) + def orange(weight: ColorWeight) = ColorDef(ColorKind.orange, weight) + def amber(weight: ColorWeight) = ColorDef(ColorKind.amber, weight) + def yellow(weight: ColorWeight) = ColorDef(ColorKind.yellow, weight) + def lime(weight: ColorWeight) = ColorDef(ColorKind.lime, weight) + def green(weight: ColorWeight) = ColorDef(ColorKind.green, weight) + def emerald(weight: ColorWeight) = ColorDef(ColorKind.emerald, weight) + def teal(weight: ColorWeight) = ColorDef(ColorKind.teal, weight) + def cyan(weight: ColorWeight) = ColorDef(ColorKind.cyan, weight) + def sky(weight: ColorWeight) = ColorDef(ColorKind.sky, weight) + def blue(weight: ColorWeight) = ColorDef(ColorKind.blue, weight) + def indigo(weight: ColorWeight) = ColorDef(ColorKind.indigo, weight) + def violet(weight: ColorWeight) = ColorDef(ColorKind.violet, weight) + def purple(weight: ColorWeight) = ColorDef(ColorKind.purple, weight) + def fuchsia(weight: ColorWeight) = ColorDef(ColorKind.fuchsia, weight) + def pink(weight: ColorWeight) = ColorDef(ColorKind.pink, weight) + def rose(weight: ColorWeight) = ColorDef(ColorKind.rose, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala new file mode 100644 index 0000000..4130840 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala @@ -0,0 +1,16 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines the area the color should apply to, eg. background, text, border, + * etc. + */ +enum ColorArea(val name: String): + case bg extends ColorArea("bg") + case text extends ColorArea("text") + case decoration extends ColorArea("decoration") + case border extends ColorArea("border") + case outline extends ColorArea("outline") + case divide extends ColorArea("divide") + case ring extends ColorArea("ring") + case ringOffset extends ColorArea("ring-offset") + case shadow extends ColorArea("shadow") + case accent extends ColorArea("accent") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala new file mode 100644 index 0000000..fab9404 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala @@ -0,0 +1,43 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** A combination of ColorKind and ColorWeight, if applicable. + * + * By applying area we get the full Color definition. + */ +sealed trait ColorDef: + def toCSS: String + + def bg = Color(ColorArea.bg, this) + def text = Color(ColorArea.text, this) + def decoration = Color(ColorArea.decoration, this) + def border = Color(ColorArea.border, this) + def outline = Color(ColorArea.outline, this) + def divide = Color(ColorArea.divide, this) + def ring = Color(ColorArea.ring, this) + def ringOffset = Color(ColorArea.ringOffset, this) + def shadow = Color(ColorArea.shadow, this) + def accent = Color(ColorArea.accent, this) + +// TODO: create a macro that will output the tailwind class as string directly during compilation, where possible. +object ColorDef: + case class WeightedColorDef( + kind: ColorKind, + weight: ColorWeight + ) extends ColorDef: + override def toCSS: String = s"${kind.name}-${weight.value}" + + case class UnweightedColorDef( + kind: ColorKind + ) extends ColorDef: + override def toCSS: String = kind.name + + // TODO: check that the kind is valid unweighted kind + // that means current, inherit, auto, transparent, black, white + // tried using implicit evidence, but the type inference for enumerations + // tends to generalize to the enum, instead of the real type + def apply[T <: ColorKind](kind: T)(using + ev: T <:< ColorKind.Unweighted + ): ColorDef = + UnweightedColorDef(kind) + def apply(kind: ColorKind, weight: ColorWeight): ColorDef = + WeightedColorDef(kind, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala new file mode 100644 index 0000000..865166b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala @@ -0,0 +1,47 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines what color should be used, without specifying the area or weight. + */ +sealed abstract class ColorKind private (val name: String): + def apply(weight: ColorWeight): ColorDef = + ColorDef.WeightedColorDef(this, weight) + +object ColorKind: + trait Unweighted: + self: ColorKind => + override def apply(weight: ColorWeight): ColorDef = + ColorDef.UnweightedColorDef(self) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case object current extends ColorKind("current") with Unweighted + case object inherit extends ColorKind("inherit") with Unweighted + // Not present in for all methods + case object transp extends ColorKind("transparent") with Unweighted + // Seen in accent, not preset otherwise + case object auto extends ColorKind("auto") with Unweighted + // Black and white do not have weight + case object black extends ColorKind("black") with Unweighted + case object white extends ColorKind("white") with Unweighted + case object slate extends ColorKind("slate") + case object gray extends ColorKind("gray") + case object zinc extends ColorKind("zinc") + case object neutral extends ColorKind("neutral") + case object stone extends ColorKind("stone") + case object red extends ColorKind("red") + case object orange extends ColorKind("orange") + case object amber extends ColorKind("amber") + case object yellow extends ColorKind("yellow") + case object lime extends ColorKind("lime") + case object green extends ColorKind("green") + case object emerald extends ColorKind("emerald") + case object teal extends ColorKind("teal") + case object cyan extends ColorKind("cyan") + case object sky extends ColorKind("sky") + case object blue extends ColorKind("blue") + case object indigo extends ColorKind("indigo") + case object violet extends ColorKind("violet") + case object purple extends ColorKind("purple") + case object fuchsia extends ColorKind("fuchsia") + case object pink extends ColorKind("pink") + case object rose extends ColorKind("rose") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala new file mode 100644 index 0000000..6f1a4cc --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala @@ -0,0 +1,33 @@ +package works.iterative.ui.components.laminar.tailwind.color + +opaque type ColorWeight = String + +extension (c: ColorWeight) def value: String = c + +/** Defines weight of a color, eg. 50, 100, 200, etc. + * + * Tailwind-like. + */ +object ColorWeight: + inline given int50: Conversion[50, ColorWeight] with + inline def apply(i: 50) = "50" + inline given int100: Conversion[100, ColorWeight] with + inline def apply(i: 100) = "100" + inline given int200: Conversion[200, ColorWeight] with + inline def apply(i: 200) = "200" + inline given int300: Conversion[300, ColorWeight] with + inline def apply(i: 300) = "300" + inline given int400: Conversion[400, ColorWeight] with + inline def apply(i: 400) = "400" + inline given int500: Conversion[500, ColorWeight] with + inline def apply(i: 500) = "500" + inline given int600: Conversion[600, ColorWeight] with + inline def apply(i: 600) = "600" + inline given int700: Conversion[700, ColorWeight] with + inline def apply(i: 700) = "700" + inline given int800: Conversion[800, ColorWeight] with + inline def apply(i: 800) = "800" + inline given int900: Conversion[900, ColorWeight] with + inline def apply(i: 900) = "900" + inline given int950: Conversion[950, ColorWeight] with + inline def apply(i: 950) = "950" diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala new file mode 100644 index 0000000..21e21f0 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala @@ -0,0 +1,12 @@ +package works.iterative.ui.components.laminar.tailwind + +/** We need a generic Color model that can be used both on server and on client. + * + * We have adopted the Tailwind model for now. There is nothing inherently + * Tailwind-specific in the implementation, but all the values are taken from + * their palette and the area model is very HTML biased. + * + * Still, I think that it is a good starting point for exploration and will + * satisfy our current needs. + */ +package object color {} diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" new file mode 100644 index 0000000..5c4e218 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" @@ -0,0 +1,21 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.model.color.ColorKind +import works.iterative.ui.components.tailwind.ComponentContext +import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given +import works.iterative.ui.model.Tag + +trait TailwindUIBadgesComponentsModule(using ComponentContext): + + object badges: + override def tag(name: String, color: ColorKind): HtmlElement = + p( + cls( + "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" + ), + color(800).text, + color(100).bg, + name + ) diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" new file mode 100644 index 0000000..0c9ca25 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" @@ -0,0 +1,24 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} + +trait TailwindUIHeadingsComponentsModule: + val headings: HeadingsComponents = new HeadingsComponents + + class HeadingsComponents: + def section(title: Node, actions: HtmlElement*): HtmlElement = + div( + cls("border-b border-gray-200 pb-5"), + div( + cls("sm:flex sm:items-center sm:justify-between"), + h3(cls("text-base font-semibold leading-6 text-gray-900"), title), + div( + cls("mt-3 flex sm:ml-4 sm:mt-0"), + actions match + case Nil => emptyMod + case first :: rest => + first :: rest.map(_.amend(cls("ml-3"))) + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala new file mode 100644 index 0000000..16ca01b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala @@ -0,0 +1,19 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.laminar.tailwind.color.ColorKind + +trait BadgeComponentsModule: + + object badges: + def pill(name: String, color: ColorKind): HtmlElement = + p( + cls( + "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" + ), + color(800).text, + color(100).bg, + name + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala new file mode 100644 index 0000000..ebb2968 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} + +trait ButtonComponentsModule: + + object buttons: + + private inline def srHelp(text: String): Modifier[HtmlElement] = + span(cls := "sr-only", text) + + def primaryButton( + id: String, + text: Modifier[HtmlElement], + icon: Option[SvgElement] = None, + buttonType: String = "submit" + )(mods: Modifier[HtmlElement]*): HtmlElement = + button( + tpe(buttonType), + cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + icon, + text, + mods + ) + + def secondaryButton( + id: String, + text: Modifier[HtmlElement], + icon: Option[SvgElement] = None, + buttonType: String = "button" + )(mods: Modifier[HtmlElement]*): HtmlElement = + button( + tpe(buttonType), + cls( + "ml-2 bg-white inline-flex justify-center 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" + ), + icon, + text, + mods + ) + + def inlineButton(id: String, icon: SvgElement)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + button( + tpe := "button", + cls := "ml-1 inline-flex h-4 w-4 flex-shrink-0 rounded-full p-1 text-gray-400 hover:bg-gray-200 hover:text-gray-500", + srHelp(id), + icon + ) + + def iconButton(id: String, icon: SvgElement, srText: Option[String] = None)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + button( + tpe := "button", + cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", + icon, + srText.map(srHelp(_)) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala new file mode 100644 index 0000000..eb6dd5f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala @@ -0,0 +1,101 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import org.scalajs.dom.html +import com.raquo.laminar.modifiers.KeyUpdater + +trait FormComponentsModule: + self: IconsModule => + + object forms: + + def section( + title: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] + )(content: Modifier[HtmlElement]*): HtmlElement = + div( + cls("space-y-6 sm:space-y-5"), + div( + h3(cls("text-lg leading-6 font-medium text-gray-900"), title), + subtitle.map(st => p(cls("mt-1 max-w-2xl text-sm text-gray-500"), st)) + ), + div(cls("mt-6 sm:mt-5 space-y-6 sm:space-y-5"), content) + ) + + def label(labelText: String, forId: Option[String] = None)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + L.label( + cls("block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"), + forId.map(id => L.forId(id)), + labelText, + mods + ) + + def field( + label: Modifier[HtmlElement] + )(content: Modifier[HtmlElement]*): 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, + div(cls("mt-1 sm:mt-0 sm:col-span-2"), content) + ) + + def field( + id: String, + labelText: String, + input: HtmlElement, + help: Option[String] + ): HtmlElement = + field( + label(labelText, Some(id))() + )( + input.amend(idAttr(id)), + help.map(h => p(cls("mt-2 text-sm text-gray-500"), h)) + ) + + def form(mods: Modifier[HtmlElement]*)( + sections: Modifier[HtmlElement]* + )(actions: Modifier[HtmlElement]*): HtmlElement = + L.form( + cls("space-y-8 divide-y divide-gray-200"), + mods, + sections, + div( + cls("pt-5"), + div(cls("flex justify-end"), actions) + ) + ) + + def inlineForm( + mods: Modifier[HtmlElement]* + ): HtmlElement = + L.form(cls("flex space-x-4"), mods) + + def inputField( + id: String, + labelText: String, + placeholderText: Option[String] = None, + inputType: String = "text", + helpText: Option[String] = None + ): HtmlElement = + field( + id, + labelText, + input( + tpe(inputType), + cls( + "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:max-w-xs sm:text-sm sm:leading-6" + ), + placeholderText.map(placeholder(_)) + ), + helpText + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala new file mode 100644 index 0000000..52da07e --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala @@ -0,0 +1,22 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} + +trait HeadingsComponentsModule: + object headings: + def section(title: Node, actions: HtmlElement*): HtmlElement = + div( + cls("border-b border-gray-200 pb-5"), + div( + cls("sm:flex sm:items-center sm:justify-between"), + h3(cls("text-base font-semibold leading-6 text-gray-900"), title), + div( + cls("mt-3 flex sm:ml-4 sm:mt-0"), + actions match + case Nil => emptyMod + case first :: rest => + first :: rest.map(_.amend(cls("ml-3"))) + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala index a2c637c..bfa063a 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala @@ -1,13 +1,11 @@ package works.iterative.ui.components.laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorDef import com.raquo.laminar.tags.HtmlTag case class Bin[Source, +Value]( label: String, description: Option[String | HtmlElement], - color: ColorDef, valueOf: Source => Value ): def map[NewValue]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala deleted file mode 100644 index 3aac0bf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala +++ /dev/null @@ -1,34 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ButtonComponentsModule: - def buttons: ButtonComponents - - trait ButtonComponents: - def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala deleted file mode 100644 index 646e73d..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala +++ /dev/null @@ -1,191 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.color.ColorDef -import com.raquo.laminar.tags.HtmlTag -import org.scalajs.dom.html.UList -import com.raquo.laminar.nodes.ReactiveHtmlElement -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind - -trait DashboardComponentsModule: - def dashboard: DashboardComponents - - // Only methods that create HtmlElements - trait DashboardComponents(using ComponentContext): - def section(label: String, children: Modifier[HtmlElement]): Div - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI - def cardInitials( - initials: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div - def count(label: String, value: Int, color: ColorDef): Span - def counts(children: HtmlElement*): Span - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - -trait DefaultDashboardComponentsModule(using ComponentContext) - extends DashboardComponentsModule: - override val dashboard: DashboardComponents = new DashboardComponents: - def section(label: String, children: Modifier[HtmlElement]): Div = - div( - cls("mt-3"), - h2( - cls("text-gray-500 text-xs font-medium uppercase tracking-wide"), - label - ), - children - ) - - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] = - ul( - cls( - "mt-3 grid gap-5 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" - ), - children - ) - - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI = - li( - div( - cls("col-span-1 flex shadow-sm rounded-md"), - initials, - div( - cls( - "flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate" - ), - div( - cls("flex-1 px-4 py-2 text-sm truncate"), - div( - cls("text-gray-900 font-medium hover:text-gray-600 truncate"), - label - ), - p(cls("text-gray-500"), children) - ) - ) - ) - ) - - def cardInitials( - id: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div = - div( - cls( - "flex-shrink-0 flex flex-col items-center justify-center w-16 text-white text-sm font-medium rounded-l-md" - ), - color.map(_(600).bg), - div(id), - children - ) - - def count(label: String, value: Int, color: ColorDef): Span = - span(color.text, title(label), s"${value}") - - def counts(children: HtmlElement*): Span = - span(interleave(children, span(" / "))) - - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div = - div( - cls("flex flex-col mt-2"), - div( - cls( - "align-middle min-w-full shadow sm:rounded-lg" - ), - L.table( - cls("min-w-full border-separate border-spacing-0"), - styleAttr("border-spacing: 0"), // Tailwind somehow doesn't work - thead( - tr( - cls("border-t border-gray-200"), - headers.map(h => - th( - cls( - "sticky top-0 z-10 px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - h - ) - ) - ) - ), - tbody(cls("bg-white divide-y divide-gray-100"), sections) - ) - ) - ) - - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - td( - cls("bg-gray-50 border-gray-200 border-t border-b"), - cls( - "sticky top-14 z-10 bg-gray-50 px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - colSpan(cs), - label - ), - mods - ) - - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - mods, - td( - cls( - "px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900 truncate" - ), - label - ), - children.map(e => - td( - cls( - "whitespace-nowrap px-2 py-2 text-sm text-gray-500" - ), - e - ) - ) - ) - - private def interleave( - a: Seq[HtmlElement], - separator: => HtmlElement - ): Seq[HtmlElement] = - if a.size < 2 then a - else a.init.flatMap(n => Seq(n, separator)) :+ a.last diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala deleted file mode 100644 index a45beaf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.FileRef - -trait DetailComponentsModule: - - def details: DetailComponents - - trait DetailComponents: - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement - - def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement - - def files(fs: List[FileRef]): HtmlElement - - def file(f: FileRef): HtmlElement - -trait DefaultDetailComponentsModule(using ctx: ComponentContext) - extends DetailComponentsModule: - self: IconsModule => - - override val details: DetailComponents = new DetailComponents: - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement = - div( - div( - cls := "px-4 sm:px-0", - h3( - cls := "text-base font-semibold leading-7 text-gray-900", - title - ), - subtitle.map(st => - p( - cls := "mt-1 max-w-2xl text-sm leading-6 text-gray-500", - st - ) - ) - // TODO: actions - ), - div( - cls := "mt-6 border-t border-gray-100", - dl( - cls := "divide-y divide-gray-100", - fields - ) - ) - ) - - override def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0", - dt( - cls := "text-sm font-medium leading-6 text-gray-900", - title - ), - dd( - cls := "mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0", - content - ) - ) - - override def files(fs: List[FileRef]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-100 rounded-md border border-gray-200", - fs.map(file) - ) - - override def file(f: FileRef): HtmlElement = - li( - cls := "flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6", - div( - cls := "flex w-0 flex-1 items-center", - icons.`paper-clip-solid`(), - div( - cls := "ml-4 flex min-w-0 flex-1 gap-2", - span( - cls := "truncate font-medium", - f.name - ), - f.size.map(size => span(cls := "flex-shrink-0 text-gray-400", size)) - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - "Uložit" - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala new file mode 100644 index 0000000..f6c583f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait FormComponents(using ctx: ComponentContext) extends LocalDateSelectModule: + def searchIcon: SvgElement + + def renderLocalDateSelect( + id: String, + labelText: Option[String], + placeholderText: Option[String], + mods: LocalDateSelect => Modifier[HtmlElement] + ): HtmlElement = + div( + labelText, + input( + idAttr(id), + nameAttr(id), + autoComplete("date"), + tpe("date"), + cls( + "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ), + placeholderText.map(placeholder(_)), + mods(localDateSelect) + ) + ) + + def searchField( + id: String, + placeholderText: Option[String] = None + )( + mods: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls := "flex-1 min-w-0", + label(ctx.messages("forms.search.label"), forId(id), cls("sr-only")), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + searchIcon + ), + input( + tpe := "search", + nameAttr := "search", + idAttr := id, + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholderText + .orElse( + ctx.messages + .opt( + s"forms.search.${id}.placeholder", + s"form.search.placeholder" + ) + ) + .map(placeholder(_)), + mods + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala deleted file mode 100644 index 7714490..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala +++ /dev/null @@ -1,82 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -/** Form components - * - * A form is a collection of sections, each of which contains a collection of - * fields. - * - * Form -> * Section -> * Field - * -> * Action - * - * Each Field has an `id`, `label` and `input` element. The `id` is used to - * link the label to the input. - * - * The `input` element can be a simple text input, or a more complex component - * such as a date picker. The caller is expected to make the input work. - */ -trait FormComponentsModule extends LocalDateSelectModule: - def forms: FormComponents - - trait FormComponents: - - /** Layout the sections and actions of a form */ - def form(mods: Modifier[HtmlElement]*)(sections: Modifier[HtmlElement]*)( - actions: Modifier[HtmlElement]* - ): HtmlElement - - /** Section layout, with a title, optional subtitle and any content. */ - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label and any content */ - def field(label: Modifier[HtmlElement])( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label, input and optional help text */ - def field( - id: String, - label: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement - - def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout an inline form element */ - def inlineForm(mods: Modifier[HtmlElement]*): HtmlElement - - /** An input field with a search icon */ - def searchField(id: String, placeholderText: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement - - /** LocalDate input */ - def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala deleted file mode 100644 index 8acbe7f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala +++ /dev/null @@ -1,28 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.Tag - -trait GenericComponentsModule: - - def generic: GenericComponents - - trait GenericComponents: - def tag(name: String, color: ColorKind): HtmlElement - def tag(t: Tag): HtmlElement = tag(t.value, t.color) - -trait DefaultGenericComponentsModule(using ComponentContext) - extends GenericComponentsModule: - override val generic: GenericComponents = new GenericComponents: - override def tag(name: String, color: ColorKind): HtmlElement = - p( - cls( - "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" - ), - color(800).text, - color(100).bg, - name - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala deleted file mode 100644 index 8cfc1d9..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala +++ /dev/null @@ -1,123 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.CustomAttrs - -trait IconsModule: - def icons: Icons - - trait Icons: - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement - def close(mods: Modifier[SvgElement]*): SvgElement - def `search-solid`(mods: Modifier[SvgElement]*): SvgElement - def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement - def `document-chart-bar-outline`(mods: Modifier[SvgElement]*): SvgElement - def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement - -trait DefaultIconsModule(using ComponentContext) extends IconsModule: - override val icons: Icons = new Icons: - import svg.* - - object aria: - val hidden = CustomAttrs.svg.ariaHidden - - private def withDefault( - mods: Seq[Modifier[SvgElement]], - default: Modifier[SvgElement] - ): Modifier[SvgElement] = - if mods.isEmpty then default else mods - - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-8 w-8"), - 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" - ) - ) - ) - - override def close(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-2 w-2"), - stroke := "currentColor", - fill := "none", - viewBox := "0 0 8 8", - path( - strokeLineCap := "round", - strokeWidth := "1.5", - d := "M1 1l6 6m0-6L1 7" - ) - ) - - override def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 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 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") - ) - ) - - override def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 text-gray-400")), - 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") - ) - ) - - override def `document-chart-bar-outline`( - mods: Modifier[SvgElement]* - ): SvgElement = - svg( - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - strokeWidth := "1.5", - stroke := "currentColor", - withDefault(mods, cls := "h-6 w-6"), - path( - strokeLineCap := "round", - strokeLineJoin := "round", - d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" - ) - ) - - override def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-5 w-5"), - cls := "flex-shrink-0 text-gray-400", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", - clipRule := "evenodd" - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala deleted file mode 100644 index 95ab654..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait LayoutModule: - def layout: LayoutComponents - - trait LayoutComponents: - def card(content: Modifier[HtmlElement]*): HtmlElement - -trait DefaultLayoutModule(using ctx: ComponentContext) extends LayoutModule: - override val layout: LayoutComponents = new LayoutComponents: - override def card(content: Modifier[HtmlElement]*): HtmlElement = - div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala deleted file mode 100644 index 5946974..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom.html.Paragraph -import com.raquo.laminar.nodes.TextNode - -trait ListComponentsModule: - - def list: ListComponents - - trait ListComponents(using ComponentContext): - def label(text: String, color: ColorKind): HtmlElement - def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI - def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] - def listSection(header: String, list: HtmlElement): Div - def navigation(sections: Modifier[HtmlElement]): HtmlElement - -trait DefaultListComponentsModule(using ComponentContext) - extends ListComponentsModule: - self: GenericComponentsModule => - - override val list: ListComponents = new ListComponents: - override def label( - text: String, - color: ColorKind - ): HtmlElement = generic.tag(text, color) - - override def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI = - li( - cls("group"), - div( - cls( - "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" - ), - avatar.map(a => - div( - cls("flex-shrink-0"), - div( - cls( - "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" - ), - a - ) - ) - ), - div( - cls("flex-1 min-w-0"), - p( - cls("text-sm font-medium text-gray-900"), - title, - span(cls("float-right"), right) - ), - subtitle.map(st => - p( - cls("text-sm text-gray-500 truncate"), - st - ) - ) - ) - ) - ) - - override def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = - ul( - cls("relative z-0 divide-y divide-gray-200"), - role("list"), - children - ) - - override def listSection( - header: String, - list: HtmlElement - ): Div = - div( - cls("relative"), - div( - cls( - "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" - ), - header - ), - list - ) - - override def navigation(sections: Modifier[HtmlElement]): HtmlElement = - navTag( - cls("flex-1 min-h-0 overflow-y-auto"), - sections - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala deleted file mode 100644 index 5b5490f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ModalComponentsModule: - - def modal: ModalComponents - - trait ModalComponents: - def modalDialog( - content: HtmlElement, - onClose: Modifier[HtmlElement] - ): HtmlElement - -trait DefaultModalComponentsModule(using ctx: ComponentContext) - extends ModalComponentsModule: - - override val modal: ModalComponents = new ModalComponents: - - override def modalDialog( - content: HtmlElement, - closeMod: Modifier[HtmlElement] - ): 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( - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")), - closeMod - ) - ) - - div( - cls("fixed inset-0 z-20 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - 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-7xl sm:align-middle" - ), - content - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala deleted file mode 100644 index b713d8a..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala +++ /dev/null @@ -1,71 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait PageComponentsModule: - - val page: PageComponents - - trait PageComponents: - def container( - children: Modifier[HtmlElement]* - ): HtmlElement - - def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement - - def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement] = emptyMod, - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement - - /** Visage for clickable text, like links - */ - def clickable: Modifier[HtmlElement] - -trait DefaultPageComponentsModule(using ComponentContext) - extends PageComponentsModule: - - override val page: PageComponents = new PageComponents: - override def container( - children: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), - children - ) - - override def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement = - div( - cls("p-8 bg-gray-100 h-full"), - header, - children - ) - - override def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement = - div( - cls("pb-5 border-b border-gray-200"), - div(cls("float-right"), right), - h1( - cls("text-2xl leading-6 font-medium text-gray-900"), - title - ), - subtitle.map( - p( - cls("text-sm font-medium text-gray-500"), - _ - ) - ) - ) - - override def clickable: Modifier[HtmlElement] = - cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala deleted file mode 100644 index f70ee87..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala +++ /dev/null @@ -1,108 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} - -trait TableComponentsModule: - def tables: TableComponents - - trait TableComponents: - def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )(table: Modifier[HtmlElement]*): HtmlElement - - def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement - - def headerRow(mods: Modifier[HtmlElement]*)( - cells: HtmlElement* - ): HtmlElement - - def dataRow(mods: Modifier[HtmlElement]*)(cells: HtmlElement*): HtmlElement - - def headerCell(content: Modifier[HtmlElement]): HtmlElement - - def dataCell(content: Modifier[HtmlElement]): HtmlElement - -trait DefaultTableComponentsModule extends TableComponentsModule: - - override lazy val tables: TableComponents = new TableComponents: - - override def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )( - table: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("px-4 sm:px-6 lg:px-8"), - div( - cls("sm:flex sm:items-center"), - div( - cls("sm:flex-auto"), - h1(cls("text-base font-semibold leading-6 text-gray-900"), title), - subtitle.map(st => p(cls("mt-2 text-sm text-gray-700"), st)) - ), - div(cls("mt-4 sm:ml-16 sm:mt-0 sm:flex-none"), actions) - ), - div( - cls("mt-8 flow-root"), - div( - cls("-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"), - div( - cls("inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"), - table - ) - ) - ) - ) - - override def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement = - table( - cls("min-w-full divide-y divide-gray-300"), - thead(header), - tbody(cls("divide-y divide-gray-200"), body) - ) - - override def headerRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-3.5 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-3.5 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-3.5 px-3")) - ) - ) - - override def dataRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - mods, - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-4 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-4 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-4 px-3")) - ) - ) - - override def headerCell(content: Modifier[HtmlElement]): HtmlElement = - th( - cls("text-left text-sm font-semibold text-gray-900"), - content - ) - - override def dataCell(content: Modifier[HtmlElement]): HtmlElement = - td( - cls("whitespace-nowrap text-sm text-gray-500"), - content - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala index b937d45..12d01ae 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala @@ -3,7 +3,7 @@ import com.raquo.laminar.api.L.{*, given} import works.iterative.ui.components.laminar.HtmlTabular import works.iterative.ui.model.tables.Tabular -import works.iterative.ui.components.laminar.TableComponentsModule +import works.iterative.ui.components.laminar.tailwind.ui.TableComponentsModule import works.iterative.core.UserMessage import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given import works.iterative.ui.components.tailwind.ComponentContext diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala new file mode 100644 index 0000000..87f3e8d --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala @@ -0,0 +1,54 @@ +package works.iterative.ui.components.laminar.tailwind.color + +import com.raquo.laminar.api.L.{*, given} + +/** Complete color definition that can be rendered to CSS. + * + * Includes the area, kind and weight of the color. + */ +case class Color(area: ColorArea, color: ColorDef): + def toCSS: String = s"${area.name}-${color.toCSS}" + +object Color: + import ColorDef.given + + given colorToCSS: Conversion[Color, HtmlMod] with + def apply(c: Color) = cls(c.toCSS) + + given colorToSVGCSS: Conversion[Color, SvgMod] with + def apply(c: Color) = svg.cls(c.toCSS) + + given colorSignalToCSS: Conversion[Signal[Color], HtmlMod] with + def apply(c: Signal[Color]) = cls <-- c.map(_.toCSS) + + given colorSignalToSVGCSS: Conversion[Signal[Color], SvgMod] with + def apply(c: Signal[Color]) = svg.cls <-- c.map(_.toCSS) + + def current = ColorDef(ColorKind.current) + def inherit = ColorDef(ColorKind.inherit) + def transp = ColorDef(ColorKind.transp) + def auto = ColorDef(ColorKind.auto) + def black = ColorDef(ColorKind.black) + def white = ColorDef(ColorKind.white) + def slate(weight: ColorWeight) = ColorDef(ColorKind.slate, weight) + def gray(weight: ColorWeight) = ColorDef(ColorKind.gray, weight) + def zinc(weight: ColorWeight) = ColorDef(ColorKind.zinc, weight) + def neutral(weight: ColorWeight) = ColorDef(ColorKind.neutral, weight) + def stone(weight: ColorWeight) = ColorDef(ColorKind.stone, weight) + def red(weight: ColorWeight) = ColorDef(ColorKind.red, weight) + def orange(weight: ColorWeight) = ColorDef(ColorKind.orange, weight) + def amber(weight: ColorWeight) = ColorDef(ColorKind.amber, weight) + def yellow(weight: ColorWeight) = ColorDef(ColorKind.yellow, weight) + def lime(weight: ColorWeight) = ColorDef(ColorKind.lime, weight) + def green(weight: ColorWeight) = ColorDef(ColorKind.green, weight) + def emerald(weight: ColorWeight) = ColorDef(ColorKind.emerald, weight) + def teal(weight: ColorWeight) = ColorDef(ColorKind.teal, weight) + def cyan(weight: ColorWeight) = ColorDef(ColorKind.cyan, weight) + def sky(weight: ColorWeight) = ColorDef(ColorKind.sky, weight) + def blue(weight: ColorWeight) = ColorDef(ColorKind.blue, weight) + def indigo(weight: ColorWeight) = ColorDef(ColorKind.indigo, weight) + def violet(weight: ColorWeight) = ColorDef(ColorKind.violet, weight) + def purple(weight: ColorWeight) = ColorDef(ColorKind.purple, weight) + def fuchsia(weight: ColorWeight) = ColorDef(ColorKind.fuchsia, weight) + def pink(weight: ColorWeight) = ColorDef(ColorKind.pink, weight) + def rose(weight: ColorWeight) = ColorDef(ColorKind.rose, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala new file mode 100644 index 0000000..4130840 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala @@ -0,0 +1,16 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines the area the color should apply to, eg. background, text, border, + * etc. + */ +enum ColorArea(val name: String): + case bg extends ColorArea("bg") + case text extends ColorArea("text") + case decoration extends ColorArea("decoration") + case border extends ColorArea("border") + case outline extends ColorArea("outline") + case divide extends ColorArea("divide") + case ring extends ColorArea("ring") + case ringOffset extends ColorArea("ring-offset") + case shadow extends ColorArea("shadow") + case accent extends ColorArea("accent") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala new file mode 100644 index 0000000..fab9404 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala @@ -0,0 +1,43 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** A combination of ColorKind and ColorWeight, if applicable. + * + * By applying area we get the full Color definition. + */ +sealed trait ColorDef: + def toCSS: String + + def bg = Color(ColorArea.bg, this) + def text = Color(ColorArea.text, this) + def decoration = Color(ColorArea.decoration, this) + def border = Color(ColorArea.border, this) + def outline = Color(ColorArea.outline, this) + def divide = Color(ColorArea.divide, this) + def ring = Color(ColorArea.ring, this) + def ringOffset = Color(ColorArea.ringOffset, this) + def shadow = Color(ColorArea.shadow, this) + def accent = Color(ColorArea.accent, this) + +// TODO: create a macro that will output the tailwind class as string directly during compilation, where possible. +object ColorDef: + case class WeightedColorDef( + kind: ColorKind, + weight: ColorWeight + ) extends ColorDef: + override def toCSS: String = s"${kind.name}-${weight.value}" + + case class UnweightedColorDef( + kind: ColorKind + ) extends ColorDef: + override def toCSS: String = kind.name + + // TODO: check that the kind is valid unweighted kind + // that means current, inherit, auto, transparent, black, white + // tried using implicit evidence, but the type inference for enumerations + // tends to generalize to the enum, instead of the real type + def apply[T <: ColorKind](kind: T)(using + ev: T <:< ColorKind.Unweighted + ): ColorDef = + UnweightedColorDef(kind) + def apply(kind: ColorKind, weight: ColorWeight): ColorDef = + WeightedColorDef(kind, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala new file mode 100644 index 0000000..865166b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala @@ -0,0 +1,47 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines what color should be used, without specifying the area or weight. + */ +sealed abstract class ColorKind private (val name: String): + def apply(weight: ColorWeight): ColorDef = + ColorDef.WeightedColorDef(this, weight) + +object ColorKind: + trait Unweighted: + self: ColorKind => + override def apply(weight: ColorWeight): ColorDef = + ColorDef.UnweightedColorDef(self) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case object current extends ColorKind("current") with Unweighted + case object inherit extends ColorKind("inherit") with Unweighted + // Not present in for all methods + case object transp extends ColorKind("transparent") with Unweighted + // Seen in accent, not preset otherwise + case object auto extends ColorKind("auto") with Unweighted + // Black and white do not have weight + case object black extends ColorKind("black") with Unweighted + case object white extends ColorKind("white") with Unweighted + case object slate extends ColorKind("slate") + case object gray extends ColorKind("gray") + case object zinc extends ColorKind("zinc") + case object neutral extends ColorKind("neutral") + case object stone extends ColorKind("stone") + case object red extends ColorKind("red") + case object orange extends ColorKind("orange") + case object amber extends ColorKind("amber") + case object yellow extends ColorKind("yellow") + case object lime extends ColorKind("lime") + case object green extends ColorKind("green") + case object emerald extends ColorKind("emerald") + case object teal extends ColorKind("teal") + case object cyan extends ColorKind("cyan") + case object sky extends ColorKind("sky") + case object blue extends ColorKind("blue") + case object indigo extends ColorKind("indigo") + case object violet extends ColorKind("violet") + case object purple extends ColorKind("purple") + case object fuchsia extends ColorKind("fuchsia") + case object pink extends ColorKind("pink") + case object rose extends ColorKind("rose") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala new file mode 100644 index 0000000..6f1a4cc --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala @@ -0,0 +1,33 @@ +package works.iterative.ui.components.laminar.tailwind.color + +opaque type ColorWeight = String + +extension (c: ColorWeight) def value: String = c + +/** Defines weight of a color, eg. 50, 100, 200, etc. + * + * Tailwind-like. + */ +object ColorWeight: + inline given int50: Conversion[50, ColorWeight] with + inline def apply(i: 50) = "50" + inline given int100: Conversion[100, ColorWeight] with + inline def apply(i: 100) = "100" + inline given int200: Conversion[200, ColorWeight] with + inline def apply(i: 200) = "200" + inline given int300: Conversion[300, ColorWeight] with + inline def apply(i: 300) = "300" + inline given int400: Conversion[400, ColorWeight] with + inline def apply(i: 400) = "400" + inline given int500: Conversion[500, ColorWeight] with + inline def apply(i: 500) = "500" + inline given int600: Conversion[600, ColorWeight] with + inline def apply(i: 600) = "600" + inline given int700: Conversion[700, ColorWeight] with + inline def apply(i: 700) = "700" + inline given int800: Conversion[800, ColorWeight] with + inline def apply(i: 800) = "800" + inline given int900: Conversion[900, ColorWeight] with + inline def apply(i: 900) = "900" + inline given int950: Conversion[950, ColorWeight] with + inline def apply(i: 950) = "950" diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala new file mode 100644 index 0000000..21e21f0 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala @@ -0,0 +1,12 @@ +package works.iterative.ui.components.laminar.tailwind + +/** We need a generic Color model that can be used both on server and on client. + * + * We have adopted the Tailwind model for now. There is nothing inherently + * Tailwind-specific in the implementation, but all the values are taken from + * their palette and the area model is very HTML biased. + * + * Still, I think that it is a good starting point for exploration and will + * satisfy our current needs. + */ +package object color {} diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" new file mode 100644 index 0000000..5c4e218 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" @@ -0,0 +1,21 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.model.color.ColorKind +import works.iterative.ui.components.tailwind.ComponentContext +import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given +import works.iterative.ui.model.Tag + +trait TailwindUIBadgesComponentsModule(using ComponentContext): + + object badges: + override def tag(name: String, color: ColorKind): HtmlElement = + p( + cls( + "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" + ), + color(800).text, + color(100).bg, + name + ) diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" new file mode 100644 index 0000000..0c9ca25 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" @@ -0,0 +1,24 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} + +trait TailwindUIHeadingsComponentsModule: + val headings: HeadingsComponents = new HeadingsComponents + + class HeadingsComponents: + def section(title: Node, actions: HtmlElement*): HtmlElement = + div( + cls("border-b border-gray-200 pb-5"), + div( + cls("sm:flex sm:items-center sm:justify-between"), + h3(cls("text-base font-semibold leading-6 text-gray-900"), title), + div( + cls("mt-3 flex sm:ml-4 sm:mt-0"), + actions match + case Nil => emptyMod + case first :: rest => + first :: rest.map(_.amend(cls("ml-3"))) + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala new file mode 100644 index 0000000..16ca01b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala @@ -0,0 +1,19 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.laminar.tailwind.color.ColorKind + +trait BadgeComponentsModule: + + object badges: + def pill(name: String, color: ColorKind): HtmlElement = + p( + cls( + "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" + ), + color(800).text, + color(100).bg, + name + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala new file mode 100644 index 0000000..ebb2968 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} + +trait ButtonComponentsModule: + + object buttons: + + private inline def srHelp(text: String): Modifier[HtmlElement] = + span(cls := "sr-only", text) + + def primaryButton( + id: String, + text: Modifier[HtmlElement], + icon: Option[SvgElement] = None, + buttonType: String = "submit" + )(mods: Modifier[HtmlElement]*): HtmlElement = + button( + tpe(buttonType), + cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + icon, + text, + mods + ) + + def secondaryButton( + id: String, + text: Modifier[HtmlElement], + icon: Option[SvgElement] = None, + buttonType: String = "button" + )(mods: Modifier[HtmlElement]*): HtmlElement = + button( + tpe(buttonType), + cls( + "ml-2 bg-white inline-flex justify-center 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" + ), + icon, + text, + mods + ) + + def inlineButton(id: String, icon: SvgElement)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + button( + tpe := "button", + cls := "ml-1 inline-flex h-4 w-4 flex-shrink-0 rounded-full p-1 text-gray-400 hover:bg-gray-200 hover:text-gray-500", + srHelp(id), + icon + ) + + def iconButton(id: String, icon: SvgElement, srText: Option[String] = None)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + button( + tpe := "button", + cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", + icon, + srText.map(srHelp(_)) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala new file mode 100644 index 0000000..eb6dd5f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala @@ -0,0 +1,101 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import org.scalajs.dom.html +import com.raquo.laminar.modifiers.KeyUpdater + +trait FormComponentsModule: + self: IconsModule => + + object forms: + + def section( + title: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] + )(content: Modifier[HtmlElement]*): HtmlElement = + div( + cls("space-y-6 sm:space-y-5"), + div( + h3(cls("text-lg leading-6 font-medium text-gray-900"), title), + subtitle.map(st => p(cls("mt-1 max-w-2xl text-sm text-gray-500"), st)) + ), + div(cls("mt-6 sm:mt-5 space-y-6 sm:space-y-5"), content) + ) + + def label(labelText: String, forId: Option[String] = None)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + L.label( + cls("block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"), + forId.map(id => L.forId(id)), + labelText, + mods + ) + + def field( + label: Modifier[HtmlElement] + )(content: Modifier[HtmlElement]*): 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, + div(cls("mt-1 sm:mt-0 sm:col-span-2"), content) + ) + + def field( + id: String, + labelText: String, + input: HtmlElement, + help: Option[String] + ): HtmlElement = + field( + label(labelText, Some(id))() + )( + input.amend(idAttr(id)), + help.map(h => p(cls("mt-2 text-sm text-gray-500"), h)) + ) + + def form(mods: Modifier[HtmlElement]*)( + sections: Modifier[HtmlElement]* + )(actions: Modifier[HtmlElement]*): HtmlElement = + L.form( + cls("space-y-8 divide-y divide-gray-200"), + mods, + sections, + div( + cls("pt-5"), + div(cls("flex justify-end"), actions) + ) + ) + + def inlineForm( + mods: Modifier[HtmlElement]* + ): HtmlElement = + L.form(cls("flex space-x-4"), mods) + + def inputField( + id: String, + labelText: String, + placeholderText: Option[String] = None, + inputType: String = "text", + helpText: Option[String] = None + ): HtmlElement = + field( + id, + labelText, + input( + tpe(inputType), + cls( + "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:max-w-xs sm:text-sm sm:leading-6" + ), + placeholderText.map(placeholder(_)) + ), + helpText + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala new file mode 100644 index 0000000..52da07e --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala @@ -0,0 +1,22 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} + +trait HeadingsComponentsModule: + object headings: + def section(title: Node, actions: HtmlElement*): HtmlElement = + div( + cls("border-b border-gray-200 pb-5"), + div( + cls("sm:flex sm:items-center sm:justify-between"), + h3(cls("text-base font-semibold leading-6 text-gray-900"), title), + div( + cls("mt-3 flex sm:ml-4 sm:mt-0"), + actions match + case Nil => emptyMod + case first :: rest => + first :: rest.map(_.amend(cls("ml-3"))) + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala new file mode 100644 index 0000000..c543150 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala @@ -0,0 +1,112 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext +import works.iterative.ui.components.tailwind.CustomAttrs + +trait IconsModule: + object icons: + import svg.* + + object aria: + val hidden = CustomAttrs.svg.ariaHidden + + private def withDefault( + mods: Seq[Modifier[SvgElement]], + default: Modifier[SvgElement] + ): Modifier[SvgElement] = + if mods.isEmpty then default else mods + + def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-8 w-8"), + 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" + ) + ) + ) + + def close(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-2 w-2"), + stroke := "currentColor", + fill := "none", + viewBox := "0 0 8 8", + path( + strokeLineCap := "round", + strokeWidth := "1.5", + d := "M1 1l6 6m0-6L1 7" + ) + ) + + def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls("h-5 w-5 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 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") + ) + ) + + def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls("h-5 w-5 text-gray-400")), + 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") + ) + ) + + def `document-chart-bar-outline`( + mods: Modifier[SvgElement]* + ): SvgElement = + svg( + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + strokeWidth := "1.5", + stroke := "currentColor", + withDefault(mods, cls := "h-6 w-6"), + path( + strokeLineCap := "round", + strokeLineJoin := "round", + d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" + ) + ) + + def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-5 w-5"), + cls := "flex-shrink-0 text-gray-400", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", + clipRule := "evenodd" + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala index a2c637c..bfa063a 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala @@ -1,13 +1,11 @@ package works.iterative.ui.components.laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorDef import com.raquo.laminar.tags.HtmlTag case class Bin[Source, +Value]( label: String, description: Option[String | HtmlElement], - color: ColorDef, valueOf: Source => Value ): def map[NewValue]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala deleted file mode 100644 index 3aac0bf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala +++ /dev/null @@ -1,34 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ButtonComponentsModule: - def buttons: ButtonComponents - - trait ButtonComponents: - def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala deleted file mode 100644 index 646e73d..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala +++ /dev/null @@ -1,191 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.color.ColorDef -import com.raquo.laminar.tags.HtmlTag -import org.scalajs.dom.html.UList -import com.raquo.laminar.nodes.ReactiveHtmlElement -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind - -trait DashboardComponentsModule: - def dashboard: DashboardComponents - - // Only methods that create HtmlElements - trait DashboardComponents(using ComponentContext): - def section(label: String, children: Modifier[HtmlElement]): Div - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI - def cardInitials( - initials: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div - def count(label: String, value: Int, color: ColorDef): Span - def counts(children: HtmlElement*): Span - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - -trait DefaultDashboardComponentsModule(using ComponentContext) - extends DashboardComponentsModule: - override val dashboard: DashboardComponents = new DashboardComponents: - def section(label: String, children: Modifier[HtmlElement]): Div = - div( - cls("mt-3"), - h2( - cls("text-gray-500 text-xs font-medium uppercase tracking-wide"), - label - ), - children - ) - - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] = - ul( - cls( - "mt-3 grid gap-5 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" - ), - children - ) - - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI = - li( - div( - cls("col-span-1 flex shadow-sm rounded-md"), - initials, - div( - cls( - "flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate" - ), - div( - cls("flex-1 px-4 py-2 text-sm truncate"), - div( - cls("text-gray-900 font-medium hover:text-gray-600 truncate"), - label - ), - p(cls("text-gray-500"), children) - ) - ) - ) - ) - - def cardInitials( - id: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div = - div( - cls( - "flex-shrink-0 flex flex-col items-center justify-center w-16 text-white text-sm font-medium rounded-l-md" - ), - color.map(_(600).bg), - div(id), - children - ) - - def count(label: String, value: Int, color: ColorDef): Span = - span(color.text, title(label), s"${value}") - - def counts(children: HtmlElement*): Span = - span(interleave(children, span(" / "))) - - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div = - div( - cls("flex flex-col mt-2"), - div( - cls( - "align-middle min-w-full shadow sm:rounded-lg" - ), - L.table( - cls("min-w-full border-separate border-spacing-0"), - styleAttr("border-spacing: 0"), // Tailwind somehow doesn't work - thead( - tr( - cls("border-t border-gray-200"), - headers.map(h => - th( - cls( - "sticky top-0 z-10 px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - h - ) - ) - ) - ), - tbody(cls("bg-white divide-y divide-gray-100"), sections) - ) - ) - ) - - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - td( - cls("bg-gray-50 border-gray-200 border-t border-b"), - cls( - "sticky top-14 z-10 bg-gray-50 px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - colSpan(cs), - label - ), - mods - ) - - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - mods, - td( - cls( - "px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900 truncate" - ), - label - ), - children.map(e => - td( - cls( - "whitespace-nowrap px-2 py-2 text-sm text-gray-500" - ), - e - ) - ) - ) - - private def interleave( - a: Seq[HtmlElement], - separator: => HtmlElement - ): Seq[HtmlElement] = - if a.size < 2 then a - else a.init.flatMap(n => Seq(n, separator)) :+ a.last diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala deleted file mode 100644 index a45beaf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.FileRef - -trait DetailComponentsModule: - - def details: DetailComponents - - trait DetailComponents: - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement - - def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement - - def files(fs: List[FileRef]): HtmlElement - - def file(f: FileRef): HtmlElement - -trait DefaultDetailComponentsModule(using ctx: ComponentContext) - extends DetailComponentsModule: - self: IconsModule => - - override val details: DetailComponents = new DetailComponents: - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement = - div( - div( - cls := "px-4 sm:px-0", - h3( - cls := "text-base font-semibold leading-7 text-gray-900", - title - ), - subtitle.map(st => - p( - cls := "mt-1 max-w-2xl text-sm leading-6 text-gray-500", - st - ) - ) - // TODO: actions - ), - div( - cls := "mt-6 border-t border-gray-100", - dl( - cls := "divide-y divide-gray-100", - fields - ) - ) - ) - - override def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0", - dt( - cls := "text-sm font-medium leading-6 text-gray-900", - title - ), - dd( - cls := "mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0", - content - ) - ) - - override def files(fs: List[FileRef]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-100 rounded-md border border-gray-200", - fs.map(file) - ) - - override def file(f: FileRef): HtmlElement = - li( - cls := "flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6", - div( - cls := "flex w-0 flex-1 items-center", - icons.`paper-clip-solid`(), - div( - cls := "ml-4 flex min-w-0 flex-1 gap-2", - span( - cls := "truncate font-medium", - f.name - ), - f.size.map(size => span(cls := "flex-shrink-0 text-gray-400", size)) - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - "Uložit" - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala new file mode 100644 index 0000000..f6c583f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait FormComponents(using ctx: ComponentContext) extends LocalDateSelectModule: + def searchIcon: SvgElement + + def renderLocalDateSelect( + id: String, + labelText: Option[String], + placeholderText: Option[String], + mods: LocalDateSelect => Modifier[HtmlElement] + ): HtmlElement = + div( + labelText, + input( + idAttr(id), + nameAttr(id), + autoComplete("date"), + tpe("date"), + cls( + "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ), + placeholderText.map(placeholder(_)), + mods(localDateSelect) + ) + ) + + def searchField( + id: String, + placeholderText: Option[String] = None + )( + mods: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls := "flex-1 min-w-0", + label(ctx.messages("forms.search.label"), forId(id), cls("sr-only")), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + searchIcon + ), + input( + tpe := "search", + nameAttr := "search", + idAttr := id, + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholderText + .orElse( + ctx.messages + .opt( + s"forms.search.${id}.placeholder", + s"form.search.placeholder" + ) + ) + .map(placeholder(_)), + mods + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala deleted file mode 100644 index 7714490..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala +++ /dev/null @@ -1,82 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -/** Form components - * - * A form is a collection of sections, each of which contains a collection of - * fields. - * - * Form -> * Section -> * Field - * -> * Action - * - * Each Field has an `id`, `label` and `input` element. The `id` is used to - * link the label to the input. - * - * The `input` element can be a simple text input, or a more complex component - * such as a date picker. The caller is expected to make the input work. - */ -trait FormComponentsModule extends LocalDateSelectModule: - def forms: FormComponents - - trait FormComponents: - - /** Layout the sections and actions of a form */ - def form(mods: Modifier[HtmlElement]*)(sections: Modifier[HtmlElement]*)( - actions: Modifier[HtmlElement]* - ): HtmlElement - - /** Section layout, with a title, optional subtitle and any content. */ - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label and any content */ - def field(label: Modifier[HtmlElement])( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label, input and optional help text */ - def field( - id: String, - label: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement - - def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout an inline form element */ - def inlineForm(mods: Modifier[HtmlElement]*): HtmlElement - - /** An input field with a search icon */ - def searchField(id: String, placeholderText: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement - - /** LocalDate input */ - def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala deleted file mode 100644 index 8acbe7f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala +++ /dev/null @@ -1,28 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.Tag - -trait GenericComponentsModule: - - def generic: GenericComponents - - trait GenericComponents: - def tag(name: String, color: ColorKind): HtmlElement - def tag(t: Tag): HtmlElement = tag(t.value, t.color) - -trait DefaultGenericComponentsModule(using ComponentContext) - extends GenericComponentsModule: - override val generic: GenericComponents = new GenericComponents: - override def tag(name: String, color: ColorKind): HtmlElement = - p( - cls( - "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" - ), - color(800).text, - color(100).bg, - name - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala deleted file mode 100644 index 8cfc1d9..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala +++ /dev/null @@ -1,123 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.CustomAttrs - -trait IconsModule: - def icons: Icons - - trait Icons: - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement - def close(mods: Modifier[SvgElement]*): SvgElement - def `search-solid`(mods: Modifier[SvgElement]*): SvgElement - def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement - def `document-chart-bar-outline`(mods: Modifier[SvgElement]*): SvgElement - def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement - -trait DefaultIconsModule(using ComponentContext) extends IconsModule: - override val icons: Icons = new Icons: - import svg.* - - object aria: - val hidden = CustomAttrs.svg.ariaHidden - - private def withDefault( - mods: Seq[Modifier[SvgElement]], - default: Modifier[SvgElement] - ): Modifier[SvgElement] = - if mods.isEmpty then default else mods - - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-8 w-8"), - 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" - ) - ) - ) - - override def close(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-2 w-2"), - stroke := "currentColor", - fill := "none", - viewBox := "0 0 8 8", - path( - strokeLineCap := "round", - strokeWidth := "1.5", - d := "M1 1l6 6m0-6L1 7" - ) - ) - - override def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 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 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") - ) - ) - - override def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 text-gray-400")), - 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") - ) - ) - - override def `document-chart-bar-outline`( - mods: Modifier[SvgElement]* - ): SvgElement = - svg( - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - strokeWidth := "1.5", - stroke := "currentColor", - withDefault(mods, cls := "h-6 w-6"), - path( - strokeLineCap := "round", - strokeLineJoin := "round", - d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" - ) - ) - - override def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-5 w-5"), - cls := "flex-shrink-0 text-gray-400", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", - clipRule := "evenodd" - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala deleted file mode 100644 index 95ab654..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait LayoutModule: - def layout: LayoutComponents - - trait LayoutComponents: - def card(content: Modifier[HtmlElement]*): HtmlElement - -trait DefaultLayoutModule(using ctx: ComponentContext) extends LayoutModule: - override val layout: LayoutComponents = new LayoutComponents: - override def card(content: Modifier[HtmlElement]*): HtmlElement = - div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala deleted file mode 100644 index 5946974..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom.html.Paragraph -import com.raquo.laminar.nodes.TextNode - -trait ListComponentsModule: - - def list: ListComponents - - trait ListComponents(using ComponentContext): - def label(text: String, color: ColorKind): HtmlElement - def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI - def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] - def listSection(header: String, list: HtmlElement): Div - def navigation(sections: Modifier[HtmlElement]): HtmlElement - -trait DefaultListComponentsModule(using ComponentContext) - extends ListComponentsModule: - self: GenericComponentsModule => - - override val list: ListComponents = new ListComponents: - override def label( - text: String, - color: ColorKind - ): HtmlElement = generic.tag(text, color) - - override def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI = - li( - cls("group"), - div( - cls( - "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" - ), - avatar.map(a => - div( - cls("flex-shrink-0"), - div( - cls( - "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" - ), - a - ) - ) - ), - div( - cls("flex-1 min-w-0"), - p( - cls("text-sm font-medium text-gray-900"), - title, - span(cls("float-right"), right) - ), - subtitle.map(st => - p( - cls("text-sm text-gray-500 truncate"), - st - ) - ) - ) - ) - ) - - override def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = - ul( - cls("relative z-0 divide-y divide-gray-200"), - role("list"), - children - ) - - override def listSection( - header: String, - list: HtmlElement - ): Div = - div( - cls("relative"), - div( - cls( - "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" - ), - header - ), - list - ) - - override def navigation(sections: Modifier[HtmlElement]): HtmlElement = - navTag( - cls("flex-1 min-h-0 overflow-y-auto"), - sections - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala deleted file mode 100644 index 5b5490f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ModalComponentsModule: - - def modal: ModalComponents - - trait ModalComponents: - def modalDialog( - content: HtmlElement, - onClose: Modifier[HtmlElement] - ): HtmlElement - -trait DefaultModalComponentsModule(using ctx: ComponentContext) - extends ModalComponentsModule: - - override val modal: ModalComponents = new ModalComponents: - - override def modalDialog( - content: HtmlElement, - closeMod: Modifier[HtmlElement] - ): 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( - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")), - closeMod - ) - ) - - div( - cls("fixed inset-0 z-20 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - 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-7xl sm:align-middle" - ), - content - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala deleted file mode 100644 index b713d8a..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala +++ /dev/null @@ -1,71 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait PageComponentsModule: - - val page: PageComponents - - trait PageComponents: - def container( - children: Modifier[HtmlElement]* - ): HtmlElement - - def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement - - def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement] = emptyMod, - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement - - /** Visage for clickable text, like links - */ - def clickable: Modifier[HtmlElement] - -trait DefaultPageComponentsModule(using ComponentContext) - extends PageComponentsModule: - - override val page: PageComponents = new PageComponents: - override def container( - children: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), - children - ) - - override def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement = - div( - cls("p-8 bg-gray-100 h-full"), - header, - children - ) - - override def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement = - div( - cls("pb-5 border-b border-gray-200"), - div(cls("float-right"), right), - h1( - cls("text-2xl leading-6 font-medium text-gray-900"), - title - ), - subtitle.map( - p( - cls("text-sm font-medium text-gray-500"), - _ - ) - ) - ) - - override def clickable: Modifier[HtmlElement] = - cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala deleted file mode 100644 index f70ee87..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala +++ /dev/null @@ -1,108 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} - -trait TableComponentsModule: - def tables: TableComponents - - trait TableComponents: - def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )(table: Modifier[HtmlElement]*): HtmlElement - - def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement - - def headerRow(mods: Modifier[HtmlElement]*)( - cells: HtmlElement* - ): HtmlElement - - def dataRow(mods: Modifier[HtmlElement]*)(cells: HtmlElement*): HtmlElement - - def headerCell(content: Modifier[HtmlElement]): HtmlElement - - def dataCell(content: Modifier[HtmlElement]): HtmlElement - -trait DefaultTableComponentsModule extends TableComponentsModule: - - override lazy val tables: TableComponents = new TableComponents: - - override def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )( - table: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("px-4 sm:px-6 lg:px-8"), - div( - cls("sm:flex sm:items-center"), - div( - cls("sm:flex-auto"), - h1(cls("text-base font-semibold leading-6 text-gray-900"), title), - subtitle.map(st => p(cls("mt-2 text-sm text-gray-700"), st)) - ), - div(cls("mt-4 sm:ml-16 sm:mt-0 sm:flex-none"), actions) - ), - div( - cls("mt-8 flow-root"), - div( - cls("-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"), - div( - cls("inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"), - table - ) - ) - ) - ) - - override def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement = - table( - cls("min-w-full divide-y divide-gray-300"), - thead(header), - tbody(cls("divide-y divide-gray-200"), body) - ) - - override def headerRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-3.5 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-3.5 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-3.5 px-3")) - ) - ) - - override def dataRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - mods, - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-4 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-4 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-4 px-3")) - ) - ) - - override def headerCell(content: Modifier[HtmlElement]): HtmlElement = - th( - cls("text-left text-sm font-semibold text-gray-900"), - content - ) - - override def dataCell(content: Modifier[HtmlElement]): HtmlElement = - td( - cls("whitespace-nowrap text-sm text-gray-500"), - content - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala index b937d45..12d01ae 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala @@ -3,7 +3,7 @@ import com.raquo.laminar.api.L.{*, given} import works.iterative.ui.components.laminar.HtmlTabular import works.iterative.ui.model.tables.Tabular -import works.iterative.ui.components.laminar.TableComponentsModule +import works.iterative.ui.components.laminar.tailwind.ui.TableComponentsModule import works.iterative.core.UserMessage import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given import works.iterative.ui.components.tailwind.ComponentContext diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala new file mode 100644 index 0000000..87f3e8d --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala @@ -0,0 +1,54 @@ +package works.iterative.ui.components.laminar.tailwind.color + +import com.raquo.laminar.api.L.{*, given} + +/** Complete color definition that can be rendered to CSS. + * + * Includes the area, kind and weight of the color. + */ +case class Color(area: ColorArea, color: ColorDef): + def toCSS: String = s"${area.name}-${color.toCSS}" + +object Color: + import ColorDef.given + + given colorToCSS: Conversion[Color, HtmlMod] with + def apply(c: Color) = cls(c.toCSS) + + given colorToSVGCSS: Conversion[Color, SvgMod] with + def apply(c: Color) = svg.cls(c.toCSS) + + given colorSignalToCSS: Conversion[Signal[Color], HtmlMod] with + def apply(c: Signal[Color]) = cls <-- c.map(_.toCSS) + + given colorSignalToSVGCSS: Conversion[Signal[Color], SvgMod] with + def apply(c: Signal[Color]) = svg.cls <-- c.map(_.toCSS) + + def current = ColorDef(ColorKind.current) + def inherit = ColorDef(ColorKind.inherit) + def transp = ColorDef(ColorKind.transp) + def auto = ColorDef(ColorKind.auto) + def black = ColorDef(ColorKind.black) + def white = ColorDef(ColorKind.white) + def slate(weight: ColorWeight) = ColorDef(ColorKind.slate, weight) + def gray(weight: ColorWeight) = ColorDef(ColorKind.gray, weight) + def zinc(weight: ColorWeight) = ColorDef(ColorKind.zinc, weight) + def neutral(weight: ColorWeight) = ColorDef(ColorKind.neutral, weight) + def stone(weight: ColorWeight) = ColorDef(ColorKind.stone, weight) + def red(weight: ColorWeight) = ColorDef(ColorKind.red, weight) + def orange(weight: ColorWeight) = ColorDef(ColorKind.orange, weight) + def amber(weight: ColorWeight) = ColorDef(ColorKind.amber, weight) + def yellow(weight: ColorWeight) = ColorDef(ColorKind.yellow, weight) + def lime(weight: ColorWeight) = ColorDef(ColorKind.lime, weight) + def green(weight: ColorWeight) = ColorDef(ColorKind.green, weight) + def emerald(weight: ColorWeight) = ColorDef(ColorKind.emerald, weight) + def teal(weight: ColorWeight) = ColorDef(ColorKind.teal, weight) + def cyan(weight: ColorWeight) = ColorDef(ColorKind.cyan, weight) + def sky(weight: ColorWeight) = ColorDef(ColorKind.sky, weight) + def blue(weight: ColorWeight) = ColorDef(ColorKind.blue, weight) + def indigo(weight: ColorWeight) = ColorDef(ColorKind.indigo, weight) + def violet(weight: ColorWeight) = ColorDef(ColorKind.violet, weight) + def purple(weight: ColorWeight) = ColorDef(ColorKind.purple, weight) + def fuchsia(weight: ColorWeight) = ColorDef(ColorKind.fuchsia, weight) + def pink(weight: ColorWeight) = ColorDef(ColorKind.pink, weight) + def rose(weight: ColorWeight) = ColorDef(ColorKind.rose, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala new file mode 100644 index 0000000..4130840 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala @@ -0,0 +1,16 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines the area the color should apply to, eg. background, text, border, + * etc. + */ +enum ColorArea(val name: String): + case bg extends ColorArea("bg") + case text extends ColorArea("text") + case decoration extends ColorArea("decoration") + case border extends ColorArea("border") + case outline extends ColorArea("outline") + case divide extends ColorArea("divide") + case ring extends ColorArea("ring") + case ringOffset extends ColorArea("ring-offset") + case shadow extends ColorArea("shadow") + case accent extends ColorArea("accent") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala new file mode 100644 index 0000000..fab9404 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala @@ -0,0 +1,43 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** A combination of ColorKind and ColorWeight, if applicable. + * + * By applying area we get the full Color definition. + */ +sealed trait ColorDef: + def toCSS: String + + def bg = Color(ColorArea.bg, this) + def text = Color(ColorArea.text, this) + def decoration = Color(ColorArea.decoration, this) + def border = Color(ColorArea.border, this) + def outline = Color(ColorArea.outline, this) + def divide = Color(ColorArea.divide, this) + def ring = Color(ColorArea.ring, this) + def ringOffset = Color(ColorArea.ringOffset, this) + def shadow = Color(ColorArea.shadow, this) + def accent = Color(ColorArea.accent, this) + +// TODO: create a macro that will output the tailwind class as string directly during compilation, where possible. +object ColorDef: + case class WeightedColorDef( + kind: ColorKind, + weight: ColorWeight + ) extends ColorDef: + override def toCSS: String = s"${kind.name}-${weight.value}" + + case class UnweightedColorDef( + kind: ColorKind + ) extends ColorDef: + override def toCSS: String = kind.name + + // TODO: check that the kind is valid unweighted kind + // that means current, inherit, auto, transparent, black, white + // tried using implicit evidence, but the type inference for enumerations + // tends to generalize to the enum, instead of the real type + def apply[T <: ColorKind](kind: T)(using + ev: T <:< ColorKind.Unweighted + ): ColorDef = + UnweightedColorDef(kind) + def apply(kind: ColorKind, weight: ColorWeight): ColorDef = + WeightedColorDef(kind, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala new file mode 100644 index 0000000..865166b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala @@ -0,0 +1,47 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines what color should be used, without specifying the area or weight. + */ +sealed abstract class ColorKind private (val name: String): + def apply(weight: ColorWeight): ColorDef = + ColorDef.WeightedColorDef(this, weight) + +object ColorKind: + trait Unweighted: + self: ColorKind => + override def apply(weight: ColorWeight): ColorDef = + ColorDef.UnweightedColorDef(self) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case object current extends ColorKind("current") with Unweighted + case object inherit extends ColorKind("inherit") with Unweighted + // Not present in for all methods + case object transp extends ColorKind("transparent") with Unweighted + // Seen in accent, not preset otherwise + case object auto extends ColorKind("auto") with Unweighted + // Black and white do not have weight + case object black extends ColorKind("black") with Unweighted + case object white extends ColorKind("white") with Unweighted + case object slate extends ColorKind("slate") + case object gray extends ColorKind("gray") + case object zinc extends ColorKind("zinc") + case object neutral extends ColorKind("neutral") + case object stone extends ColorKind("stone") + case object red extends ColorKind("red") + case object orange extends ColorKind("orange") + case object amber extends ColorKind("amber") + case object yellow extends ColorKind("yellow") + case object lime extends ColorKind("lime") + case object green extends ColorKind("green") + case object emerald extends ColorKind("emerald") + case object teal extends ColorKind("teal") + case object cyan extends ColorKind("cyan") + case object sky extends ColorKind("sky") + case object blue extends ColorKind("blue") + case object indigo extends ColorKind("indigo") + case object violet extends ColorKind("violet") + case object purple extends ColorKind("purple") + case object fuchsia extends ColorKind("fuchsia") + case object pink extends ColorKind("pink") + case object rose extends ColorKind("rose") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala new file mode 100644 index 0000000..6f1a4cc --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala @@ -0,0 +1,33 @@ +package works.iterative.ui.components.laminar.tailwind.color + +opaque type ColorWeight = String + +extension (c: ColorWeight) def value: String = c + +/** Defines weight of a color, eg. 50, 100, 200, etc. + * + * Tailwind-like. + */ +object ColorWeight: + inline given int50: Conversion[50, ColorWeight] with + inline def apply(i: 50) = "50" + inline given int100: Conversion[100, ColorWeight] with + inline def apply(i: 100) = "100" + inline given int200: Conversion[200, ColorWeight] with + inline def apply(i: 200) = "200" + inline given int300: Conversion[300, ColorWeight] with + inline def apply(i: 300) = "300" + inline given int400: Conversion[400, ColorWeight] with + inline def apply(i: 400) = "400" + inline given int500: Conversion[500, ColorWeight] with + inline def apply(i: 500) = "500" + inline given int600: Conversion[600, ColorWeight] with + inline def apply(i: 600) = "600" + inline given int700: Conversion[700, ColorWeight] with + inline def apply(i: 700) = "700" + inline given int800: Conversion[800, ColorWeight] with + inline def apply(i: 800) = "800" + inline given int900: Conversion[900, ColorWeight] with + inline def apply(i: 900) = "900" + inline given int950: Conversion[950, ColorWeight] with + inline def apply(i: 950) = "950" diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala new file mode 100644 index 0000000..21e21f0 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala @@ -0,0 +1,12 @@ +package works.iterative.ui.components.laminar.tailwind + +/** We need a generic Color model that can be used both on server and on client. + * + * We have adopted the Tailwind model for now. There is nothing inherently + * Tailwind-specific in the implementation, but all the values are taken from + * their palette and the area model is very HTML biased. + * + * Still, I think that it is a good starting point for exploration and will + * satisfy our current needs. + */ +package object color {} diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" new file mode 100644 index 0000000..5c4e218 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" @@ -0,0 +1,21 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.model.color.ColorKind +import works.iterative.ui.components.tailwind.ComponentContext +import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given +import works.iterative.ui.model.Tag + +trait TailwindUIBadgesComponentsModule(using ComponentContext): + + object badges: + override def tag(name: String, color: ColorKind): HtmlElement = + p( + cls( + "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" + ), + color(800).text, + color(100).bg, + name + ) diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" new file mode 100644 index 0000000..0c9ca25 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" @@ -0,0 +1,24 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} + +trait TailwindUIHeadingsComponentsModule: + val headings: HeadingsComponents = new HeadingsComponents + + class HeadingsComponents: + def section(title: Node, actions: HtmlElement*): HtmlElement = + div( + cls("border-b border-gray-200 pb-5"), + div( + cls("sm:flex sm:items-center sm:justify-between"), + h3(cls("text-base font-semibold leading-6 text-gray-900"), title), + div( + cls("mt-3 flex sm:ml-4 sm:mt-0"), + actions match + case Nil => emptyMod + case first :: rest => + first :: rest.map(_.amend(cls("ml-3"))) + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala new file mode 100644 index 0000000..16ca01b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala @@ -0,0 +1,19 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.laminar.tailwind.color.ColorKind + +trait BadgeComponentsModule: + + object badges: + def pill(name: String, color: ColorKind): HtmlElement = + p( + cls( + "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" + ), + color(800).text, + color(100).bg, + name + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala new file mode 100644 index 0000000..ebb2968 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} + +trait ButtonComponentsModule: + + object buttons: + + private inline def srHelp(text: String): Modifier[HtmlElement] = + span(cls := "sr-only", text) + + def primaryButton( + id: String, + text: Modifier[HtmlElement], + icon: Option[SvgElement] = None, + buttonType: String = "submit" + )(mods: Modifier[HtmlElement]*): HtmlElement = + button( + tpe(buttonType), + cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + icon, + text, + mods + ) + + def secondaryButton( + id: String, + text: Modifier[HtmlElement], + icon: Option[SvgElement] = None, + buttonType: String = "button" + )(mods: Modifier[HtmlElement]*): HtmlElement = + button( + tpe(buttonType), + cls( + "ml-2 bg-white inline-flex justify-center 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" + ), + icon, + text, + mods + ) + + def inlineButton(id: String, icon: SvgElement)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + button( + tpe := "button", + cls := "ml-1 inline-flex h-4 w-4 flex-shrink-0 rounded-full p-1 text-gray-400 hover:bg-gray-200 hover:text-gray-500", + srHelp(id), + icon + ) + + def iconButton(id: String, icon: SvgElement, srText: Option[String] = None)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + button( + tpe := "button", + cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", + icon, + srText.map(srHelp(_)) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala new file mode 100644 index 0000000..eb6dd5f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala @@ -0,0 +1,101 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import org.scalajs.dom.html +import com.raquo.laminar.modifiers.KeyUpdater + +trait FormComponentsModule: + self: IconsModule => + + object forms: + + def section( + title: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] + )(content: Modifier[HtmlElement]*): HtmlElement = + div( + cls("space-y-6 sm:space-y-5"), + div( + h3(cls("text-lg leading-6 font-medium text-gray-900"), title), + subtitle.map(st => p(cls("mt-1 max-w-2xl text-sm text-gray-500"), st)) + ), + div(cls("mt-6 sm:mt-5 space-y-6 sm:space-y-5"), content) + ) + + def label(labelText: String, forId: Option[String] = None)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + L.label( + cls("block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"), + forId.map(id => L.forId(id)), + labelText, + mods + ) + + def field( + label: Modifier[HtmlElement] + )(content: Modifier[HtmlElement]*): 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, + div(cls("mt-1 sm:mt-0 sm:col-span-2"), content) + ) + + def field( + id: String, + labelText: String, + input: HtmlElement, + help: Option[String] + ): HtmlElement = + field( + label(labelText, Some(id))() + )( + input.amend(idAttr(id)), + help.map(h => p(cls("mt-2 text-sm text-gray-500"), h)) + ) + + def form(mods: Modifier[HtmlElement]*)( + sections: Modifier[HtmlElement]* + )(actions: Modifier[HtmlElement]*): HtmlElement = + L.form( + cls("space-y-8 divide-y divide-gray-200"), + mods, + sections, + div( + cls("pt-5"), + div(cls("flex justify-end"), actions) + ) + ) + + def inlineForm( + mods: Modifier[HtmlElement]* + ): HtmlElement = + L.form(cls("flex space-x-4"), mods) + + def inputField( + id: String, + labelText: String, + placeholderText: Option[String] = None, + inputType: String = "text", + helpText: Option[String] = None + ): HtmlElement = + field( + id, + labelText, + input( + tpe(inputType), + cls( + "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:max-w-xs sm:text-sm sm:leading-6" + ), + placeholderText.map(placeholder(_)) + ), + helpText + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala new file mode 100644 index 0000000..52da07e --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala @@ -0,0 +1,22 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} + +trait HeadingsComponentsModule: + object headings: + def section(title: Node, actions: HtmlElement*): HtmlElement = + div( + cls("border-b border-gray-200 pb-5"), + div( + cls("sm:flex sm:items-center sm:justify-between"), + h3(cls("text-base font-semibold leading-6 text-gray-900"), title), + div( + cls("mt-3 flex sm:ml-4 sm:mt-0"), + actions match + case Nil => emptyMod + case first :: rest => + first :: rest.map(_.amend(cls("ml-3"))) + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala new file mode 100644 index 0000000..c543150 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala @@ -0,0 +1,112 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext +import works.iterative.ui.components.tailwind.CustomAttrs + +trait IconsModule: + object icons: + import svg.* + + object aria: + val hidden = CustomAttrs.svg.ariaHidden + + private def withDefault( + mods: Seq[Modifier[SvgElement]], + default: Modifier[SvgElement] + ): Modifier[SvgElement] = + if mods.isEmpty then default else mods + + def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-8 w-8"), + 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" + ) + ) + ) + + def close(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-2 w-2"), + stroke := "currentColor", + fill := "none", + viewBox := "0 0 8 8", + path( + strokeLineCap := "round", + strokeWidth := "1.5", + d := "M1 1l6 6m0-6L1 7" + ) + ) + + def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls("h-5 w-5 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 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") + ) + ) + + def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls("h-5 w-5 text-gray-400")), + 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") + ) + ) + + def `document-chart-bar-outline`( + mods: Modifier[SvgElement]* + ): SvgElement = + svg( + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + strokeWidth := "1.5", + stroke := "currentColor", + withDefault(mods, cls := "h-6 w-6"), + path( + strokeLineCap := "round", + strokeLineJoin := "round", + d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" + ) + ) + + def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-5 w-5"), + cls := "flex-shrink-0 text-gray-400", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", + clipRule := "evenodd" + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala new file mode 100644 index 0000000..02cbdb6 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala @@ -0,0 +1,11 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait LayoutModule: + object layout: + def card(content: Modifier[HtmlElement]*): HtmlElement = + div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala index a2c637c..bfa063a 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala @@ -1,13 +1,11 @@ package works.iterative.ui.components.laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorDef import com.raquo.laminar.tags.HtmlTag case class Bin[Source, +Value]( label: String, description: Option[String | HtmlElement], - color: ColorDef, valueOf: Source => Value ): def map[NewValue]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala deleted file mode 100644 index 3aac0bf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala +++ /dev/null @@ -1,34 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ButtonComponentsModule: - def buttons: ButtonComponents - - trait ButtonComponents: - def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala deleted file mode 100644 index 646e73d..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala +++ /dev/null @@ -1,191 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.color.ColorDef -import com.raquo.laminar.tags.HtmlTag -import org.scalajs.dom.html.UList -import com.raquo.laminar.nodes.ReactiveHtmlElement -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind - -trait DashboardComponentsModule: - def dashboard: DashboardComponents - - // Only methods that create HtmlElements - trait DashboardComponents(using ComponentContext): - def section(label: String, children: Modifier[HtmlElement]): Div - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI - def cardInitials( - initials: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div - def count(label: String, value: Int, color: ColorDef): Span - def counts(children: HtmlElement*): Span - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - -trait DefaultDashboardComponentsModule(using ComponentContext) - extends DashboardComponentsModule: - override val dashboard: DashboardComponents = new DashboardComponents: - def section(label: String, children: Modifier[HtmlElement]): Div = - div( - cls("mt-3"), - h2( - cls("text-gray-500 text-xs font-medium uppercase tracking-wide"), - label - ), - children - ) - - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] = - ul( - cls( - "mt-3 grid gap-5 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" - ), - children - ) - - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI = - li( - div( - cls("col-span-1 flex shadow-sm rounded-md"), - initials, - div( - cls( - "flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate" - ), - div( - cls("flex-1 px-4 py-2 text-sm truncate"), - div( - cls("text-gray-900 font-medium hover:text-gray-600 truncate"), - label - ), - p(cls("text-gray-500"), children) - ) - ) - ) - ) - - def cardInitials( - id: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div = - div( - cls( - "flex-shrink-0 flex flex-col items-center justify-center w-16 text-white text-sm font-medium rounded-l-md" - ), - color.map(_(600).bg), - div(id), - children - ) - - def count(label: String, value: Int, color: ColorDef): Span = - span(color.text, title(label), s"${value}") - - def counts(children: HtmlElement*): Span = - span(interleave(children, span(" / "))) - - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div = - div( - cls("flex flex-col mt-2"), - div( - cls( - "align-middle min-w-full shadow sm:rounded-lg" - ), - L.table( - cls("min-w-full border-separate border-spacing-0"), - styleAttr("border-spacing: 0"), // Tailwind somehow doesn't work - thead( - tr( - cls("border-t border-gray-200"), - headers.map(h => - th( - cls( - "sticky top-0 z-10 px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - h - ) - ) - ) - ), - tbody(cls("bg-white divide-y divide-gray-100"), sections) - ) - ) - ) - - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - td( - cls("bg-gray-50 border-gray-200 border-t border-b"), - cls( - "sticky top-14 z-10 bg-gray-50 px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - colSpan(cs), - label - ), - mods - ) - - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - mods, - td( - cls( - "px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900 truncate" - ), - label - ), - children.map(e => - td( - cls( - "whitespace-nowrap px-2 py-2 text-sm text-gray-500" - ), - e - ) - ) - ) - - private def interleave( - a: Seq[HtmlElement], - separator: => HtmlElement - ): Seq[HtmlElement] = - if a.size < 2 then a - else a.init.flatMap(n => Seq(n, separator)) :+ a.last diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala deleted file mode 100644 index a45beaf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.FileRef - -trait DetailComponentsModule: - - def details: DetailComponents - - trait DetailComponents: - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement - - def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement - - def files(fs: List[FileRef]): HtmlElement - - def file(f: FileRef): HtmlElement - -trait DefaultDetailComponentsModule(using ctx: ComponentContext) - extends DetailComponentsModule: - self: IconsModule => - - override val details: DetailComponents = new DetailComponents: - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement = - div( - div( - cls := "px-4 sm:px-0", - h3( - cls := "text-base font-semibold leading-7 text-gray-900", - title - ), - subtitle.map(st => - p( - cls := "mt-1 max-w-2xl text-sm leading-6 text-gray-500", - st - ) - ) - // TODO: actions - ), - div( - cls := "mt-6 border-t border-gray-100", - dl( - cls := "divide-y divide-gray-100", - fields - ) - ) - ) - - override def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0", - dt( - cls := "text-sm font-medium leading-6 text-gray-900", - title - ), - dd( - cls := "mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0", - content - ) - ) - - override def files(fs: List[FileRef]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-100 rounded-md border border-gray-200", - fs.map(file) - ) - - override def file(f: FileRef): HtmlElement = - li( - cls := "flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6", - div( - cls := "flex w-0 flex-1 items-center", - icons.`paper-clip-solid`(), - div( - cls := "ml-4 flex min-w-0 flex-1 gap-2", - span( - cls := "truncate font-medium", - f.name - ), - f.size.map(size => span(cls := "flex-shrink-0 text-gray-400", size)) - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - "Uložit" - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala new file mode 100644 index 0000000..f6c583f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait FormComponents(using ctx: ComponentContext) extends LocalDateSelectModule: + def searchIcon: SvgElement + + def renderLocalDateSelect( + id: String, + labelText: Option[String], + placeholderText: Option[String], + mods: LocalDateSelect => Modifier[HtmlElement] + ): HtmlElement = + div( + labelText, + input( + idAttr(id), + nameAttr(id), + autoComplete("date"), + tpe("date"), + cls( + "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ), + placeholderText.map(placeholder(_)), + mods(localDateSelect) + ) + ) + + def searchField( + id: String, + placeholderText: Option[String] = None + )( + mods: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls := "flex-1 min-w-0", + label(ctx.messages("forms.search.label"), forId(id), cls("sr-only")), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + searchIcon + ), + input( + tpe := "search", + nameAttr := "search", + idAttr := id, + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholderText + .orElse( + ctx.messages + .opt( + s"forms.search.${id}.placeholder", + s"form.search.placeholder" + ) + ) + .map(placeholder(_)), + mods + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala deleted file mode 100644 index 7714490..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala +++ /dev/null @@ -1,82 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -/** Form components - * - * A form is a collection of sections, each of which contains a collection of - * fields. - * - * Form -> * Section -> * Field - * -> * Action - * - * Each Field has an `id`, `label` and `input` element. The `id` is used to - * link the label to the input. - * - * The `input` element can be a simple text input, or a more complex component - * such as a date picker. The caller is expected to make the input work. - */ -trait FormComponentsModule extends LocalDateSelectModule: - def forms: FormComponents - - trait FormComponents: - - /** Layout the sections and actions of a form */ - def form(mods: Modifier[HtmlElement]*)(sections: Modifier[HtmlElement]*)( - actions: Modifier[HtmlElement]* - ): HtmlElement - - /** Section layout, with a title, optional subtitle and any content. */ - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label and any content */ - def field(label: Modifier[HtmlElement])( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label, input and optional help text */ - def field( - id: String, - label: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement - - def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout an inline form element */ - def inlineForm(mods: Modifier[HtmlElement]*): HtmlElement - - /** An input field with a search icon */ - def searchField(id: String, placeholderText: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement - - /** LocalDate input */ - def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala deleted file mode 100644 index 8acbe7f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala +++ /dev/null @@ -1,28 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.Tag - -trait GenericComponentsModule: - - def generic: GenericComponents - - trait GenericComponents: - def tag(name: String, color: ColorKind): HtmlElement - def tag(t: Tag): HtmlElement = tag(t.value, t.color) - -trait DefaultGenericComponentsModule(using ComponentContext) - extends GenericComponentsModule: - override val generic: GenericComponents = new GenericComponents: - override def tag(name: String, color: ColorKind): HtmlElement = - p( - cls( - "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" - ), - color(800).text, - color(100).bg, - name - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala deleted file mode 100644 index 8cfc1d9..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala +++ /dev/null @@ -1,123 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.CustomAttrs - -trait IconsModule: - def icons: Icons - - trait Icons: - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement - def close(mods: Modifier[SvgElement]*): SvgElement - def `search-solid`(mods: Modifier[SvgElement]*): SvgElement - def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement - def `document-chart-bar-outline`(mods: Modifier[SvgElement]*): SvgElement - def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement - -trait DefaultIconsModule(using ComponentContext) extends IconsModule: - override val icons: Icons = new Icons: - import svg.* - - object aria: - val hidden = CustomAttrs.svg.ariaHidden - - private def withDefault( - mods: Seq[Modifier[SvgElement]], - default: Modifier[SvgElement] - ): Modifier[SvgElement] = - if mods.isEmpty then default else mods - - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-8 w-8"), - 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" - ) - ) - ) - - override def close(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-2 w-2"), - stroke := "currentColor", - fill := "none", - viewBox := "0 0 8 8", - path( - strokeLineCap := "round", - strokeWidth := "1.5", - d := "M1 1l6 6m0-6L1 7" - ) - ) - - override def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 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 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") - ) - ) - - override def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 text-gray-400")), - 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") - ) - ) - - override def `document-chart-bar-outline`( - mods: Modifier[SvgElement]* - ): SvgElement = - svg( - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - strokeWidth := "1.5", - stroke := "currentColor", - withDefault(mods, cls := "h-6 w-6"), - path( - strokeLineCap := "round", - strokeLineJoin := "round", - d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" - ) - ) - - override def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-5 w-5"), - cls := "flex-shrink-0 text-gray-400", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", - clipRule := "evenodd" - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala deleted file mode 100644 index 95ab654..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait LayoutModule: - def layout: LayoutComponents - - trait LayoutComponents: - def card(content: Modifier[HtmlElement]*): HtmlElement - -trait DefaultLayoutModule(using ctx: ComponentContext) extends LayoutModule: - override val layout: LayoutComponents = new LayoutComponents: - override def card(content: Modifier[HtmlElement]*): HtmlElement = - div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala deleted file mode 100644 index 5946974..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom.html.Paragraph -import com.raquo.laminar.nodes.TextNode - -trait ListComponentsModule: - - def list: ListComponents - - trait ListComponents(using ComponentContext): - def label(text: String, color: ColorKind): HtmlElement - def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI - def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] - def listSection(header: String, list: HtmlElement): Div - def navigation(sections: Modifier[HtmlElement]): HtmlElement - -trait DefaultListComponentsModule(using ComponentContext) - extends ListComponentsModule: - self: GenericComponentsModule => - - override val list: ListComponents = new ListComponents: - override def label( - text: String, - color: ColorKind - ): HtmlElement = generic.tag(text, color) - - override def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI = - li( - cls("group"), - div( - cls( - "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" - ), - avatar.map(a => - div( - cls("flex-shrink-0"), - div( - cls( - "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" - ), - a - ) - ) - ), - div( - cls("flex-1 min-w-0"), - p( - cls("text-sm font-medium text-gray-900"), - title, - span(cls("float-right"), right) - ), - subtitle.map(st => - p( - cls("text-sm text-gray-500 truncate"), - st - ) - ) - ) - ) - ) - - override def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = - ul( - cls("relative z-0 divide-y divide-gray-200"), - role("list"), - children - ) - - override def listSection( - header: String, - list: HtmlElement - ): Div = - div( - cls("relative"), - div( - cls( - "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" - ), - header - ), - list - ) - - override def navigation(sections: Modifier[HtmlElement]): HtmlElement = - navTag( - cls("flex-1 min-h-0 overflow-y-auto"), - sections - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala deleted file mode 100644 index 5b5490f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ModalComponentsModule: - - def modal: ModalComponents - - trait ModalComponents: - def modalDialog( - content: HtmlElement, - onClose: Modifier[HtmlElement] - ): HtmlElement - -trait DefaultModalComponentsModule(using ctx: ComponentContext) - extends ModalComponentsModule: - - override val modal: ModalComponents = new ModalComponents: - - override def modalDialog( - content: HtmlElement, - closeMod: Modifier[HtmlElement] - ): 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( - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")), - closeMod - ) - ) - - div( - cls("fixed inset-0 z-20 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - 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-7xl sm:align-middle" - ), - content - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala deleted file mode 100644 index b713d8a..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala +++ /dev/null @@ -1,71 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait PageComponentsModule: - - val page: PageComponents - - trait PageComponents: - def container( - children: Modifier[HtmlElement]* - ): HtmlElement - - def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement - - def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement] = emptyMod, - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement - - /** Visage for clickable text, like links - */ - def clickable: Modifier[HtmlElement] - -trait DefaultPageComponentsModule(using ComponentContext) - extends PageComponentsModule: - - override val page: PageComponents = new PageComponents: - override def container( - children: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), - children - ) - - override def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement = - div( - cls("p-8 bg-gray-100 h-full"), - header, - children - ) - - override def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement = - div( - cls("pb-5 border-b border-gray-200"), - div(cls("float-right"), right), - h1( - cls("text-2xl leading-6 font-medium text-gray-900"), - title - ), - subtitle.map( - p( - cls("text-sm font-medium text-gray-500"), - _ - ) - ) - ) - - override def clickable: Modifier[HtmlElement] = - cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala deleted file mode 100644 index f70ee87..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala +++ /dev/null @@ -1,108 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} - -trait TableComponentsModule: - def tables: TableComponents - - trait TableComponents: - def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )(table: Modifier[HtmlElement]*): HtmlElement - - def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement - - def headerRow(mods: Modifier[HtmlElement]*)( - cells: HtmlElement* - ): HtmlElement - - def dataRow(mods: Modifier[HtmlElement]*)(cells: HtmlElement*): HtmlElement - - def headerCell(content: Modifier[HtmlElement]): HtmlElement - - def dataCell(content: Modifier[HtmlElement]): HtmlElement - -trait DefaultTableComponentsModule extends TableComponentsModule: - - override lazy val tables: TableComponents = new TableComponents: - - override def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )( - table: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("px-4 sm:px-6 lg:px-8"), - div( - cls("sm:flex sm:items-center"), - div( - cls("sm:flex-auto"), - h1(cls("text-base font-semibold leading-6 text-gray-900"), title), - subtitle.map(st => p(cls("mt-2 text-sm text-gray-700"), st)) - ), - div(cls("mt-4 sm:ml-16 sm:mt-0 sm:flex-none"), actions) - ), - div( - cls("mt-8 flow-root"), - div( - cls("-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"), - div( - cls("inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"), - table - ) - ) - ) - ) - - override def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement = - table( - cls("min-w-full divide-y divide-gray-300"), - thead(header), - tbody(cls("divide-y divide-gray-200"), body) - ) - - override def headerRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-3.5 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-3.5 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-3.5 px-3")) - ) - ) - - override def dataRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - mods, - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-4 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-4 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-4 px-3")) - ) - ) - - override def headerCell(content: Modifier[HtmlElement]): HtmlElement = - th( - cls("text-left text-sm font-semibold text-gray-900"), - content - ) - - override def dataCell(content: Modifier[HtmlElement]): HtmlElement = - td( - cls("whitespace-nowrap text-sm text-gray-500"), - content - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala index b937d45..12d01ae 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala @@ -3,7 +3,7 @@ import com.raquo.laminar.api.L.{*, given} import works.iterative.ui.components.laminar.HtmlTabular import works.iterative.ui.model.tables.Tabular -import works.iterative.ui.components.laminar.TableComponentsModule +import works.iterative.ui.components.laminar.tailwind.ui.TableComponentsModule import works.iterative.core.UserMessage import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given import works.iterative.ui.components.tailwind.ComponentContext diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala new file mode 100644 index 0000000..87f3e8d --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala @@ -0,0 +1,54 @@ +package works.iterative.ui.components.laminar.tailwind.color + +import com.raquo.laminar.api.L.{*, given} + +/** Complete color definition that can be rendered to CSS. + * + * Includes the area, kind and weight of the color. + */ +case class Color(area: ColorArea, color: ColorDef): + def toCSS: String = s"${area.name}-${color.toCSS}" + +object Color: + import ColorDef.given + + given colorToCSS: Conversion[Color, HtmlMod] with + def apply(c: Color) = cls(c.toCSS) + + given colorToSVGCSS: Conversion[Color, SvgMod] with + def apply(c: Color) = svg.cls(c.toCSS) + + given colorSignalToCSS: Conversion[Signal[Color], HtmlMod] with + def apply(c: Signal[Color]) = cls <-- c.map(_.toCSS) + + given colorSignalToSVGCSS: Conversion[Signal[Color], SvgMod] with + def apply(c: Signal[Color]) = svg.cls <-- c.map(_.toCSS) + + def current = ColorDef(ColorKind.current) + def inherit = ColorDef(ColorKind.inherit) + def transp = ColorDef(ColorKind.transp) + def auto = ColorDef(ColorKind.auto) + def black = ColorDef(ColorKind.black) + def white = ColorDef(ColorKind.white) + def slate(weight: ColorWeight) = ColorDef(ColorKind.slate, weight) + def gray(weight: ColorWeight) = ColorDef(ColorKind.gray, weight) + def zinc(weight: ColorWeight) = ColorDef(ColorKind.zinc, weight) + def neutral(weight: ColorWeight) = ColorDef(ColorKind.neutral, weight) + def stone(weight: ColorWeight) = ColorDef(ColorKind.stone, weight) + def red(weight: ColorWeight) = ColorDef(ColorKind.red, weight) + def orange(weight: ColorWeight) = ColorDef(ColorKind.orange, weight) + def amber(weight: ColorWeight) = ColorDef(ColorKind.amber, weight) + def yellow(weight: ColorWeight) = ColorDef(ColorKind.yellow, weight) + def lime(weight: ColorWeight) = ColorDef(ColorKind.lime, weight) + def green(weight: ColorWeight) = ColorDef(ColorKind.green, weight) + def emerald(weight: ColorWeight) = ColorDef(ColorKind.emerald, weight) + def teal(weight: ColorWeight) = ColorDef(ColorKind.teal, weight) + def cyan(weight: ColorWeight) = ColorDef(ColorKind.cyan, weight) + def sky(weight: ColorWeight) = ColorDef(ColorKind.sky, weight) + def blue(weight: ColorWeight) = ColorDef(ColorKind.blue, weight) + def indigo(weight: ColorWeight) = ColorDef(ColorKind.indigo, weight) + def violet(weight: ColorWeight) = ColorDef(ColorKind.violet, weight) + def purple(weight: ColorWeight) = ColorDef(ColorKind.purple, weight) + def fuchsia(weight: ColorWeight) = ColorDef(ColorKind.fuchsia, weight) + def pink(weight: ColorWeight) = ColorDef(ColorKind.pink, weight) + def rose(weight: ColorWeight) = ColorDef(ColorKind.rose, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala new file mode 100644 index 0000000..4130840 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala @@ -0,0 +1,16 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines the area the color should apply to, eg. background, text, border, + * etc. + */ +enum ColorArea(val name: String): + case bg extends ColorArea("bg") + case text extends ColorArea("text") + case decoration extends ColorArea("decoration") + case border extends ColorArea("border") + case outline extends ColorArea("outline") + case divide extends ColorArea("divide") + case ring extends ColorArea("ring") + case ringOffset extends ColorArea("ring-offset") + case shadow extends ColorArea("shadow") + case accent extends ColorArea("accent") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala new file mode 100644 index 0000000..fab9404 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala @@ -0,0 +1,43 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** A combination of ColorKind and ColorWeight, if applicable. + * + * By applying area we get the full Color definition. + */ +sealed trait ColorDef: + def toCSS: String + + def bg = Color(ColorArea.bg, this) + def text = Color(ColorArea.text, this) + def decoration = Color(ColorArea.decoration, this) + def border = Color(ColorArea.border, this) + def outline = Color(ColorArea.outline, this) + def divide = Color(ColorArea.divide, this) + def ring = Color(ColorArea.ring, this) + def ringOffset = Color(ColorArea.ringOffset, this) + def shadow = Color(ColorArea.shadow, this) + def accent = Color(ColorArea.accent, this) + +// TODO: create a macro that will output the tailwind class as string directly during compilation, where possible. +object ColorDef: + case class WeightedColorDef( + kind: ColorKind, + weight: ColorWeight + ) extends ColorDef: + override def toCSS: String = s"${kind.name}-${weight.value}" + + case class UnweightedColorDef( + kind: ColorKind + ) extends ColorDef: + override def toCSS: String = kind.name + + // TODO: check that the kind is valid unweighted kind + // that means current, inherit, auto, transparent, black, white + // tried using implicit evidence, but the type inference for enumerations + // tends to generalize to the enum, instead of the real type + def apply[T <: ColorKind](kind: T)(using + ev: T <:< ColorKind.Unweighted + ): ColorDef = + UnweightedColorDef(kind) + def apply(kind: ColorKind, weight: ColorWeight): ColorDef = + WeightedColorDef(kind, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala new file mode 100644 index 0000000..865166b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala @@ -0,0 +1,47 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines what color should be used, without specifying the area or weight. + */ +sealed abstract class ColorKind private (val name: String): + def apply(weight: ColorWeight): ColorDef = + ColorDef.WeightedColorDef(this, weight) + +object ColorKind: + trait Unweighted: + self: ColorKind => + override def apply(weight: ColorWeight): ColorDef = + ColorDef.UnweightedColorDef(self) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case object current extends ColorKind("current") with Unweighted + case object inherit extends ColorKind("inherit") with Unweighted + // Not present in for all methods + case object transp extends ColorKind("transparent") with Unweighted + // Seen in accent, not preset otherwise + case object auto extends ColorKind("auto") with Unweighted + // Black and white do not have weight + case object black extends ColorKind("black") with Unweighted + case object white extends ColorKind("white") with Unweighted + case object slate extends ColorKind("slate") + case object gray extends ColorKind("gray") + case object zinc extends ColorKind("zinc") + case object neutral extends ColorKind("neutral") + case object stone extends ColorKind("stone") + case object red extends ColorKind("red") + case object orange extends ColorKind("orange") + case object amber extends ColorKind("amber") + case object yellow extends ColorKind("yellow") + case object lime extends ColorKind("lime") + case object green extends ColorKind("green") + case object emerald extends ColorKind("emerald") + case object teal extends ColorKind("teal") + case object cyan extends ColorKind("cyan") + case object sky extends ColorKind("sky") + case object blue extends ColorKind("blue") + case object indigo extends ColorKind("indigo") + case object violet extends ColorKind("violet") + case object purple extends ColorKind("purple") + case object fuchsia extends ColorKind("fuchsia") + case object pink extends ColorKind("pink") + case object rose extends ColorKind("rose") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala new file mode 100644 index 0000000..6f1a4cc --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala @@ -0,0 +1,33 @@ +package works.iterative.ui.components.laminar.tailwind.color + +opaque type ColorWeight = String + +extension (c: ColorWeight) def value: String = c + +/** Defines weight of a color, eg. 50, 100, 200, etc. + * + * Tailwind-like. + */ +object ColorWeight: + inline given int50: Conversion[50, ColorWeight] with + inline def apply(i: 50) = "50" + inline given int100: Conversion[100, ColorWeight] with + inline def apply(i: 100) = "100" + inline given int200: Conversion[200, ColorWeight] with + inline def apply(i: 200) = "200" + inline given int300: Conversion[300, ColorWeight] with + inline def apply(i: 300) = "300" + inline given int400: Conversion[400, ColorWeight] with + inline def apply(i: 400) = "400" + inline given int500: Conversion[500, ColorWeight] with + inline def apply(i: 500) = "500" + inline given int600: Conversion[600, ColorWeight] with + inline def apply(i: 600) = "600" + inline given int700: Conversion[700, ColorWeight] with + inline def apply(i: 700) = "700" + inline given int800: Conversion[800, ColorWeight] with + inline def apply(i: 800) = "800" + inline given int900: Conversion[900, ColorWeight] with + inline def apply(i: 900) = "900" + inline given int950: Conversion[950, ColorWeight] with + inline def apply(i: 950) = "950" diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala new file mode 100644 index 0000000..21e21f0 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala @@ -0,0 +1,12 @@ +package works.iterative.ui.components.laminar.tailwind + +/** We need a generic Color model that can be used both on server and on client. + * + * We have adopted the Tailwind model for now. There is nothing inherently + * Tailwind-specific in the implementation, but all the values are taken from + * their palette and the area model is very HTML biased. + * + * Still, I think that it is a good starting point for exploration and will + * satisfy our current needs. + */ +package object color {} diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" new file mode 100644 index 0000000..5c4e218 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" @@ -0,0 +1,21 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.model.color.ColorKind +import works.iterative.ui.components.tailwind.ComponentContext +import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given +import works.iterative.ui.model.Tag + +trait TailwindUIBadgesComponentsModule(using ComponentContext): + + object badges: + override def tag(name: String, color: ColorKind): HtmlElement = + p( + cls( + "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" + ), + color(800).text, + color(100).bg, + name + ) diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" new file mode 100644 index 0000000..0c9ca25 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" @@ -0,0 +1,24 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} + +trait TailwindUIHeadingsComponentsModule: + val headings: HeadingsComponents = new HeadingsComponents + + class HeadingsComponents: + def section(title: Node, actions: HtmlElement*): HtmlElement = + div( + cls("border-b border-gray-200 pb-5"), + div( + cls("sm:flex sm:items-center sm:justify-between"), + h3(cls("text-base font-semibold leading-6 text-gray-900"), title), + div( + cls("mt-3 flex sm:ml-4 sm:mt-0"), + actions match + case Nil => emptyMod + case first :: rest => + first :: rest.map(_.amend(cls("ml-3"))) + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala new file mode 100644 index 0000000..16ca01b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala @@ -0,0 +1,19 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.laminar.tailwind.color.ColorKind + +trait BadgeComponentsModule: + + object badges: + def pill(name: String, color: ColorKind): HtmlElement = + p( + cls( + "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" + ), + color(800).text, + color(100).bg, + name + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala new file mode 100644 index 0000000..ebb2968 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} + +trait ButtonComponentsModule: + + object buttons: + + private inline def srHelp(text: String): Modifier[HtmlElement] = + span(cls := "sr-only", text) + + def primaryButton( + id: String, + text: Modifier[HtmlElement], + icon: Option[SvgElement] = None, + buttonType: String = "submit" + )(mods: Modifier[HtmlElement]*): HtmlElement = + button( + tpe(buttonType), + cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + icon, + text, + mods + ) + + def secondaryButton( + id: String, + text: Modifier[HtmlElement], + icon: Option[SvgElement] = None, + buttonType: String = "button" + )(mods: Modifier[HtmlElement]*): HtmlElement = + button( + tpe(buttonType), + cls( + "ml-2 bg-white inline-flex justify-center 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" + ), + icon, + text, + mods + ) + + def inlineButton(id: String, icon: SvgElement)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + button( + tpe := "button", + cls := "ml-1 inline-flex h-4 w-4 flex-shrink-0 rounded-full p-1 text-gray-400 hover:bg-gray-200 hover:text-gray-500", + srHelp(id), + icon + ) + + def iconButton(id: String, icon: SvgElement, srText: Option[String] = None)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + button( + tpe := "button", + cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", + icon, + srText.map(srHelp(_)) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala new file mode 100644 index 0000000..eb6dd5f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala @@ -0,0 +1,101 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import org.scalajs.dom.html +import com.raquo.laminar.modifiers.KeyUpdater + +trait FormComponentsModule: + self: IconsModule => + + object forms: + + def section( + title: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] + )(content: Modifier[HtmlElement]*): HtmlElement = + div( + cls("space-y-6 sm:space-y-5"), + div( + h3(cls("text-lg leading-6 font-medium text-gray-900"), title), + subtitle.map(st => p(cls("mt-1 max-w-2xl text-sm text-gray-500"), st)) + ), + div(cls("mt-6 sm:mt-5 space-y-6 sm:space-y-5"), content) + ) + + def label(labelText: String, forId: Option[String] = None)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + L.label( + cls("block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"), + forId.map(id => L.forId(id)), + labelText, + mods + ) + + def field( + label: Modifier[HtmlElement] + )(content: Modifier[HtmlElement]*): 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, + div(cls("mt-1 sm:mt-0 sm:col-span-2"), content) + ) + + def field( + id: String, + labelText: String, + input: HtmlElement, + help: Option[String] + ): HtmlElement = + field( + label(labelText, Some(id))() + )( + input.amend(idAttr(id)), + help.map(h => p(cls("mt-2 text-sm text-gray-500"), h)) + ) + + def form(mods: Modifier[HtmlElement]*)( + sections: Modifier[HtmlElement]* + )(actions: Modifier[HtmlElement]*): HtmlElement = + L.form( + cls("space-y-8 divide-y divide-gray-200"), + mods, + sections, + div( + cls("pt-5"), + div(cls("flex justify-end"), actions) + ) + ) + + def inlineForm( + mods: Modifier[HtmlElement]* + ): HtmlElement = + L.form(cls("flex space-x-4"), mods) + + def inputField( + id: String, + labelText: String, + placeholderText: Option[String] = None, + inputType: String = "text", + helpText: Option[String] = None + ): HtmlElement = + field( + id, + labelText, + input( + tpe(inputType), + cls( + "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:max-w-xs sm:text-sm sm:leading-6" + ), + placeholderText.map(placeholder(_)) + ), + helpText + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala new file mode 100644 index 0000000..52da07e --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala @@ -0,0 +1,22 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} + +trait HeadingsComponentsModule: + object headings: + def section(title: Node, actions: HtmlElement*): HtmlElement = + div( + cls("border-b border-gray-200 pb-5"), + div( + cls("sm:flex sm:items-center sm:justify-between"), + h3(cls("text-base font-semibold leading-6 text-gray-900"), title), + div( + cls("mt-3 flex sm:ml-4 sm:mt-0"), + actions match + case Nil => emptyMod + case first :: rest => + first :: rest.map(_.amend(cls("ml-3"))) + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala new file mode 100644 index 0000000..c543150 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala @@ -0,0 +1,112 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext +import works.iterative.ui.components.tailwind.CustomAttrs + +trait IconsModule: + object icons: + import svg.* + + object aria: + val hidden = CustomAttrs.svg.ariaHidden + + private def withDefault( + mods: Seq[Modifier[SvgElement]], + default: Modifier[SvgElement] + ): Modifier[SvgElement] = + if mods.isEmpty then default else mods + + def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-8 w-8"), + 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" + ) + ) + ) + + def close(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-2 w-2"), + stroke := "currentColor", + fill := "none", + viewBox := "0 0 8 8", + path( + strokeLineCap := "round", + strokeWidth := "1.5", + d := "M1 1l6 6m0-6L1 7" + ) + ) + + def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls("h-5 w-5 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 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") + ) + ) + + def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls("h-5 w-5 text-gray-400")), + 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") + ) + ) + + def `document-chart-bar-outline`( + mods: Modifier[SvgElement]* + ): SvgElement = + svg( + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + strokeWidth := "1.5", + stroke := "currentColor", + withDefault(mods, cls := "h-6 w-6"), + path( + strokeLineCap := "round", + strokeLineJoin := "round", + d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" + ) + ) + + def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-5 w-5"), + cls := "flex-shrink-0 text-gray-400", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", + clipRule := "evenodd" + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala new file mode 100644 index 0000000..02cbdb6 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala @@ -0,0 +1,11 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait LayoutModule: + object layout: + def card(content: Modifier[HtmlElement]*): HtmlElement = + div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala new file mode 100644 index 0000000..b652da9 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala @@ -0,0 +1,88 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.laminar.nodes.ReactiveHtmlElement +import org.scalajs.dom.html.Paragraph +import com.raquo.laminar.nodes.TextNode +import works.iterative.ui.components.laminar.tailwind.color.ColorKind + +trait ListComponentsModule: + self: BadgeComponentsModule => + + object list: + def label( + text: String, + color: ColorKind + ): HtmlElement = badges.pill(text, color) + + def item( + title: String, + subtitle: Option[String], + right: Modifier[HtmlElement] = emptyMod, + avatar: Option[Modifier[HtmlElement]] = None + ): LI = + li( + cls("group"), + div( + cls( + "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" + ), + avatar.map(a => + div( + cls("flex-shrink-0"), + div( + cls( + "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" + ), + a + ) + ) + ), + div( + cls("flex-1 min-w-0"), + p( + cls("text-sm font-medium text-gray-900"), + title, + span(cls("float-right"), right) + ), + subtitle.map(st => + p( + cls("text-sm text-gray-500 truncate"), + st + ) + ) + ) + ) + ) + + def unordered( + children: Modifier[HtmlElement] + ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = + ul( + cls("relative z-0 divide-y divide-gray-200"), + role("list"), + children + ) + + def listSection( + header: String, + list: HtmlElement + ): Div = + div( + cls("relative"), + div( + cls( + "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" + ), + header + ), + list + ) + + def navigation(sections: Modifier[HtmlElement]): HtmlElement = + navTag( + cls("flex-1 min-h-0 overflow-y-auto"), + sections + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala index a2c637c..bfa063a 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala @@ -1,13 +1,11 @@ package works.iterative.ui.components.laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorDef import com.raquo.laminar.tags.HtmlTag case class Bin[Source, +Value]( label: String, description: Option[String | HtmlElement], - color: ColorDef, valueOf: Source => Value ): def map[NewValue]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala deleted file mode 100644 index 3aac0bf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala +++ /dev/null @@ -1,34 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ButtonComponentsModule: - def buttons: ButtonComponents - - trait ButtonComponents: - def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala deleted file mode 100644 index 646e73d..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala +++ /dev/null @@ -1,191 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.color.ColorDef -import com.raquo.laminar.tags.HtmlTag -import org.scalajs.dom.html.UList -import com.raquo.laminar.nodes.ReactiveHtmlElement -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind - -trait DashboardComponentsModule: - def dashboard: DashboardComponents - - // Only methods that create HtmlElements - trait DashboardComponents(using ComponentContext): - def section(label: String, children: Modifier[HtmlElement]): Div - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI - def cardInitials( - initials: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div - def count(label: String, value: Int, color: ColorDef): Span - def counts(children: HtmlElement*): Span - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - -trait DefaultDashboardComponentsModule(using ComponentContext) - extends DashboardComponentsModule: - override val dashboard: DashboardComponents = new DashboardComponents: - def section(label: String, children: Modifier[HtmlElement]): Div = - div( - cls("mt-3"), - h2( - cls("text-gray-500 text-xs font-medium uppercase tracking-wide"), - label - ), - children - ) - - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] = - ul( - cls( - "mt-3 grid gap-5 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" - ), - children - ) - - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI = - li( - div( - cls("col-span-1 flex shadow-sm rounded-md"), - initials, - div( - cls( - "flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate" - ), - div( - cls("flex-1 px-4 py-2 text-sm truncate"), - div( - cls("text-gray-900 font-medium hover:text-gray-600 truncate"), - label - ), - p(cls("text-gray-500"), children) - ) - ) - ) - ) - - def cardInitials( - id: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div = - div( - cls( - "flex-shrink-0 flex flex-col items-center justify-center w-16 text-white text-sm font-medium rounded-l-md" - ), - color.map(_(600).bg), - div(id), - children - ) - - def count(label: String, value: Int, color: ColorDef): Span = - span(color.text, title(label), s"${value}") - - def counts(children: HtmlElement*): Span = - span(interleave(children, span(" / "))) - - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div = - div( - cls("flex flex-col mt-2"), - div( - cls( - "align-middle min-w-full shadow sm:rounded-lg" - ), - L.table( - cls("min-w-full border-separate border-spacing-0"), - styleAttr("border-spacing: 0"), // Tailwind somehow doesn't work - thead( - tr( - cls("border-t border-gray-200"), - headers.map(h => - th( - cls( - "sticky top-0 z-10 px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - h - ) - ) - ) - ), - tbody(cls("bg-white divide-y divide-gray-100"), sections) - ) - ) - ) - - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - td( - cls("bg-gray-50 border-gray-200 border-t border-b"), - cls( - "sticky top-14 z-10 bg-gray-50 px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - colSpan(cs), - label - ), - mods - ) - - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - mods, - td( - cls( - "px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900 truncate" - ), - label - ), - children.map(e => - td( - cls( - "whitespace-nowrap px-2 py-2 text-sm text-gray-500" - ), - e - ) - ) - ) - - private def interleave( - a: Seq[HtmlElement], - separator: => HtmlElement - ): Seq[HtmlElement] = - if a.size < 2 then a - else a.init.flatMap(n => Seq(n, separator)) :+ a.last diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala deleted file mode 100644 index a45beaf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.FileRef - -trait DetailComponentsModule: - - def details: DetailComponents - - trait DetailComponents: - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement - - def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement - - def files(fs: List[FileRef]): HtmlElement - - def file(f: FileRef): HtmlElement - -trait DefaultDetailComponentsModule(using ctx: ComponentContext) - extends DetailComponentsModule: - self: IconsModule => - - override val details: DetailComponents = new DetailComponents: - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement = - div( - div( - cls := "px-4 sm:px-0", - h3( - cls := "text-base font-semibold leading-7 text-gray-900", - title - ), - subtitle.map(st => - p( - cls := "mt-1 max-w-2xl text-sm leading-6 text-gray-500", - st - ) - ) - // TODO: actions - ), - div( - cls := "mt-6 border-t border-gray-100", - dl( - cls := "divide-y divide-gray-100", - fields - ) - ) - ) - - override def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0", - dt( - cls := "text-sm font-medium leading-6 text-gray-900", - title - ), - dd( - cls := "mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0", - content - ) - ) - - override def files(fs: List[FileRef]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-100 rounded-md border border-gray-200", - fs.map(file) - ) - - override def file(f: FileRef): HtmlElement = - li( - cls := "flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6", - div( - cls := "flex w-0 flex-1 items-center", - icons.`paper-clip-solid`(), - div( - cls := "ml-4 flex min-w-0 flex-1 gap-2", - span( - cls := "truncate font-medium", - f.name - ), - f.size.map(size => span(cls := "flex-shrink-0 text-gray-400", size)) - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - "Uložit" - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala new file mode 100644 index 0000000..f6c583f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait FormComponents(using ctx: ComponentContext) extends LocalDateSelectModule: + def searchIcon: SvgElement + + def renderLocalDateSelect( + id: String, + labelText: Option[String], + placeholderText: Option[String], + mods: LocalDateSelect => Modifier[HtmlElement] + ): HtmlElement = + div( + labelText, + input( + idAttr(id), + nameAttr(id), + autoComplete("date"), + tpe("date"), + cls( + "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ), + placeholderText.map(placeholder(_)), + mods(localDateSelect) + ) + ) + + def searchField( + id: String, + placeholderText: Option[String] = None + )( + mods: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls := "flex-1 min-w-0", + label(ctx.messages("forms.search.label"), forId(id), cls("sr-only")), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + searchIcon + ), + input( + tpe := "search", + nameAttr := "search", + idAttr := id, + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholderText + .orElse( + ctx.messages + .opt( + s"forms.search.${id}.placeholder", + s"form.search.placeholder" + ) + ) + .map(placeholder(_)), + mods + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala deleted file mode 100644 index 7714490..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala +++ /dev/null @@ -1,82 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -/** Form components - * - * A form is a collection of sections, each of which contains a collection of - * fields. - * - * Form -> * Section -> * Field - * -> * Action - * - * Each Field has an `id`, `label` and `input` element. The `id` is used to - * link the label to the input. - * - * The `input` element can be a simple text input, or a more complex component - * such as a date picker. The caller is expected to make the input work. - */ -trait FormComponentsModule extends LocalDateSelectModule: - def forms: FormComponents - - trait FormComponents: - - /** Layout the sections and actions of a form */ - def form(mods: Modifier[HtmlElement]*)(sections: Modifier[HtmlElement]*)( - actions: Modifier[HtmlElement]* - ): HtmlElement - - /** Section layout, with a title, optional subtitle and any content. */ - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label and any content */ - def field(label: Modifier[HtmlElement])( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label, input and optional help text */ - def field( - id: String, - label: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement - - def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout an inline form element */ - def inlineForm(mods: Modifier[HtmlElement]*): HtmlElement - - /** An input field with a search icon */ - def searchField(id: String, placeholderText: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement - - /** LocalDate input */ - def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala deleted file mode 100644 index 8acbe7f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala +++ /dev/null @@ -1,28 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.Tag - -trait GenericComponentsModule: - - def generic: GenericComponents - - trait GenericComponents: - def tag(name: String, color: ColorKind): HtmlElement - def tag(t: Tag): HtmlElement = tag(t.value, t.color) - -trait DefaultGenericComponentsModule(using ComponentContext) - extends GenericComponentsModule: - override val generic: GenericComponents = new GenericComponents: - override def tag(name: String, color: ColorKind): HtmlElement = - p( - cls( - "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" - ), - color(800).text, - color(100).bg, - name - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala deleted file mode 100644 index 8cfc1d9..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala +++ /dev/null @@ -1,123 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.CustomAttrs - -trait IconsModule: - def icons: Icons - - trait Icons: - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement - def close(mods: Modifier[SvgElement]*): SvgElement - def `search-solid`(mods: Modifier[SvgElement]*): SvgElement - def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement - def `document-chart-bar-outline`(mods: Modifier[SvgElement]*): SvgElement - def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement - -trait DefaultIconsModule(using ComponentContext) extends IconsModule: - override val icons: Icons = new Icons: - import svg.* - - object aria: - val hidden = CustomAttrs.svg.ariaHidden - - private def withDefault( - mods: Seq[Modifier[SvgElement]], - default: Modifier[SvgElement] - ): Modifier[SvgElement] = - if mods.isEmpty then default else mods - - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-8 w-8"), - 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" - ) - ) - ) - - override def close(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-2 w-2"), - stroke := "currentColor", - fill := "none", - viewBox := "0 0 8 8", - path( - strokeLineCap := "round", - strokeWidth := "1.5", - d := "M1 1l6 6m0-6L1 7" - ) - ) - - override def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 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 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") - ) - ) - - override def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 text-gray-400")), - 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") - ) - ) - - override def `document-chart-bar-outline`( - mods: Modifier[SvgElement]* - ): SvgElement = - svg( - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - strokeWidth := "1.5", - stroke := "currentColor", - withDefault(mods, cls := "h-6 w-6"), - path( - strokeLineCap := "round", - strokeLineJoin := "round", - d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" - ) - ) - - override def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-5 w-5"), - cls := "flex-shrink-0 text-gray-400", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", - clipRule := "evenodd" - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala deleted file mode 100644 index 95ab654..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait LayoutModule: - def layout: LayoutComponents - - trait LayoutComponents: - def card(content: Modifier[HtmlElement]*): HtmlElement - -trait DefaultLayoutModule(using ctx: ComponentContext) extends LayoutModule: - override val layout: LayoutComponents = new LayoutComponents: - override def card(content: Modifier[HtmlElement]*): HtmlElement = - div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala deleted file mode 100644 index 5946974..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom.html.Paragraph -import com.raquo.laminar.nodes.TextNode - -trait ListComponentsModule: - - def list: ListComponents - - trait ListComponents(using ComponentContext): - def label(text: String, color: ColorKind): HtmlElement - def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI - def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] - def listSection(header: String, list: HtmlElement): Div - def navigation(sections: Modifier[HtmlElement]): HtmlElement - -trait DefaultListComponentsModule(using ComponentContext) - extends ListComponentsModule: - self: GenericComponentsModule => - - override val list: ListComponents = new ListComponents: - override def label( - text: String, - color: ColorKind - ): HtmlElement = generic.tag(text, color) - - override def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI = - li( - cls("group"), - div( - cls( - "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" - ), - avatar.map(a => - div( - cls("flex-shrink-0"), - div( - cls( - "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" - ), - a - ) - ) - ), - div( - cls("flex-1 min-w-0"), - p( - cls("text-sm font-medium text-gray-900"), - title, - span(cls("float-right"), right) - ), - subtitle.map(st => - p( - cls("text-sm text-gray-500 truncate"), - st - ) - ) - ) - ) - ) - - override def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = - ul( - cls("relative z-0 divide-y divide-gray-200"), - role("list"), - children - ) - - override def listSection( - header: String, - list: HtmlElement - ): Div = - div( - cls("relative"), - div( - cls( - "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" - ), - header - ), - list - ) - - override def navigation(sections: Modifier[HtmlElement]): HtmlElement = - navTag( - cls("flex-1 min-h-0 overflow-y-auto"), - sections - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala deleted file mode 100644 index 5b5490f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ModalComponentsModule: - - def modal: ModalComponents - - trait ModalComponents: - def modalDialog( - content: HtmlElement, - onClose: Modifier[HtmlElement] - ): HtmlElement - -trait DefaultModalComponentsModule(using ctx: ComponentContext) - extends ModalComponentsModule: - - override val modal: ModalComponents = new ModalComponents: - - override def modalDialog( - content: HtmlElement, - closeMod: Modifier[HtmlElement] - ): 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( - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")), - closeMod - ) - ) - - div( - cls("fixed inset-0 z-20 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - 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-7xl sm:align-middle" - ), - content - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala deleted file mode 100644 index b713d8a..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala +++ /dev/null @@ -1,71 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait PageComponentsModule: - - val page: PageComponents - - trait PageComponents: - def container( - children: Modifier[HtmlElement]* - ): HtmlElement - - def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement - - def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement] = emptyMod, - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement - - /** Visage for clickable text, like links - */ - def clickable: Modifier[HtmlElement] - -trait DefaultPageComponentsModule(using ComponentContext) - extends PageComponentsModule: - - override val page: PageComponents = new PageComponents: - override def container( - children: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), - children - ) - - override def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement = - div( - cls("p-8 bg-gray-100 h-full"), - header, - children - ) - - override def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement = - div( - cls("pb-5 border-b border-gray-200"), - div(cls("float-right"), right), - h1( - cls("text-2xl leading-6 font-medium text-gray-900"), - title - ), - subtitle.map( - p( - cls("text-sm font-medium text-gray-500"), - _ - ) - ) - ) - - override def clickable: Modifier[HtmlElement] = - cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala deleted file mode 100644 index f70ee87..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala +++ /dev/null @@ -1,108 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} - -trait TableComponentsModule: - def tables: TableComponents - - trait TableComponents: - def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )(table: Modifier[HtmlElement]*): HtmlElement - - def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement - - def headerRow(mods: Modifier[HtmlElement]*)( - cells: HtmlElement* - ): HtmlElement - - def dataRow(mods: Modifier[HtmlElement]*)(cells: HtmlElement*): HtmlElement - - def headerCell(content: Modifier[HtmlElement]): HtmlElement - - def dataCell(content: Modifier[HtmlElement]): HtmlElement - -trait DefaultTableComponentsModule extends TableComponentsModule: - - override lazy val tables: TableComponents = new TableComponents: - - override def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )( - table: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("px-4 sm:px-6 lg:px-8"), - div( - cls("sm:flex sm:items-center"), - div( - cls("sm:flex-auto"), - h1(cls("text-base font-semibold leading-6 text-gray-900"), title), - subtitle.map(st => p(cls("mt-2 text-sm text-gray-700"), st)) - ), - div(cls("mt-4 sm:ml-16 sm:mt-0 sm:flex-none"), actions) - ), - div( - cls("mt-8 flow-root"), - div( - cls("-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"), - div( - cls("inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"), - table - ) - ) - ) - ) - - override def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement = - table( - cls("min-w-full divide-y divide-gray-300"), - thead(header), - tbody(cls("divide-y divide-gray-200"), body) - ) - - override def headerRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-3.5 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-3.5 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-3.5 px-3")) - ) - ) - - override def dataRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - mods, - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-4 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-4 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-4 px-3")) - ) - ) - - override def headerCell(content: Modifier[HtmlElement]): HtmlElement = - th( - cls("text-left text-sm font-semibold text-gray-900"), - content - ) - - override def dataCell(content: Modifier[HtmlElement]): HtmlElement = - td( - cls("whitespace-nowrap text-sm text-gray-500"), - content - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala index b937d45..12d01ae 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala @@ -3,7 +3,7 @@ import com.raquo.laminar.api.L.{*, given} import works.iterative.ui.components.laminar.HtmlTabular import works.iterative.ui.model.tables.Tabular -import works.iterative.ui.components.laminar.TableComponentsModule +import works.iterative.ui.components.laminar.tailwind.ui.TableComponentsModule import works.iterative.core.UserMessage import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given import works.iterative.ui.components.tailwind.ComponentContext diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala new file mode 100644 index 0000000..87f3e8d --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala @@ -0,0 +1,54 @@ +package works.iterative.ui.components.laminar.tailwind.color + +import com.raquo.laminar.api.L.{*, given} + +/** Complete color definition that can be rendered to CSS. + * + * Includes the area, kind and weight of the color. + */ +case class Color(area: ColorArea, color: ColorDef): + def toCSS: String = s"${area.name}-${color.toCSS}" + +object Color: + import ColorDef.given + + given colorToCSS: Conversion[Color, HtmlMod] with + def apply(c: Color) = cls(c.toCSS) + + given colorToSVGCSS: Conversion[Color, SvgMod] with + def apply(c: Color) = svg.cls(c.toCSS) + + given colorSignalToCSS: Conversion[Signal[Color], HtmlMod] with + def apply(c: Signal[Color]) = cls <-- c.map(_.toCSS) + + given colorSignalToSVGCSS: Conversion[Signal[Color], SvgMod] with + def apply(c: Signal[Color]) = svg.cls <-- c.map(_.toCSS) + + def current = ColorDef(ColorKind.current) + def inherit = ColorDef(ColorKind.inherit) + def transp = ColorDef(ColorKind.transp) + def auto = ColorDef(ColorKind.auto) + def black = ColorDef(ColorKind.black) + def white = ColorDef(ColorKind.white) + def slate(weight: ColorWeight) = ColorDef(ColorKind.slate, weight) + def gray(weight: ColorWeight) = ColorDef(ColorKind.gray, weight) + def zinc(weight: ColorWeight) = ColorDef(ColorKind.zinc, weight) + def neutral(weight: ColorWeight) = ColorDef(ColorKind.neutral, weight) + def stone(weight: ColorWeight) = ColorDef(ColorKind.stone, weight) + def red(weight: ColorWeight) = ColorDef(ColorKind.red, weight) + def orange(weight: ColorWeight) = ColorDef(ColorKind.orange, weight) + def amber(weight: ColorWeight) = ColorDef(ColorKind.amber, weight) + def yellow(weight: ColorWeight) = ColorDef(ColorKind.yellow, weight) + def lime(weight: ColorWeight) = ColorDef(ColorKind.lime, weight) + def green(weight: ColorWeight) = ColorDef(ColorKind.green, weight) + def emerald(weight: ColorWeight) = ColorDef(ColorKind.emerald, weight) + def teal(weight: ColorWeight) = ColorDef(ColorKind.teal, weight) + def cyan(weight: ColorWeight) = ColorDef(ColorKind.cyan, weight) + def sky(weight: ColorWeight) = ColorDef(ColorKind.sky, weight) + def blue(weight: ColorWeight) = ColorDef(ColorKind.blue, weight) + def indigo(weight: ColorWeight) = ColorDef(ColorKind.indigo, weight) + def violet(weight: ColorWeight) = ColorDef(ColorKind.violet, weight) + def purple(weight: ColorWeight) = ColorDef(ColorKind.purple, weight) + def fuchsia(weight: ColorWeight) = ColorDef(ColorKind.fuchsia, weight) + def pink(weight: ColorWeight) = ColorDef(ColorKind.pink, weight) + def rose(weight: ColorWeight) = ColorDef(ColorKind.rose, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala new file mode 100644 index 0000000..4130840 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala @@ -0,0 +1,16 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines the area the color should apply to, eg. background, text, border, + * etc. + */ +enum ColorArea(val name: String): + case bg extends ColorArea("bg") + case text extends ColorArea("text") + case decoration extends ColorArea("decoration") + case border extends ColorArea("border") + case outline extends ColorArea("outline") + case divide extends ColorArea("divide") + case ring extends ColorArea("ring") + case ringOffset extends ColorArea("ring-offset") + case shadow extends ColorArea("shadow") + case accent extends ColorArea("accent") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala new file mode 100644 index 0000000..fab9404 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala @@ -0,0 +1,43 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** A combination of ColorKind and ColorWeight, if applicable. + * + * By applying area we get the full Color definition. + */ +sealed trait ColorDef: + def toCSS: String + + def bg = Color(ColorArea.bg, this) + def text = Color(ColorArea.text, this) + def decoration = Color(ColorArea.decoration, this) + def border = Color(ColorArea.border, this) + def outline = Color(ColorArea.outline, this) + def divide = Color(ColorArea.divide, this) + def ring = Color(ColorArea.ring, this) + def ringOffset = Color(ColorArea.ringOffset, this) + def shadow = Color(ColorArea.shadow, this) + def accent = Color(ColorArea.accent, this) + +// TODO: create a macro that will output the tailwind class as string directly during compilation, where possible. +object ColorDef: + case class WeightedColorDef( + kind: ColorKind, + weight: ColorWeight + ) extends ColorDef: + override def toCSS: String = s"${kind.name}-${weight.value}" + + case class UnweightedColorDef( + kind: ColorKind + ) extends ColorDef: + override def toCSS: String = kind.name + + // TODO: check that the kind is valid unweighted kind + // that means current, inherit, auto, transparent, black, white + // tried using implicit evidence, but the type inference for enumerations + // tends to generalize to the enum, instead of the real type + def apply[T <: ColorKind](kind: T)(using + ev: T <:< ColorKind.Unweighted + ): ColorDef = + UnweightedColorDef(kind) + def apply(kind: ColorKind, weight: ColorWeight): ColorDef = + WeightedColorDef(kind, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala new file mode 100644 index 0000000..865166b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala @@ -0,0 +1,47 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines what color should be used, without specifying the area or weight. + */ +sealed abstract class ColorKind private (val name: String): + def apply(weight: ColorWeight): ColorDef = + ColorDef.WeightedColorDef(this, weight) + +object ColorKind: + trait Unweighted: + self: ColorKind => + override def apply(weight: ColorWeight): ColorDef = + ColorDef.UnweightedColorDef(self) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case object current extends ColorKind("current") with Unweighted + case object inherit extends ColorKind("inherit") with Unweighted + // Not present in for all methods + case object transp extends ColorKind("transparent") with Unweighted + // Seen in accent, not preset otherwise + case object auto extends ColorKind("auto") with Unweighted + // Black and white do not have weight + case object black extends ColorKind("black") with Unweighted + case object white extends ColorKind("white") with Unweighted + case object slate extends ColorKind("slate") + case object gray extends ColorKind("gray") + case object zinc extends ColorKind("zinc") + case object neutral extends ColorKind("neutral") + case object stone extends ColorKind("stone") + case object red extends ColorKind("red") + case object orange extends ColorKind("orange") + case object amber extends ColorKind("amber") + case object yellow extends ColorKind("yellow") + case object lime extends ColorKind("lime") + case object green extends ColorKind("green") + case object emerald extends ColorKind("emerald") + case object teal extends ColorKind("teal") + case object cyan extends ColorKind("cyan") + case object sky extends ColorKind("sky") + case object blue extends ColorKind("blue") + case object indigo extends ColorKind("indigo") + case object violet extends ColorKind("violet") + case object purple extends ColorKind("purple") + case object fuchsia extends ColorKind("fuchsia") + case object pink extends ColorKind("pink") + case object rose extends ColorKind("rose") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala new file mode 100644 index 0000000..6f1a4cc --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala @@ -0,0 +1,33 @@ +package works.iterative.ui.components.laminar.tailwind.color + +opaque type ColorWeight = String + +extension (c: ColorWeight) def value: String = c + +/** Defines weight of a color, eg. 50, 100, 200, etc. + * + * Tailwind-like. + */ +object ColorWeight: + inline given int50: Conversion[50, ColorWeight] with + inline def apply(i: 50) = "50" + inline given int100: Conversion[100, ColorWeight] with + inline def apply(i: 100) = "100" + inline given int200: Conversion[200, ColorWeight] with + inline def apply(i: 200) = "200" + inline given int300: Conversion[300, ColorWeight] with + inline def apply(i: 300) = "300" + inline given int400: Conversion[400, ColorWeight] with + inline def apply(i: 400) = "400" + inline given int500: Conversion[500, ColorWeight] with + inline def apply(i: 500) = "500" + inline given int600: Conversion[600, ColorWeight] with + inline def apply(i: 600) = "600" + inline given int700: Conversion[700, ColorWeight] with + inline def apply(i: 700) = "700" + inline given int800: Conversion[800, ColorWeight] with + inline def apply(i: 800) = "800" + inline given int900: Conversion[900, ColorWeight] with + inline def apply(i: 900) = "900" + inline given int950: Conversion[950, ColorWeight] with + inline def apply(i: 950) = "950" diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala new file mode 100644 index 0000000..21e21f0 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala @@ -0,0 +1,12 @@ +package works.iterative.ui.components.laminar.tailwind + +/** We need a generic Color model that can be used both on server and on client. + * + * We have adopted the Tailwind model for now. There is nothing inherently + * Tailwind-specific in the implementation, but all the values are taken from + * their palette and the area model is very HTML biased. + * + * Still, I think that it is a good starting point for exploration and will + * satisfy our current needs. + */ +package object color {} diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" new file mode 100644 index 0000000..5c4e218 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" @@ -0,0 +1,21 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.model.color.ColorKind +import works.iterative.ui.components.tailwind.ComponentContext +import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given +import works.iterative.ui.model.Tag + +trait TailwindUIBadgesComponentsModule(using ComponentContext): + + object badges: + override def tag(name: String, color: ColorKind): HtmlElement = + p( + cls( + "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" + ), + color(800).text, + color(100).bg, + name + ) diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" new file mode 100644 index 0000000..0c9ca25 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" @@ -0,0 +1,24 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} + +trait TailwindUIHeadingsComponentsModule: + val headings: HeadingsComponents = new HeadingsComponents + + class HeadingsComponents: + def section(title: Node, actions: HtmlElement*): HtmlElement = + div( + cls("border-b border-gray-200 pb-5"), + div( + cls("sm:flex sm:items-center sm:justify-between"), + h3(cls("text-base font-semibold leading-6 text-gray-900"), title), + div( + cls("mt-3 flex sm:ml-4 sm:mt-0"), + actions match + case Nil => emptyMod + case first :: rest => + first :: rest.map(_.amend(cls("ml-3"))) + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala new file mode 100644 index 0000000..16ca01b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala @@ -0,0 +1,19 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.laminar.tailwind.color.ColorKind + +trait BadgeComponentsModule: + + object badges: + def pill(name: String, color: ColorKind): HtmlElement = + p( + cls( + "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" + ), + color(800).text, + color(100).bg, + name + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala new file mode 100644 index 0000000..ebb2968 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} + +trait ButtonComponentsModule: + + object buttons: + + private inline def srHelp(text: String): Modifier[HtmlElement] = + span(cls := "sr-only", text) + + def primaryButton( + id: String, + text: Modifier[HtmlElement], + icon: Option[SvgElement] = None, + buttonType: String = "submit" + )(mods: Modifier[HtmlElement]*): HtmlElement = + button( + tpe(buttonType), + cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + icon, + text, + mods + ) + + def secondaryButton( + id: String, + text: Modifier[HtmlElement], + icon: Option[SvgElement] = None, + buttonType: String = "button" + )(mods: Modifier[HtmlElement]*): HtmlElement = + button( + tpe(buttonType), + cls( + "ml-2 bg-white inline-flex justify-center 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" + ), + icon, + text, + mods + ) + + def inlineButton(id: String, icon: SvgElement)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + button( + tpe := "button", + cls := "ml-1 inline-flex h-4 w-4 flex-shrink-0 rounded-full p-1 text-gray-400 hover:bg-gray-200 hover:text-gray-500", + srHelp(id), + icon + ) + + def iconButton(id: String, icon: SvgElement, srText: Option[String] = None)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + button( + tpe := "button", + cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", + icon, + srText.map(srHelp(_)) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala new file mode 100644 index 0000000..eb6dd5f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala @@ -0,0 +1,101 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import org.scalajs.dom.html +import com.raquo.laminar.modifiers.KeyUpdater + +trait FormComponentsModule: + self: IconsModule => + + object forms: + + def section( + title: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] + )(content: Modifier[HtmlElement]*): HtmlElement = + div( + cls("space-y-6 sm:space-y-5"), + div( + h3(cls("text-lg leading-6 font-medium text-gray-900"), title), + subtitle.map(st => p(cls("mt-1 max-w-2xl text-sm text-gray-500"), st)) + ), + div(cls("mt-6 sm:mt-5 space-y-6 sm:space-y-5"), content) + ) + + def label(labelText: String, forId: Option[String] = None)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + L.label( + cls("block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"), + forId.map(id => L.forId(id)), + labelText, + mods + ) + + def field( + label: Modifier[HtmlElement] + )(content: Modifier[HtmlElement]*): 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, + div(cls("mt-1 sm:mt-0 sm:col-span-2"), content) + ) + + def field( + id: String, + labelText: String, + input: HtmlElement, + help: Option[String] + ): HtmlElement = + field( + label(labelText, Some(id))() + )( + input.amend(idAttr(id)), + help.map(h => p(cls("mt-2 text-sm text-gray-500"), h)) + ) + + def form(mods: Modifier[HtmlElement]*)( + sections: Modifier[HtmlElement]* + )(actions: Modifier[HtmlElement]*): HtmlElement = + L.form( + cls("space-y-8 divide-y divide-gray-200"), + mods, + sections, + div( + cls("pt-5"), + div(cls("flex justify-end"), actions) + ) + ) + + def inlineForm( + mods: Modifier[HtmlElement]* + ): HtmlElement = + L.form(cls("flex space-x-4"), mods) + + def inputField( + id: String, + labelText: String, + placeholderText: Option[String] = None, + inputType: String = "text", + helpText: Option[String] = None + ): HtmlElement = + field( + id, + labelText, + input( + tpe(inputType), + cls( + "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:max-w-xs sm:text-sm sm:leading-6" + ), + placeholderText.map(placeholder(_)) + ), + helpText + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala new file mode 100644 index 0000000..52da07e --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala @@ -0,0 +1,22 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} + +trait HeadingsComponentsModule: + object headings: + def section(title: Node, actions: HtmlElement*): HtmlElement = + div( + cls("border-b border-gray-200 pb-5"), + div( + cls("sm:flex sm:items-center sm:justify-between"), + h3(cls("text-base font-semibold leading-6 text-gray-900"), title), + div( + cls("mt-3 flex sm:ml-4 sm:mt-0"), + actions match + case Nil => emptyMod + case first :: rest => + first :: rest.map(_.amend(cls("ml-3"))) + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala new file mode 100644 index 0000000..c543150 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala @@ -0,0 +1,112 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext +import works.iterative.ui.components.tailwind.CustomAttrs + +trait IconsModule: + object icons: + import svg.* + + object aria: + val hidden = CustomAttrs.svg.ariaHidden + + private def withDefault( + mods: Seq[Modifier[SvgElement]], + default: Modifier[SvgElement] + ): Modifier[SvgElement] = + if mods.isEmpty then default else mods + + def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-8 w-8"), + 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" + ) + ) + ) + + def close(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-2 w-2"), + stroke := "currentColor", + fill := "none", + viewBox := "0 0 8 8", + path( + strokeLineCap := "round", + strokeWidth := "1.5", + d := "M1 1l6 6m0-6L1 7" + ) + ) + + def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls("h-5 w-5 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 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") + ) + ) + + def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls("h-5 w-5 text-gray-400")), + 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") + ) + ) + + def `document-chart-bar-outline`( + mods: Modifier[SvgElement]* + ): SvgElement = + svg( + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + strokeWidth := "1.5", + stroke := "currentColor", + withDefault(mods, cls := "h-6 w-6"), + path( + strokeLineCap := "round", + strokeLineJoin := "round", + d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" + ) + ) + + def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-5 w-5"), + cls := "flex-shrink-0 text-gray-400", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", + clipRule := "evenodd" + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala new file mode 100644 index 0000000..02cbdb6 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala @@ -0,0 +1,11 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait LayoutModule: + object layout: + def card(content: Modifier[HtmlElement]*): HtmlElement = + div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala new file mode 100644 index 0000000..b652da9 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala @@ -0,0 +1,88 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.laminar.nodes.ReactiveHtmlElement +import org.scalajs.dom.html.Paragraph +import com.raquo.laminar.nodes.TextNode +import works.iterative.ui.components.laminar.tailwind.color.ColorKind + +trait ListComponentsModule: + self: BadgeComponentsModule => + + object list: + def label( + text: String, + color: ColorKind + ): HtmlElement = badges.pill(text, color) + + def item( + title: String, + subtitle: Option[String], + right: Modifier[HtmlElement] = emptyMod, + avatar: Option[Modifier[HtmlElement]] = None + ): LI = + li( + cls("group"), + div( + cls( + "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" + ), + avatar.map(a => + div( + cls("flex-shrink-0"), + div( + cls( + "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" + ), + a + ) + ) + ), + div( + cls("flex-1 min-w-0"), + p( + cls("text-sm font-medium text-gray-900"), + title, + span(cls("float-right"), right) + ), + subtitle.map(st => + p( + cls("text-sm text-gray-500 truncate"), + st + ) + ) + ) + ) + ) + + def unordered( + children: Modifier[HtmlElement] + ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = + ul( + cls("relative z-0 divide-y divide-gray-200"), + role("list"), + children + ) + + def listSection( + header: String, + list: HtmlElement + ): Div = + div( + cls("relative"), + div( + cls( + "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" + ), + header + ), + list + ) + + def navigation(sections: Modifier[HtmlElement]): HtmlElement = + navTag( + cls("flex-1 min-h-0 overflow-y-auto"), + sections + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ModalComponentsModule.scala new file mode 100644 index 0000000..6027404 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ModalComponentsModule.scala @@ -0,0 +1,54 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait ModalComponentsModule: + object modal: + def modalDialog( + content: HtmlElement, + closeMod: Modifier[HtmlElement] + ): 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( + cls("fixed inset-0 transition-opacity"), + div(cls("absolute inset-0 bg-gray-500 opacity-75")), + closeMod + ) + ) + + div( + cls("fixed inset-0 z-20 overflow-y-auto"), + div( + cls("text-center sm:block sm:p-0"), + overlay, + browserCenteringModalTrick, + 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-7xl sm:align-middle" + ), + content + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala index a2c637c..bfa063a 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala @@ -1,13 +1,11 @@ package works.iterative.ui.components.laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorDef import com.raquo.laminar.tags.HtmlTag case class Bin[Source, +Value]( label: String, description: Option[String | HtmlElement], - color: ColorDef, valueOf: Source => Value ): def map[NewValue]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala deleted file mode 100644 index 3aac0bf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala +++ /dev/null @@ -1,34 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ButtonComponentsModule: - def buttons: ButtonComponents - - trait ButtonComponents: - def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala deleted file mode 100644 index 646e73d..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala +++ /dev/null @@ -1,191 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.color.ColorDef -import com.raquo.laminar.tags.HtmlTag -import org.scalajs.dom.html.UList -import com.raquo.laminar.nodes.ReactiveHtmlElement -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind - -trait DashboardComponentsModule: - def dashboard: DashboardComponents - - // Only methods that create HtmlElements - trait DashboardComponents(using ComponentContext): - def section(label: String, children: Modifier[HtmlElement]): Div - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI - def cardInitials( - initials: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div - def count(label: String, value: Int, color: ColorDef): Span - def counts(children: HtmlElement*): Span - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - -trait DefaultDashboardComponentsModule(using ComponentContext) - extends DashboardComponentsModule: - override val dashboard: DashboardComponents = new DashboardComponents: - def section(label: String, children: Modifier[HtmlElement]): Div = - div( - cls("mt-3"), - h2( - cls("text-gray-500 text-xs font-medium uppercase tracking-wide"), - label - ), - children - ) - - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] = - ul( - cls( - "mt-3 grid gap-5 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" - ), - children - ) - - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI = - li( - div( - cls("col-span-1 flex shadow-sm rounded-md"), - initials, - div( - cls( - "flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate" - ), - div( - cls("flex-1 px-4 py-2 text-sm truncate"), - div( - cls("text-gray-900 font-medium hover:text-gray-600 truncate"), - label - ), - p(cls("text-gray-500"), children) - ) - ) - ) - ) - - def cardInitials( - id: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div = - div( - cls( - "flex-shrink-0 flex flex-col items-center justify-center w-16 text-white text-sm font-medium rounded-l-md" - ), - color.map(_(600).bg), - div(id), - children - ) - - def count(label: String, value: Int, color: ColorDef): Span = - span(color.text, title(label), s"${value}") - - def counts(children: HtmlElement*): Span = - span(interleave(children, span(" / "))) - - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div = - div( - cls("flex flex-col mt-2"), - div( - cls( - "align-middle min-w-full shadow sm:rounded-lg" - ), - L.table( - cls("min-w-full border-separate border-spacing-0"), - styleAttr("border-spacing: 0"), // Tailwind somehow doesn't work - thead( - tr( - cls("border-t border-gray-200"), - headers.map(h => - th( - cls( - "sticky top-0 z-10 px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - h - ) - ) - ) - ), - tbody(cls("bg-white divide-y divide-gray-100"), sections) - ) - ) - ) - - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - td( - cls("bg-gray-50 border-gray-200 border-t border-b"), - cls( - "sticky top-14 z-10 bg-gray-50 px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - colSpan(cs), - label - ), - mods - ) - - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - mods, - td( - cls( - "px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900 truncate" - ), - label - ), - children.map(e => - td( - cls( - "whitespace-nowrap px-2 py-2 text-sm text-gray-500" - ), - e - ) - ) - ) - - private def interleave( - a: Seq[HtmlElement], - separator: => HtmlElement - ): Seq[HtmlElement] = - if a.size < 2 then a - else a.init.flatMap(n => Seq(n, separator)) :+ a.last diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala deleted file mode 100644 index a45beaf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.FileRef - -trait DetailComponentsModule: - - def details: DetailComponents - - trait DetailComponents: - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement - - def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement - - def files(fs: List[FileRef]): HtmlElement - - def file(f: FileRef): HtmlElement - -trait DefaultDetailComponentsModule(using ctx: ComponentContext) - extends DetailComponentsModule: - self: IconsModule => - - override val details: DetailComponents = new DetailComponents: - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement = - div( - div( - cls := "px-4 sm:px-0", - h3( - cls := "text-base font-semibold leading-7 text-gray-900", - title - ), - subtitle.map(st => - p( - cls := "mt-1 max-w-2xl text-sm leading-6 text-gray-500", - st - ) - ) - // TODO: actions - ), - div( - cls := "mt-6 border-t border-gray-100", - dl( - cls := "divide-y divide-gray-100", - fields - ) - ) - ) - - override def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0", - dt( - cls := "text-sm font-medium leading-6 text-gray-900", - title - ), - dd( - cls := "mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0", - content - ) - ) - - override def files(fs: List[FileRef]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-100 rounded-md border border-gray-200", - fs.map(file) - ) - - override def file(f: FileRef): HtmlElement = - li( - cls := "flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6", - div( - cls := "flex w-0 flex-1 items-center", - icons.`paper-clip-solid`(), - div( - cls := "ml-4 flex min-w-0 flex-1 gap-2", - span( - cls := "truncate font-medium", - f.name - ), - f.size.map(size => span(cls := "flex-shrink-0 text-gray-400", size)) - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - "Uložit" - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala new file mode 100644 index 0000000..f6c583f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait FormComponents(using ctx: ComponentContext) extends LocalDateSelectModule: + def searchIcon: SvgElement + + def renderLocalDateSelect( + id: String, + labelText: Option[String], + placeholderText: Option[String], + mods: LocalDateSelect => Modifier[HtmlElement] + ): HtmlElement = + div( + labelText, + input( + idAttr(id), + nameAttr(id), + autoComplete("date"), + tpe("date"), + cls( + "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ), + placeholderText.map(placeholder(_)), + mods(localDateSelect) + ) + ) + + def searchField( + id: String, + placeholderText: Option[String] = None + )( + mods: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls := "flex-1 min-w-0", + label(ctx.messages("forms.search.label"), forId(id), cls("sr-only")), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + searchIcon + ), + input( + tpe := "search", + nameAttr := "search", + idAttr := id, + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholderText + .orElse( + ctx.messages + .opt( + s"forms.search.${id}.placeholder", + s"form.search.placeholder" + ) + ) + .map(placeholder(_)), + mods + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala deleted file mode 100644 index 7714490..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala +++ /dev/null @@ -1,82 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -/** Form components - * - * A form is a collection of sections, each of which contains a collection of - * fields. - * - * Form -> * Section -> * Field - * -> * Action - * - * Each Field has an `id`, `label` and `input` element. The `id` is used to - * link the label to the input. - * - * The `input` element can be a simple text input, or a more complex component - * such as a date picker. The caller is expected to make the input work. - */ -trait FormComponentsModule extends LocalDateSelectModule: - def forms: FormComponents - - trait FormComponents: - - /** Layout the sections and actions of a form */ - def form(mods: Modifier[HtmlElement]*)(sections: Modifier[HtmlElement]*)( - actions: Modifier[HtmlElement]* - ): HtmlElement - - /** Section layout, with a title, optional subtitle and any content. */ - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label and any content */ - def field(label: Modifier[HtmlElement])( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label, input and optional help text */ - def field( - id: String, - label: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement - - def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout an inline form element */ - def inlineForm(mods: Modifier[HtmlElement]*): HtmlElement - - /** An input field with a search icon */ - def searchField(id: String, placeholderText: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement - - /** LocalDate input */ - def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala deleted file mode 100644 index 8acbe7f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala +++ /dev/null @@ -1,28 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.Tag - -trait GenericComponentsModule: - - def generic: GenericComponents - - trait GenericComponents: - def tag(name: String, color: ColorKind): HtmlElement - def tag(t: Tag): HtmlElement = tag(t.value, t.color) - -trait DefaultGenericComponentsModule(using ComponentContext) - extends GenericComponentsModule: - override val generic: GenericComponents = new GenericComponents: - override def tag(name: String, color: ColorKind): HtmlElement = - p( - cls( - "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" - ), - color(800).text, - color(100).bg, - name - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala deleted file mode 100644 index 8cfc1d9..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala +++ /dev/null @@ -1,123 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.CustomAttrs - -trait IconsModule: - def icons: Icons - - trait Icons: - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement - def close(mods: Modifier[SvgElement]*): SvgElement - def `search-solid`(mods: Modifier[SvgElement]*): SvgElement - def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement - def `document-chart-bar-outline`(mods: Modifier[SvgElement]*): SvgElement - def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement - -trait DefaultIconsModule(using ComponentContext) extends IconsModule: - override val icons: Icons = new Icons: - import svg.* - - object aria: - val hidden = CustomAttrs.svg.ariaHidden - - private def withDefault( - mods: Seq[Modifier[SvgElement]], - default: Modifier[SvgElement] - ): Modifier[SvgElement] = - if mods.isEmpty then default else mods - - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-8 w-8"), - 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" - ) - ) - ) - - override def close(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-2 w-2"), - stroke := "currentColor", - fill := "none", - viewBox := "0 0 8 8", - path( - strokeLineCap := "round", - strokeWidth := "1.5", - d := "M1 1l6 6m0-6L1 7" - ) - ) - - override def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 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 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") - ) - ) - - override def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 text-gray-400")), - 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") - ) - ) - - override def `document-chart-bar-outline`( - mods: Modifier[SvgElement]* - ): SvgElement = - svg( - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - strokeWidth := "1.5", - stroke := "currentColor", - withDefault(mods, cls := "h-6 w-6"), - path( - strokeLineCap := "round", - strokeLineJoin := "round", - d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" - ) - ) - - override def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-5 w-5"), - cls := "flex-shrink-0 text-gray-400", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", - clipRule := "evenodd" - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala deleted file mode 100644 index 95ab654..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait LayoutModule: - def layout: LayoutComponents - - trait LayoutComponents: - def card(content: Modifier[HtmlElement]*): HtmlElement - -trait DefaultLayoutModule(using ctx: ComponentContext) extends LayoutModule: - override val layout: LayoutComponents = new LayoutComponents: - override def card(content: Modifier[HtmlElement]*): HtmlElement = - div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala deleted file mode 100644 index 5946974..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom.html.Paragraph -import com.raquo.laminar.nodes.TextNode - -trait ListComponentsModule: - - def list: ListComponents - - trait ListComponents(using ComponentContext): - def label(text: String, color: ColorKind): HtmlElement - def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI - def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] - def listSection(header: String, list: HtmlElement): Div - def navigation(sections: Modifier[HtmlElement]): HtmlElement - -trait DefaultListComponentsModule(using ComponentContext) - extends ListComponentsModule: - self: GenericComponentsModule => - - override val list: ListComponents = new ListComponents: - override def label( - text: String, - color: ColorKind - ): HtmlElement = generic.tag(text, color) - - override def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI = - li( - cls("group"), - div( - cls( - "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" - ), - avatar.map(a => - div( - cls("flex-shrink-0"), - div( - cls( - "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" - ), - a - ) - ) - ), - div( - cls("flex-1 min-w-0"), - p( - cls("text-sm font-medium text-gray-900"), - title, - span(cls("float-right"), right) - ), - subtitle.map(st => - p( - cls("text-sm text-gray-500 truncate"), - st - ) - ) - ) - ) - ) - - override def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = - ul( - cls("relative z-0 divide-y divide-gray-200"), - role("list"), - children - ) - - override def listSection( - header: String, - list: HtmlElement - ): Div = - div( - cls("relative"), - div( - cls( - "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" - ), - header - ), - list - ) - - override def navigation(sections: Modifier[HtmlElement]): HtmlElement = - navTag( - cls("flex-1 min-h-0 overflow-y-auto"), - sections - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala deleted file mode 100644 index 5b5490f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ModalComponentsModule: - - def modal: ModalComponents - - trait ModalComponents: - def modalDialog( - content: HtmlElement, - onClose: Modifier[HtmlElement] - ): HtmlElement - -trait DefaultModalComponentsModule(using ctx: ComponentContext) - extends ModalComponentsModule: - - override val modal: ModalComponents = new ModalComponents: - - override def modalDialog( - content: HtmlElement, - closeMod: Modifier[HtmlElement] - ): 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( - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")), - closeMod - ) - ) - - div( - cls("fixed inset-0 z-20 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - 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-7xl sm:align-middle" - ), - content - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala deleted file mode 100644 index b713d8a..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala +++ /dev/null @@ -1,71 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait PageComponentsModule: - - val page: PageComponents - - trait PageComponents: - def container( - children: Modifier[HtmlElement]* - ): HtmlElement - - def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement - - def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement] = emptyMod, - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement - - /** Visage for clickable text, like links - */ - def clickable: Modifier[HtmlElement] - -trait DefaultPageComponentsModule(using ComponentContext) - extends PageComponentsModule: - - override val page: PageComponents = new PageComponents: - override def container( - children: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), - children - ) - - override def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement = - div( - cls("p-8 bg-gray-100 h-full"), - header, - children - ) - - override def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement = - div( - cls("pb-5 border-b border-gray-200"), - div(cls("float-right"), right), - h1( - cls("text-2xl leading-6 font-medium text-gray-900"), - title - ), - subtitle.map( - p( - cls("text-sm font-medium text-gray-500"), - _ - ) - ) - ) - - override def clickable: Modifier[HtmlElement] = - cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala deleted file mode 100644 index f70ee87..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala +++ /dev/null @@ -1,108 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} - -trait TableComponentsModule: - def tables: TableComponents - - trait TableComponents: - def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )(table: Modifier[HtmlElement]*): HtmlElement - - def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement - - def headerRow(mods: Modifier[HtmlElement]*)( - cells: HtmlElement* - ): HtmlElement - - def dataRow(mods: Modifier[HtmlElement]*)(cells: HtmlElement*): HtmlElement - - def headerCell(content: Modifier[HtmlElement]): HtmlElement - - def dataCell(content: Modifier[HtmlElement]): HtmlElement - -trait DefaultTableComponentsModule extends TableComponentsModule: - - override lazy val tables: TableComponents = new TableComponents: - - override def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )( - table: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("px-4 sm:px-6 lg:px-8"), - div( - cls("sm:flex sm:items-center"), - div( - cls("sm:flex-auto"), - h1(cls("text-base font-semibold leading-6 text-gray-900"), title), - subtitle.map(st => p(cls("mt-2 text-sm text-gray-700"), st)) - ), - div(cls("mt-4 sm:ml-16 sm:mt-0 sm:flex-none"), actions) - ), - div( - cls("mt-8 flow-root"), - div( - cls("-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"), - div( - cls("inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"), - table - ) - ) - ) - ) - - override def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement = - table( - cls("min-w-full divide-y divide-gray-300"), - thead(header), - tbody(cls("divide-y divide-gray-200"), body) - ) - - override def headerRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-3.5 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-3.5 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-3.5 px-3")) - ) - ) - - override def dataRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - mods, - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-4 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-4 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-4 px-3")) - ) - ) - - override def headerCell(content: Modifier[HtmlElement]): HtmlElement = - th( - cls("text-left text-sm font-semibold text-gray-900"), - content - ) - - override def dataCell(content: Modifier[HtmlElement]): HtmlElement = - td( - cls("whitespace-nowrap text-sm text-gray-500"), - content - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala index b937d45..12d01ae 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala @@ -3,7 +3,7 @@ import com.raquo.laminar.api.L.{*, given} import works.iterative.ui.components.laminar.HtmlTabular import works.iterative.ui.model.tables.Tabular -import works.iterative.ui.components.laminar.TableComponentsModule +import works.iterative.ui.components.laminar.tailwind.ui.TableComponentsModule import works.iterative.core.UserMessage import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given import works.iterative.ui.components.tailwind.ComponentContext diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala new file mode 100644 index 0000000..87f3e8d --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala @@ -0,0 +1,54 @@ +package works.iterative.ui.components.laminar.tailwind.color + +import com.raquo.laminar.api.L.{*, given} + +/** Complete color definition that can be rendered to CSS. + * + * Includes the area, kind and weight of the color. + */ +case class Color(area: ColorArea, color: ColorDef): + def toCSS: String = s"${area.name}-${color.toCSS}" + +object Color: + import ColorDef.given + + given colorToCSS: Conversion[Color, HtmlMod] with + def apply(c: Color) = cls(c.toCSS) + + given colorToSVGCSS: Conversion[Color, SvgMod] with + def apply(c: Color) = svg.cls(c.toCSS) + + given colorSignalToCSS: Conversion[Signal[Color], HtmlMod] with + def apply(c: Signal[Color]) = cls <-- c.map(_.toCSS) + + given colorSignalToSVGCSS: Conversion[Signal[Color], SvgMod] with + def apply(c: Signal[Color]) = svg.cls <-- c.map(_.toCSS) + + def current = ColorDef(ColorKind.current) + def inherit = ColorDef(ColorKind.inherit) + def transp = ColorDef(ColorKind.transp) + def auto = ColorDef(ColorKind.auto) + def black = ColorDef(ColorKind.black) + def white = ColorDef(ColorKind.white) + def slate(weight: ColorWeight) = ColorDef(ColorKind.slate, weight) + def gray(weight: ColorWeight) = ColorDef(ColorKind.gray, weight) + def zinc(weight: ColorWeight) = ColorDef(ColorKind.zinc, weight) + def neutral(weight: ColorWeight) = ColorDef(ColorKind.neutral, weight) + def stone(weight: ColorWeight) = ColorDef(ColorKind.stone, weight) + def red(weight: ColorWeight) = ColorDef(ColorKind.red, weight) + def orange(weight: ColorWeight) = ColorDef(ColorKind.orange, weight) + def amber(weight: ColorWeight) = ColorDef(ColorKind.amber, weight) + def yellow(weight: ColorWeight) = ColorDef(ColorKind.yellow, weight) + def lime(weight: ColorWeight) = ColorDef(ColorKind.lime, weight) + def green(weight: ColorWeight) = ColorDef(ColorKind.green, weight) + def emerald(weight: ColorWeight) = ColorDef(ColorKind.emerald, weight) + def teal(weight: ColorWeight) = ColorDef(ColorKind.teal, weight) + def cyan(weight: ColorWeight) = ColorDef(ColorKind.cyan, weight) + def sky(weight: ColorWeight) = ColorDef(ColorKind.sky, weight) + def blue(weight: ColorWeight) = ColorDef(ColorKind.blue, weight) + def indigo(weight: ColorWeight) = ColorDef(ColorKind.indigo, weight) + def violet(weight: ColorWeight) = ColorDef(ColorKind.violet, weight) + def purple(weight: ColorWeight) = ColorDef(ColorKind.purple, weight) + def fuchsia(weight: ColorWeight) = ColorDef(ColorKind.fuchsia, weight) + def pink(weight: ColorWeight) = ColorDef(ColorKind.pink, weight) + def rose(weight: ColorWeight) = ColorDef(ColorKind.rose, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala new file mode 100644 index 0000000..4130840 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala @@ -0,0 +1,16 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines the area the color should apply to, eg. background, text, border, + * etc. + */ +enum ColorArea(val name: String): + case bg extends ColorArea("bg") + case text extends ColorArea("text") + case decoration extends ColorArea("decoration") + case border extends ColorArea("border") + case outline extends ColorArea("outline") + case divide extends ColorArea("divide") + case ring extends ColorArea("ring") + case ringOffset extends ColorArea("ring-offset") + case shadow extends ColorArea("shadow") + case accent extends ColorArea("accent") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala new file mode 100644 index 0000000..fab9404 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala @@ -0,0 +1,43 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** A combination of ColorKind and ColorWeight, if applicable. + * + * By applying area we get the full Color definition. + */ +sealed trait ColorDef: + def toCSS: String + + def bg = Color(ColorArea.bg, this) + def text = Color(ColorArea.text, this) + def decoration = Color(ColorArea.decoration, this) + def border = Color(ColorArea.border, this) + def outline = Color(ColorArea.outline, this) + def divide = Color(ColorArea.divide, this) + def ring = Color(ColorArea.ring, this) + def ringOffset = Color(ColorArea.ringOffset, this) + def shadow = Color(ColorArea.shadow, this) + def accent = Color(ColorArea.accent, this) + +// TODO: create a macro that will output the tailwind class as string directly during compilation, where possible. +object ColorDef: + case class WeightedColorDef( + kind: ColorKind, + weight: ColorWeight + ) extends ColorDef: + override def toCSS: String = s"${kind.name}-${weight.value}" + + case class UnweightedColorDef( + kind: ColorKind + ) extends ColorDef: + override def toCSS: String = kind.name + + // TODO: check that the kind is valid unweighted kind + // that means current, inherit, auto, transparent, black, white + // tried using implicit evidence, but the type inference for enumerations + // tends to generalize to the enum, instead of the real type + def apply[T <: ColorKind](kind: T)(using + ev: T <:< ColorKind.Unweighted + ): ColorDef = + UnweightedColorDef(kind) + def apply(kind: ColorKind, weight: ColorWeight): ColorDef = + WeightedColorDef(kind, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala new file mode 100644 index 0000000..865166b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala @@ -0,0 +1,47 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines what color should be used, without specifying the area or weight. + */ +sealed abstract class ColorKind private (val name: String): + def apply(weight: ColorWeight): ColorDef = + ColorDef.WeightedColorDef(this, weight) + +object ColorKind: + trait Unweighted: + self: ColorKind => + override def apply(weight: ColorWeight): ColorDef = + ColorDef.UnweightedColorDef(self) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case object current extends ColorKind("current") with Unweighted + case object inherit extends ColorKind("inherit") with Unweighted + // Not present in for all methods + case object transp extends ColorKind("transparent") with Unweighted + // Seen in accent, not preset otherwise + case object auto extends ColorKind("auto") with Unweighted + // Black and white do not have weight + case object black extends ColorKind("black") with Unweighted + case object white extends ColorKind("white") with Unweighted + case object slate extends ColorKind("slate") + case object gray extends ColorKind("gray") + case object zinc extends ColorKind("zinc") + case object neutral extends ColorKind("neutral") + case object stone extends ColorKind("stone") + case object red extends ColorKind("red") + case object orange extends ColorKind("orange") + case object amber extends ColorKind("amber") + case object yellow extends ColorKind("yellow") + case object lime extends ColorKind("lime") + case object green extends ColorKind("green") + case object emerald extends ColorKind("emerald") + case object teal extends ColorKind("teal") + case object cyan extends ColorKind("cyan") + case object sky extends ColorKind("sky") + case object blue extends ColorKind("blue") + case object indigo extends ColorKind("indigo") + case object violet extends ColorKind("violet") + case object purple extends ColorKind("purple") + case object fuchsia extends ColorKind("fuchsia") + case object pink extends ColorKind("pink") + case object rose extends ColorKind("rose") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala new file mode 100644 index 0000000..6f1a4cc --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala @@ -0,0 +1,33 @@ +package works.iterative.ui.components.laminar.tailwind.color + +opaque type ColorWeight = String + +extension (c: ColorWeight) def value: String = c + +/** Defines weight of a color, eg. 50, 100, 200, etc. + * + * Tailwind-like. + */ +object ColorWeight: + inline given int50: Conversion[50, ColorWeight] with + inline def apply(i: 50) = "50" + inline given int100: Conversion[100, ColorWeight] with + inline def apply(i: 100) = "100" + inline given int200: Conversion[200, ColorWeight] with + inline def apply(i: 200) = "200" + inline given int300: Conversion[300, ColorWeight] with + inline def apply(i: 300) = "300" + inline given int400: Conversion[400, ColorWeight] with + inline def apply(i: 400) = "400" + inline given int500: Conversion[500, ColorWeight] with + inline def apply(i: 500) = "500" + inline given int600: Conversion[600, ColorWeight] with + inline def apply(i: 600) = "600" + inline given int700: Conversion[700, ColorWeight] with + inline def apply(i: 700) = "700" + inline given int800: Conversion[800, ColorWeight] with + inline def apply(i: 800) = "800" + inline given int900: Conversion[900, ColorWeight] with + inline def apply(i: 900) = "900" + inline given int950: Conversion[950, ColorWeight] with + inline def apply(i: 950) = "950" diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala new file mode 100644 index 0000000..21e21f0 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala @@ -0,0 +1,12 @@ +package works.iterative.ui.components.laminar.tailwind + +/** We need a generic Color model that can be used both on server and on client. + * + * We have adopted the Tailwind model for now. There is nothing inherently + * Tailwind-specific in the implementation, but all the values are taken from + * their palette and the area model is very HTML biased. + * + * Still, I think that it is a good starting point for exploration and will + * satisfy our current needs. + */ +package object color {} diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" new file mode 100644 index 0000000..5c4e218 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" @@ -0,0 +1,21 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.model.color.ColorKind +import works.iterative.ui.components.tailwind.ComponentContext +import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given +import works.iterative.ui.model.Tag + +trait TailwindUIBadgesComponentsModule(using ComponentContext): + + object badges: + override def tag(name: String, color: ColorKind): HtmlElement = + p( + cls( + "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" + ), + color(800).text, + color(100).bg, + name + ) diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" new file mode 100644 index 0000000..0c9ca25 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" @@ -0,0 +1,24 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} + +trait TailwindUIHeadingsComponentsModule: + val headings: HeadingsComponents = new HeadingsComponents + + class HeadingsComponents: + def section(title: Node, actions: HtmlElement*): HtmlElement = + div( + cls("border-b border-gray-200 pb-5"), + div( + cls("sm:flex sm:items-center sm:justify-between"), + h3(cls("text-base font-semibold leading-6 text-gray-900"), title), + div( + cls("mt-3 flex sm:ml-4 sm:mt-0"), + actions match + case Nil => emptyMod + case first :: rest => + first :: rest.map(_.amend(cls("ml-3"))) + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala new file mode 100644 index 0000000..16ca01b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala @@ -0,0 +1,19 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.laminar.tailwind.color.ColorKind + +trait BadgeComponentsModule: + + object badges: + def pill(name: String, color: ColorKind): HtmlElement = + p( + cls( + "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" + ), + color(800).text, + color(100).bg, + name + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala new file mode 100644 index 0000000..ebb2968 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} + +trait ButtonComponentsModule: + + object buttons: + + private inline def srHelp(text: String): Modifier[HtmlElement] = + span(cls := "sr-only", text) + + def primaryButton( + id: String, + text: Modifier[HtmlElement], + icon: Option[SvgElement] = None, + buttonType: String = "submit" + )(mods: Modifier[HtmlElement]*): HtmlElement = + button( + tpe(buttonType), + cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + icon, + text, + mods + ) + + def secondaryButton( + id: String, + text: Modifier[HtmlElement], + icon: Option[SvgElement] = None, + buttonType: String = "button" + )(mods: Modifier[HtmlElement]*): HtmlElement = + button( + tpe(buttonType), + cls( + "ml-2 bg-white inline-flex justify-center 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" + ), + icon, + text, + mods + ) + + def inlineButton(id: String, icon: SvgElement)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + button( + tpe := "button", + cls := "ml-1 inline-flex h-4 w-4 flex-shrink-0 rounded-full p-1 text-gray-400 hover:bg-gray-200 hover:text-gray-500", + srHelp(id), + icon + ) + + def iconButton(id: String, icon: SvgElement, srText: Option[String] = None)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + button( + tpe := "button", + cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", + icon, + srText.map(srHelp(_)) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala new file mode 100644 index 0000000..eb6dd5f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala @@ -0,0 +1,101 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import org.scalajs.dom.html +import com.raquo.laminar.modifiers.KeyUpdater + +trait FormComponentsModule: + self: IconsModule => + + object forms: + + def section( + title: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] + )(content: Modifier[HtmlElement]*): HtmlElement = + div( + cls("space-y-6 sm:space-y-5"), + div( + h3(cls("text-lg leading-6 font-medium text-gray-900"), title), + subtitle.map(st => p(cls("mt-1 max-w-2xl text-sm text-gray-500"), st)) + ), + div(cls("mt-6 sm:mt-5 space-y-6 sm:space-y-5"), content) + ) + + def label(labelText: String, forId: Option[String] = None)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + L.label( + cls("block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"), + forId.map(id => L.forId(id)), + labelText, + mods + ) + + def field( + label: Modifier[HtmlElement] + )(content: Modifier[HtmlElement]*): 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, + div(cls("mt-1 sm:mt-0 sm:col-span-2"), content) + ) + + def field( + id: String, + labelText: String, + input: HtmlElement, + help: Option[String] + ): HtmlElement = + field( + label(labelText, Some(id))() + )( + input.amend(idAttr(id)), + help.map(h => p(cls("mt-2 text-sm text-gray-500"), h)) + ) + + def form(mods: Modifier[HtmlElement]*)( + sections: Modifier[HtmlElement]* + )(actions: Modifier[HtmlElement]*): HtmlElement = + L.form( + cls("space-y-8 divide-y divide-gray-200"), + mods, + sections, + div( + cls("pt-5"), + div(cls("flex justify-end"), actions) + ) + ) + + def inlineForm( + mods: Modifier[HtmlElement]* + ): HtmlElement = + L.form(cls("flex space-x-4"), mods) + + def inputField( + id: String, + labelText: String, + placeholderText: Option[String] = None, + inputType: String = "text", + helpText: Option[String] = None + ): HtmlElement = + field( + id, + labelText, + input( + tpe(inputType), + cls( + "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:max-w-xs sm:text-sm sm:leading-6" + ), + placeholderText.map(placeholder(_)) + ), + helpText + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala new file mode 100644 index 0000000..52da07e --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala @@ -0,0 +1,22 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} + +trait HeadingsComponentsModule: + object headings: + def section(title: Node, actions: HtmlElement*): HtmlElement = + div( + cls("border-b border-gray-200 pb-5"), + div( + cls("sm:flex sm:items-center sm:justify-between"), + h3(cls("text-base font-semibold leading-6 text-gray-900"), title), + div( + cls("mt-3 flex sm:ml-4 sm:mt-0"), + actions match + case Nil => emptyMod + case first :: rest => + first :: rest.map(_.amend(cls("ml-3"))) + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala new file mode 100644 index 0000000..c543150 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala @@ -0,0 +1,112 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext +import works.iterative.ui.components.tailwind.CustomAttrs + +trait IconsModule: + object icons: + import svg.* + + object aria: + val hidden = CustomAttrs.svg.ariaHidden + + private def withDefault( + mods: Seq[Modifier[SvgElement]], + default: Modifier[SvgElement] + ): Modifier[SvgElement] = + if mods.isEmpty then default else mods + + def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-8 w-8"), + 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" + ) + ) + ) + + def close(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-2 w-2"), + stroke := "currentColor", + fill := "none", + viewBox := "0 0 8 8", + path( + strokeLineCap := "round", + strokeWidth := "1.5", + d := "M1 1l6 6m0-6L1 7" + ) + ) + + def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls("h-5 w-5 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 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") + ) + ) + + def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls("h-5 w-5 text-gray-400")), + 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") + ) + ) + + def `document-chart-bar-outline`( + mods: Modifier[SvgElement]* + ): SvgElement = + svg( + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + strokeWidth := "1.5", + stroke := "currentColor", + withDefault(mods, cls := "h-6 w-6"), + path( + strokeLineCap := "round", + strokeLineJoin := "round", + d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" + ) + ) + + def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-5 w-5"), + cls := "flex-shrink-0 text-gray-400", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", + clipRule := "evenodd" + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala new file mode 100644 index 0000000..02cbdb6 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala @@ -0,0 +1,11 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait LayoutModule: + object layout: + def card(content: Modifier[HtmlElement]*): HtmlElement = + div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala new file mode 100644 index 0000000..b652da9 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala @@ -0,0 +1,88 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.laminar.nodes.ReactiveHtmlElement +import org.scalajs.dom.html.Paragraph +import com.raquo.laminar.nodes.TextNode +import works.iterative.ui.components.laminar.tailwind.color.ColorKind + +trait ListComponentsModule: + self: BadgeComponentsModule => + + object list: + def label( + text: String, + color: ColorKind + ): HtmlElement = badges.pill(text, color) + + def item( + title: String, + subtitle: Option[String], + right: Modifier[HtmlElement] = emptyMod, + avatar: Option[Modifier[HtmlElement]] = None + ): LI = + li( + cls("group"), + div( + cls( + "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" + ), + avatar.map(a => + div( + cls("flex-shrink-0"), + div( + cls( + "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" + ), + a + ) + ) + ), + div( + cls("flex-1 min-w-0"), + p( + cls("text-sm font-medium text-gray-900"), + title, + span(cls("float-right"), right) + ), + subtitle.map(st => + p( + cls("text-sm text-gray-500 truncate"), + st + ) + ) + ) + ) + ) + + def unordered( + children: Modifier[HtmlElement] + ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = + ul( + cls("relative z-0 divide-y divide-gray-200"), + role("list"), + children + ) + + def listSection( + header: String, + list: HtmlElement + ): Div = + div( + cls("relative"), + div( + cls( + "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" + ), + header + ), + list + ) + + def navigation(sections: Modifier[HtmlElement]): HtmlElement = + navTag( + cls("flex-1 min-h-0 overflow-y-auto"), + sections + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ModalComponentsModule.scala new file mode 100644 index 0000000..6027404 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ModalComponentsModule.scala @@ -0,0 +1,54 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait ModalComponentsModule: + object modal: + def modalDialog( + content: HtmlElement, + closeMod: Modifier[HtmlElement] + ): 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( + cls("fixed inset-0 transition-opacity"), + div(cls("absolute inset-0 bg-gray-500 opacity-75")), + closeMod + ) + ) + + div( + cls("fixed inset-0 z-20 overflow-y-auto"), + div( + cls("text-center sm:block sm:p-0"), + overlay, + browserCenteringModalTrick, + 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-7xl sm:align-middle" + ), + content + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/PageComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/PageComponentsModule.scala new file mode 100644 index 0000000..d5a422e --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/PageComponentsModule.scala @@ -0,0 +1,49 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait PageComponentsModule: + + object page: + def container( + children: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), + children + ) + + def singleColumn( + header: Modifier[HtmlElement] + )(children: Modifier[HtmlElement]*): HtmlElement = + div( + cls("p-8 bg-gray-100 h-full"), + header, + children + ) + + def pageHeader( + title: Modifier[HtmlElement], + right: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] = None + ): HtmlElement = + div( + cls("pb-5 border-b border-gray-200"), + div(cls("float-right"), right), + h1( + cls("text-2xl leading-6 font-medium text-gray-900"), + title + ), + subtitle.map( + p( + cls("text-sm font-medium text-gray-500"), + _ + ) + ) + ) + + def clickable: Modifier[HtmlElement] = + cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala index a2c637c..bfa063a 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala @@ -1,13 +1,11 @@ package works.iterative.ui.components.laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorDef import com.raquo.laminar.tags.HtmlTag case class Bin[Source, +Value]( label: String, description: Option[String | HtmlElement], - color: ColorDef, valueOf: Source => Value ): def map[NewValue]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala deleted file mode 100644 index 3aac0bf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala +++ /dev/null @@ -1,34 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ButtonComponentsModule: - def buttons: ButtonComponents - - trait ButtonComponents: - def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala deleted file mode 100644 index 646e73d..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala +++ /dev/null @@ -1,191 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.color.ColorDef -import com.raquo.laminar.tags.HtmlTag -import org.scalajs.dom.html.UList -import com.raquo.laminar.nodes.ReactiveHtmlElement -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind - -trait DashboardComponentsModule: - def dashboard: DashboardComponents - - // Only methods that create HtmlElements - trait DashboardComponents(using ComponentContext): - def section(label: String, children: Modifier[HtmlElement]): Div - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI - def cardInitials( - initials: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div - def count(label: String, value: Int, color: ColorDef): Span - def counts(children: HtmlElement*): Span - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - -trait DefaultDashboardComponentsModule(using ComponentContext) - extends DashboardComponentsModule: - override val dashboard: DashboardComponents = new DashboardComponents: - def section(label: String, children: Modifier[HtmlElement]): Div = - div( - cls("mt-3"), - h2( - cls("text-gray-500 text-xs font-medium uppercase tracking-wide"), - label - ), - children - ) - - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] = - ul( - cls( - "mt-3 grid gap-5 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" - ), - children - ) - - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI = - li( - div( - cls("col-span-1 flex shadow-sm rounded-md"), - initials, - div( - cls( - "flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate" - ), - div( - cls("flex-1 px-4 py-2 text-sm truncate"), - div( - cls("text-gray-900 font-medium hover:text-gray-600 truncate"), - label - ), - p(cls("text-gray-500"), children) - ) - ) - ) - ) - - def cardInitials( - id: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div = - div( - cls( - "flex-shrink-0 flex flex-col items-center justify-center w-16 text-white text-sm font-medium rounded-l-md" - ), - color.map(_(600).bg), - div(id), - children - ) - - def count(label: String, value: Int, color: ColorDef): Span = - span(color.text, title(label), s"${value}") - - def counts(children: HtmlElement*): Span = - span(interleave(children, span(" / "))) - - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div = - div( - cls("flex flex-col mt-2"), - div( - cls( - "align-middle min-w-full shadow sm:rounded-lg" - ), - L.table( - cls("min-w-full border-separate border-spacing-0"), - styleAttr("border-spacing: 0"), // Tailwind somehow doesn't work - thead( - tr( - cls("border-t border-gray-200"), - headers.map(h => - th( - cls( - "sticky top-0 z-10 px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - h - ) - ) - ) - ), - tbody(cls("bg-white divide-y divide-gray-100"), sections) - ) - ) - ) - - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - td( - cls("bg-gray-50 border-gray-200 border-t border-b"), - cls( - "sticky top-14 z-10 bg-gray-50 px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - colSpan(cs), - label - ), - mods - ) - - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - mods, - td( - cls( - "px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900 truncate" - ), - label - ), - children.map(e => - td( - cls( - "whitespace-nowrap px-2 py-2 text-sm text-gray-500" - ), - e - ) - ) - ) - - private def interleave( - a: Seq[HtmlElement], - separator: => HtmlElement - ): Seq[HtmlElement] = - if a.size < 2 then a - else a.init.flatMap(n => Seq(n, separator)) :+ a.last diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala deleted file mode 100644 index a45beaf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.FileRef - -trait DetailComponentsModule: - - def details: DetailComponents - - trait DetailComponents: - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement - - def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement - - def files(fs: List[FileRef]): HtmlElement - - def file(f: FileRef): HtmlElement - -trait DefaultDetailComponentsModule(using ctx: ComponentContext) - extends DetailComponentsModule: - self: IconsModule => - - override val details: DetailComponents = new DetailComponents: - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement = - div( - div( - cls := "px-4 sm:px-0", - h3( - cls := "text-base font-semibold leading-7 text-gray-900", - title - ), - subtitle.map(st => - p( - cls := "mt-1 max-w-2xl text-sm leading-6 text-gray-500", - st - ) - ) - // TODO: actions - ), - div( - cls := "mt-6 border-t border-gray-100", - dl( - cls := "divide-y divide-gray-100", - fields - ) - ) - ) - - override def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0", - dt( - cls := "text-sm font-medium leading-6 text-gray-900", - title - ), - dd( - cls := "mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0", - content - ) - ) - - override def files(fs: List[FileRef]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-100 rounded-md border border-gray-200", - fs.map(file) - ) - - override def file(f: FileRef): HtmlElement = - li( - cls := "flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6", - div( - cls := "flex w-0 flex-1 items-center", - icons.`paper-clip-solid`(), - div( - cls := "ml-4 flex min-w-0 flex-1 gap-2", - span( - cls := "truncate font-medium", - f.name - ), - f.size.map(size => span(cls := "flex-shrink-0 text-gray-400", size)) - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - "Uložit" - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala new file mode 100644 index 0000000..f6c583f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait FormComponents(using ctx: ComponentContext) extends LocalDateSelectModule: + def searchIcon: SvgElement + + def renderLocalDateSelect( + id: String, + labelText: Option[String], + placeholderText: Option[String], + mods: LocalDateSelect => Modifier[HtmlElement] + ): HtmlElement = + div( + labelText, + input( + idAttr(id), + nameAttr(id), + autoComplete("date"), + tpe("date"), + cls( + "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ), + placeholderText.map(placeholder(_)), + mods(localDateSelect) + ) + ) + + def searchField( + id: String, + placeholderText: Option[String] = None + )( + mods: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls := "flex-1 min-w-0", + label(ctx.messages("forms.search.label"), forId(id), cls("sr-only")), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + searchIcon + ), + input( + tpe := "search", + nameAttr := "search", + idAttr := id, + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholderText + .orElse( + ctx.messages + .opt( + s"forms.search.${id}.placeholder", + s"form.search.placeholder" + ) + ) + .map(placeholder(_)), + mods + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala deleted file mode 100644 index 7714490..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala +++ /dev/null @@ -1,82 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -/** Form components - * - * A form is a collection of sections, each of which contains a collection of - * fields. - * - * Form -> * Section -> * Field - * -> * Action - * - * Each Field has an `id`, `label` and `input` element. The `id` is used to - * link the label to the input. - * - * The `input` element can be a simple text input, or a more complex component - * such as a date picker. The caller is expected to make the input work. - */ -trait FormComponentsModule extends LocalDateSelectModule: - def forms: FormComponents - - trait FormComponents: - - /** Layout the sections and actions of a form */ - def form(mods: Modifier[HtmlElement]*)(sections: Modifier[HtmlElement]*)( - actions: Modifier[HtmlElement]* - ): HtmlElement - - /** Section layout, with a title, optional subtitle and any content. */ - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label and any content */ - def field(label: Modifier[HtmlElement])( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label, input and optional help text */ - def field( - id: String, - label: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement - - def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout an inline form element */ - def inlineForm(mods: Modifier[HtmlElement]*): HtmlElement - - /** An input field with a search icon */ - def searchField(id: String, placeholderText: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement - - /** LocalDate input */ - def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala deleted file mode 100644 index 8acbe7f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala +++ /dev/null @@ -1,28 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.Tag - -trait GenericComponentsModule: - - def generic: GenericComponents - - trait GenericComponents: - def tag(name: String, color: ColorKind): HtmlElement - def tag(t: Tag): HtmlElement = tag(t.value, t.color) - -trait DefaultGenericComponentsModule(using ComponentContext) - extends GenericComponentsModule: - override val generic: GenericComponents = new GenericComponents: - override def tag(name: String, color: ColorKind): HtmlElement = - p( - cls( - "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" - ), - color(800).text, - color(100).bg, - name - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala deleted file mode 100644 index 8cfc1d9..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala +++ /dev/null @@ -1,123 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.CustomAttrs - -trait IconsModule: - def icons: Icons - - trait Icons: - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement - def close(mods: Modifier[SvgElement]*): SvgElement - def `search-solid`(mods: Modifier[SvgElement]*): SvgElement - def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement - def `document-chart-bar-outline`(mods: Modifier[SvgElement]*): SvgElement - def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement - -trait DefaultIconsModule(using ComponentContext) extends IconsModule: - override val icons: Icons = new Icons: - import svg.* - - object aria: - val hidden = CustomAttrs.svg.ariaHidden - - private def withDefault( - mods: Seq[Modifier[SvgElement]], - default: Modifier[SvgElement] - ): Modifier[SvgElement] = - if mods.isEmpty then default else mods - - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-8 w-8"), - 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" - ) - ) - ) - - override def close(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-2 w-2"), - stroke := "currentColor", - fill := "none", - viewBox := "0 0 8 8", - path( - strokeLineCap := "round", - strokeWidth := "1.5", - d := "M1 1l6 6m0-6L1 7" - ) - ) - - override def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 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 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") - ) - ) - - override def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 text-gray-400")), - 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") - ) - ) - - override def `document-chart-bar-outline`( - mods: Modifier[SvgElement]* - ): SvgElement = - svg( - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - strokeWidth := "1.5", - stroke := "currentColor", - withDefault(mods, cls := "h-6 w-6"), - path( - strokeLineCap := "round", - strokeLineJoin := "round", - d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" - ) - ) - - override def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-5 w-5"), - cls := "flex-shrink-0 text-gray-400", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", - clipRule := "evenodd" - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala deleted file mode 100644 index 95ab654..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait LayoutModule: - def layout: LayoutComponents - - trait LayoutComponents: - def card(content: Modifier[HtmlElement]*): HtmlElement - -trait DefaultLayoutModule(using ctx: ComponentContext) extends LayoutModule: - override val layout: LayoutComponents = new LayoutComponents: - override def card(content: Modifier[HtmlElement]*): HtmlElement = - div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala deleted file mode 100644 index 5946974..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom.html.Paragraph -import com.raquo.laminar.nodes.TextNode - -trait ListComponentsModule: - - def list: ListComponents - - trait ListComponents(using ComponentContext): - def label(text: String, color: ColorKind): HtmlElement - def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI - def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] - def listSection(header: String, list: HtmlElement): Div - def navigation(sections: Modifier[HtmlElement]): HtmlElement - -trait DefaultListComponentsModule(using ComponentContext) - extends ListComponentsModule: - self: GenericComponentsModule => - - override val list: ListComponents = new ListComponents: - override def label( - text: String, - color: ColorKind - ): HtmlElement = generic.tag(text, color) - - override def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI = - li( - cls("group"), - div( - cls( - "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" - ), - avatar.map(a => - div( - cls("flex-shrink-0"), - div( - cls( - "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" - ), - a - ) - ) - ), - div( - cls("flex-1 min-w-0"), - p( - cls("text-sm font-medium text-gray-900"), - title, - span(cls("float-right"), right) - ), - subtitle.map(st => - p( - cls("text-sm text-gray-500 truncate"), - st - ) - ) - ) - ) - ) - - override def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = - ul( - cls("relative z-0 divide-y divide-gray-200"), - role("list"), - children - ) - - override def listSection( - header: String, - list: HtmlElement - ): Div = - div( - cls("relative"), - div( - cls( - "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" - ), - header - ), - list - ) - - override def navigation(sections: Modifier[HtmlElement]): HtmlElement = - navTag( - cls("flex-1 min-h-0 overflow-y-auto"), - sections - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala deleted file mode 100644 index 5b5490f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ModalComponentsModule: - - def modal: ModalComponents - - trait ModalComponents: - def modalDialog( - content: HtmlElement, - onClose: Modifier[HtmlElement] - ): HtmlElement - -trait DefaultModalComponentsModule(using ctx: ComponentContext) - extends ModalComponentsModule: - - override val modal: ModalComponents = new ModalComponents: - - override def modalDialog( - content: HtmlElement, - closeMod: Modifier[HtmlElement] - ): 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( - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")), - closeMod - ) - ) - - div( - cls("fixed inset-0 z-20 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - 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-7xl sm:align-middle" - ), - content - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala deleted file mode 100644 index b713d8a..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala +++ /dev/null @@ -1,71 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait PageComponentsModule: - - val page: PageComponents - - trait PageComponents: - def container( - children: Modifier[HtmlElement]* - ): HtmlElement - - def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement - - def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement] = emptyMod, - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement - - /** Visage for clickable text, like links - */ - def clickable: Modifier[HtmlElement] - -trait DefaultPageComponentsModule(using ComponentContext) - extends PageComponentsModule: - - override val page: PageComponents = new PageComponents: - override def container( - children: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), - children - ) - - override def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement = - div( - cls("p-8 bg-gray-100 h-full"), - header, - children - ) - - override def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement = - div( - cls("pb-5 border-b border-gray-200"), - div(cls("float-right"), right), - h1( - cls("text-2xl leading-6 font-medium text-gray-900"), - title - ), - subtitle.map( - p( - cls("text-sm font-medium text-gray-500"), - _ - ) - ) - ) - - override def clickable: Modifier[HtmlElement] = - cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala deleted file mode 100644 index f70ee87..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala +++ /dev/null @@ -1,108 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} - -trait TableComponentsModule: - def tables: TableComponents - - trait TableComponents: - def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )(table: Modifier[HtmlElement]*): HtmlElement - - def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement - - def headerRow(mods: Modifier[HtmlElement]*)( - cells: HtmlElement* - ): HtmlElement - - def dataRow(mods: Modifier[HtmlElement]*)(cells: HtmlElement*): HtmlElement - - def headerCell(content: Modifier[HtmlElement]): HtmlElement - - def dataCell(content: Modifier[HtmlElement]): HtmlElement - -trait DefaultTableComponentsModule extends TableComponentsModule: - - override lazy val tables: TableComponents = new TableComponents: - - override def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )( - table: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("px-4 sm:px-6 lg:px-8"), - div( - cls("sm:flex sm:items-center"), - div( - cls("sm:flex-auto"), - h1(cls("text-base font-semibold leading-6 text-gray-900"), title), - subtitle.map(st => p(cls("mt-2 text-sm text-gray-700"), st)) - ), - div(cls("mt-4 sm:ml-16 sm:mt-0 sm:flex-none"), actions) - ), - div( - cls("mt-8 flow-root"), - div( - cls("-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"), - div( - cls("inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"), - table - ) - ) - ) - ) - - override def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement = - table( - cls("min-w-full divide-y divide-gray-300"), - thead(header), - tbody(cls("divide-y divide-gray-200"), body) - ) - - override def headerRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-3.5 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-3.5 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-3.5 px-3")) - ) - ) - - override def dataRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - mods, - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-4 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-4 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-4 px-3")) - ) - ) - - override def headerCell(content: Modifier[HtmlElement]): HtmlElement = - th( - cls("text-left text-sm font-semibold text-gray-900"), - content - ) - - override def dataCell(content: Modifier[HtmlElement]): HtmlElement = - td( - cls("whitespace-nowrap text-sm text-gray-500"), - content - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala index b937d45..12d01ae 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala @@ -3,7 +3,7 @@ import com.raquo.laminar.api.L.{*, given} import works.iterative.ui.components.laminar.HtmlTabular import works.iterative.ui.model.tables.Tabular -import works.iterative.ui.components.laminar.TableComponentsModule +import works.iterative.ui.components.laminar.tailwind.ui.TableComponentsModule import works.iterative.core.UserMessage import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given import works.iterative.ui.components.tailwind.ComponentContext diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala new file mode 100644 index 0000000..87f3e8d --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala @@ -0,0 +1,54 @@ +package works.iterative.ui.components.laminar.tailwind.color + +import com.raquo.laminar.api.L.{*, given} + +/** Complete color definition that can be rendered to CSS. + * + * Includes the area, kind and weight of the color. + */ +case class Color(area: ColorArea, color: ColorDef): + def toCSS: String = s"${area.name}-${color.toCSS}" + +object Color: + import ColorDef.given + + given colorToCSS: Conversion[Color, HtmlMod] with + def apply(c: Color) = cls(c.toCSS) + + given colorToSVGCSS: Conversion[Color, SvgMod] with + def apply(c: Color) = svg.cls(c.toCSS) + + given colorSignalToCSS: Conversion[Signal[Color], HtmlMod] with + def apply(c: Signal[Color]) = cls <-- c.map(_.toCSS) + + given colorSignalToSVGCSS: Conversion[Signal[Color], SvgMod] with + def apply(c: Signal[Color]) = svg.cls <-- c.map(_.toCSS) + + def current = ColorDef(ColorKind.current) + def inherit = ColorDef(ColorKind.inherit) + def transp = ColorDef(ColorKind.transp) + def auto = ColorDef(ColorKind.auto) + def black = ColorDef(ColorKind.black) + def white = ColorDef(ColorKind.white) + def slate(weight: ColorWeight) = ColorDef(ColorKind.slate, weight) + def gray(weight: ColorWeight) = ColorDef(ColorKind.gray, weight) + def zinc(weight: ColorWeight) = ColorDef(ColorKind.zinc, weight) + def neutral(weight: ColorWeight) = ColorDef(ColorKind.neutral, weight) + def stone(weight: ColorWeight) = ColorDef(ColorKind.stone, weight) + def red(weight: ColorWeight) = ColorDef(ColorKind.red, weight) + def orange(weight: ColorWeight) = ColorDef(ColorKind.orange, weight) + def amber(weight: ColorWeight) = ColorDef(ColorKind.amber, weight) + def yellow(weight: ColorWeight) = ColorDef(ColorKind.yellow, weight) + def lime(weight: ColorWeight) = ColorDef(ColorKind.lime, weight) + def green(weight: ColorWeight) = ColorDef(ColorKind.green, weight) + def emerald(weight: ColorWeight) = ColorDef(ColorKind.emerald, weight) + def teal(weight: ColorWeight) = ColorDef(ColorKind.teal, weight) + def cyan(weight: ColorWeight) = ColorDef(ColorKind.cyan, weight) + def sky(weight: ColorWeight) = ColorDef(ColorKind.sky, weight) + def blue(weight: ColorWeight) = ColorDef(ColorKind.blue, weight) + def indigo(weight: ColorWeight) = ColorDef(ColorKind.indigo, weight) + def violet(weight: ColorWeight) = ColorDef(ColorKind.violet, weight) + def purple(weight: ColorWeight) = ColorDef(ColorKind.purple, weight) + def fuchsia(weight: ColorWeight) = ColorDef(ColorKind.fuchsia, weight) + def pink(weight: ColorWeight) = ColorDef(ColorKind.pink, weight) + def rose(weight: ColorWeight) = ColorDef(ColorKind.rose, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala new file mode 100644 index 0000000..4130840 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala @@ -0,0 +1,16 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines the area the color should apply to, eg. background, text, border, + * etc. + */ +enum ColorArea(val name: String): + case bg extends ColorArea("bg") + case text extends ColorArea("text") + case decoration extends ColorArea("decoration") + case border extends ColorArea("border") + case outline extends ColorArea("outline") + case divide extends ColorArea("divide") + case ring extends ColorArea("ring") + case ringOffset extends ColorArea("ring-offset") + case shadow extends ColorArea("shadow") + case accent extends ColorArea("accent") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala new file mode 100644 index 0000000..fab9404 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala @@ -0,0 +1,43 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** A combination of ColorKind and ColorWeight, if applicable. + * + * By applying area we get the full Color definition. + */ +sealed trait ColorDef: + def toCSS: String + + def bg = Color(ColorArea.bg, this) + def text = Color(ColorArea.text, this) + def decoration = Color(ColorArea.decoration, this) + def border = Color(ColorArea.border, this) + def outline = Color(ColorArea.outline, this) + def divide = Color(ColorArea.divide, this) + def ring = Color(ColorArea.ring, this) + def ringOffset = Color(ColorArea.ringOffset, this) + def shadow = Color(ColorArea.shadow, this) + def accent = Color(ColorArea.accent, this) + +// TODO: create a macro that will output the tailwind class as string directly during compilation, where possible. +object ColorDef: + case class WeightedColorDef( + kind: ColorKind, + weight: ColorWeight + ) extends ColorDef: + override def toCSS: String = s"${kind.name}-${weight.value}" + + case class UnweightedColorDef( + kind: ColorKind + ) extends ColorDef: + override def toCSS: String = kind.name + + // TODO: check that the kind is valid unweighted kind + // that means current, inherit, auto, transparent, black, white + // tried using implicit evidence, but the type inference for enumerations + // tends to generalize to the enum, instead of the real type + def apply[T <: ColorKind](kind: T)(using + ev: T <:< ColorKind.Unweighted + ): ColorDef = + UnweightedColorDef(kind) + def apply(kind: ColorKind, weight: ColorWeight): ColorDef = + WeightedColorDef(kind, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala new file mode 100644 index 0000000..865166b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala @@ -0,0 +1,47 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines what color should be used, without specifying the area or weight. + */ +sealed abstract class ColorKind private (val name: String): + def apply(weight: ColorWeight): ColorDef = + ColorDef.WeightedColorDef(this, weight) + +object ColorKind: + trait Unweighted: + self: ColorKind => + override def apply(weight: ColorWeight): ColorDef = + ColorDef.UnweightedColorDef(self) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case object current extends ColorKind("current") with Unweighted + case object inherit extends ColorKind("inherit") with Unweighted + // Not present in for all methods + case object transp extends ColorKind("transparent") with Unweighted + // Seen in accent, not preset otherwise + case object auto extends ColorKind("auto") with Unweighted + // Black and white do not have weight + case object black extends ColorKind("black") with Unweighted + case object white extends ColorKind("white") with Unweighted + case object slate extends ColorKind("slate") + case object gray extends ColorKind("gray") + case object zinc extends ColorKind("zinc") + case object neutral extends ColorKind("neutral") + case object stone extends ColorKind("stone") + case object red extends ColorKind("red") + case object orange extends ColorKind("orange") + case object amber extends ColorKind("amber") + case object yellow extends ColorKind("yellow") + case object lime extends ColorKind("lime") + case object green extends ColorKind("green") + case object emerald extends ColorKind("emerald") + case object teal extends ColorKind("teal") + case object cyan extends ColorKind("cyan") + case object sky extends ColorKind("sky") + case object blue extends ColorKind("blue") + case object indigo extends ColorKind("indigo") + case object violet extends ColorKind("violet") + case object purple extends ColorKind("purple") + case object fuchsia extends ColorKind("fuchsia") + case object pink extends ColorKind("pink") + case object rose extends ColorKind("rose") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala new file mode 100644 index 0000000..6f1a4cc --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala @@ -0,0 +1,33 @@ +package works.iterative.ui.components.laminar.tailwind.color + +opaque type ColorWeight = String + +extension (c: ColorWeight) def value: String = c + +/** Defines weight of a color, eg. 50, 100, 200, etc. + * + * Tailwind-like. + */ +object ColorWeight: + inline given int50: Conversion[50, ColorWeight] with + inline def apply(i: 50) = "50" + inline given int100: Conversion[100, ColorWeight] with + inline def apply(i: 100) = "100" + inline given int200: Conversion[200, ColorWeight] with + inline def apply(i: 200) = "200" + inline given int300: Conversion[300, ColorWeight] with + inline def apply(i: 300) = "300" + inline given int400: Conversion[400, ColorWeight] with + inline def apply(i: 400) = "400" + inline given int500: Conversion[500, ColorWeight] with + inline def apply(i: 500) = "500" + inline given int600: Conversion[600, ColorWeight] with + inline def apply(i: 600) = "600" + inline given int700: Conversion[700, ColorWeight] with + inline def apply(i: 700) = "700" + inline given int800: Conversion[800, ColorWeight] with + inline def apply(i: 800) = "800" + inline given int900: Conversion[900, ColorWeight] with + inline def apply(i: 900) = "900" + inline given int950: Conversion[950, ColorWeight] with + inline def apply(i: 950) = "950" diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala new file mode 100644 index 0000000..21e21f0 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala @@ -0,0 +1,12 @@ +package works.iterative.ui.components.laminar.tailwind + +/** We need a generic Color model that can be used both on server and on client. + * + * We have adopted the Tailwind model for now. There is nothing inherently + * Tailwind-specific in the implementation, but all the values are taken from + * their palette and the area model is very HTML biased. + * + * Still, I think that it is a good starting point for exploration and will + * satisfy our current needs. + */ +package object color {} diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" new file mode 100644 index 0000000..5c4e218 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" @@ -0,0 +1,21 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.model.color.ColorKind +import works.iterative.ui.components.tailwind.ComponentContext +import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given +import works.iterative.ui.model.Tag + +trait TailwindUIBadgesComponentsModule(using ComponentContext): + + object badges: + override def tag(name: String, color: ColorKind): HtmlElement = + p( + cls( + "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" + ), + color(800).text, + color(100).bg, + name + ) diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" new file mode 100644 index 0000000..0c9ca25 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" @@ -0,0 +1,24 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} + +trait TailwindUIHeadingsComponentsModule: + val headings: HeadingsComponents = new HeadingsComponents + + class HeadingsComponents: + def section(title: Node, actions: HtmlElement*): HtmlElement = + div( + cls("border-b border-gray-200 pb-5"), + div( + cls("sm:flex sm:items-center sm:justify-between"), + h3(cls("text-base font-semibold leading-6 text-gray-900"), title), + div( + cls("mt-3 flex sm:ml-4 sm:mt-0"), + actions match + case Nil => emptyMod + case first :: rest => + first :: rest.map(_.amend(cls("ml-3"))) + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala new file mode 100644 index 0000000..16ca01b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala @@ -0,0 +1,19 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.laminar.tailwind.color.ColorKind + +trait BadgeComponentsModule: + + object badges: + def pill(name: String, color: ColorKind): HtmlElement = + p( + cls( + "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" + ), + color(800).text, + color(100).bg, + name + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala new file mode 100644 index 0000000..ebb2968 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} + +trait ButtonComponentsModule: + + object buttons: + + private inline def srHelp(text: String): Modifier[HtmlElement] = + span(cls := "sr-only", text) + + def primaryButton( + id: String, + text: Modifier[HtmlElement], + icon: Option[SvgElement] = None, + buttonType: String = "submit" + )(mods: Modifier[HtmlElement]*): HtmlElement = + button( + tpe(buttonType), + cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + icon, + text, + mods + ) + + def secondaryButton( + id: String, + text: Modifier[HtmlElement], + icon: Option[SvgElement] = None, + buttonType: String = "button" + )(mods: Modifier[HtmlElement]*): HtmlElement = + button( + tpe(buttonType), + cls( + "ml-2 bg-white inline-flex justify-center 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" + ), + icon, + text, + mods + ) + + def inlineButton(id: String, icon: SvgElement)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + button( + tpe := "button", + cls := "ml-1 inline-flex h-4 w-4 flex-shrink-0 rounded-full p-1 text-gray-400 hover:bg-gray-200 hover:text-gray-500", + srHelp(id), + icon + ) + + def iconButton(id: String, icon: SvgElement, srText: Option[String] = None)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + button( + tpe := "button", + cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", + icon, + srText.map(srHelp(_)) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala new file mode 100644 index 0000000..eb6dd5f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala @@ -0,0 +1,101 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import org.scalajs.dom.html +import com.raquo.laminar.modifiers.KeyUpdater + +trait FormComponentsModule: + self: IconsModule => + + object forms: + + def section( + title: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] + )(content: Modifier[HtmlElement]*): HtmlElement = + div( + cls("space-y-6 sm:space-y-5"), + div( + h3(cls("text-lg leading-6 font-medium text-gray-900"), title), + subtitle.map(st => p(cls("mt-1 max-w-2xl text-sm text-gray-500"), st)) + ), + div(cls("mt-6 sm:mt-5 space-y-6 sm:space-y-5"), content) + ) + + def label(labelText: String, forId: Option[String] = None)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + L.label( + cls("block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"), + forId.map(id => L.forId(id)), + labelText, + mods + ) + + def field( + label: Modifier[HtmlElement] + )(content: Modifier[HtmlElement]*): 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, + div(cls("mt-1 sm:mt-0 sm:col-span-2"), content) + ) + + def field( + id: String, + labelText: String, + input: HtmlElement, + help: Option[String] + ): HtmlElement = + field( + label(labelText, Some(id))() + )( + input.amend(idAttr(id)), + help.map(h => p(cls("mt-2 text-sm text-gray-500"), h)) + ) + + def form(mods: Modifier[HtmlElement]*)( + sections: Modifier[HtmlElement]* + )(actions: Modifier[HtmlElement]*): HtmlElement = + L.form( + cls("space-y-8 divide-y divide-gray-200"), + mods, + sections, + div( + cls("pt-5"), + div(cls("flex justify-end"), actions) + ) + ) + + def inlineForm( + mods: Modifier[HtmlElement]* + ): HtmlElement = + L.form(cls("flex space-x-4"), mods) + + def inputField( + id: String, + labelText: String, + placeholderText: Option[String] = None, + inputType: String = "text", + helpText: Option[String] = None + ): HtmlElement = + field( + id, + labelText, + input( + tpe(inputType), + cls( + "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:max-w-xs sm:text-sm sm:leading-6" + ), + placeholderText.map(placeholder(_)) + ), + helpText + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala new file mode 100644 index 0000000..52da07e --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala @@ -0,0 +1,22 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} + +trait HeadingsComponentsModule: + object headings: + def section(title: Node, actions: HtmlElement*): HtmlElement = + div( + cls("border-b border-gray-200 pb-5"), + div( + cls("sm:flex sm:items-center sm:justify-between"), + h3(cls("text-base font-semibold leading-6 text-gray-900"), title), + div( + cls("mt-3 flex sm:ml-4 sm:mt-0"), + actions match + case Nil => emptyMod + case first :: rest => + first :: rest.map(_.amend(cls("ml-3"))) + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala new file mode 100644 index 0000000..c543150 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala @@ -0,0 +1,112 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext +import works.iterative.ui.components.tailwind.CustomAttrs + +trait IconsModule: + object icons: + import svg.* + + object aria: + val hidden = CustomAttrs.svg.ariaHidden + + private def withDefault( + mods: Seq[Modifier[SvgElement]], + default: Modifier[SvgElement] + ): Modifier[SvgElement] = + if mods.isEmpty then default else mods + + def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-8 w-8"), + 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" + ) + ) + ) + + def close(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-2 w-2"), + stroke := "currentColor", + fill := "none", + viewBox := "0 0 8 8", + path( + strokeLineCap := "round", + strokeWidth := "1.5", + d := "M1 1l6 6m0-6L1 7" + ) + ) + + def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls("h-5 w-5 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 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") + ) + ) + + def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls("h-5 w-5 text-gray-400")), + 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") + ) + ) + + def `document-chart-bar-outline`( + mods: Modifier[SvgElement]* + ): SvgElement = + svg( + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + strokeWidth := "1.5", + stroke := "currentColor", + withDefault(mods, cls := "h-6 w-6"), + path( + strokeLineCap := "round", + strokeLineJoin := "round", + d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" + ) + ) + + def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-5 w-5"), + cls := "flex-shrink-0 text-gray-400", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", + clipRule := "evenodd" + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala new file mode 100644 index 0000000..02cbdb6 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala @@ -0,0 +1,11 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait LayoutModule: + object layout: + def card(content: Modifier[HtmlElement]*): HtmlElement = + div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala new file mode 100644 index 0000000..b652da9 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala @@ -0,0 +1,88 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.laminar.nodes.ReactiveHtmlElement +import org.scalajs.dom.html.Paragraph +import com.raquo.laminar.nodes.TextNode +import works.iterative.ui.components.laminar.tailwind.color.ColorKind + +trait ListComponentsModule: + self: BadgeComponentsModule => + + object list: + def label( + text: String, + color: ColorKind + ): HtmlElement = badges.pill(text, color) + + def item( + title: String, + subtitle: Option[String], + right: Modifier[HtmlElement] = emptyMod, + avatar: Option[Modifier[HtmlElement]] = None + ): LI = + li( + cls("group"), + div( + cls( + "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" + ), + avatar.map(a => + div( + cls("flex-shrink-0"), + div( + cls( + "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" + ), + a + ) + ) + ), + div( + cls("flex-1 min-w-0"), + p( + cls("text-sm font-medium text-gray-900"), + title, + span(cls("float-right"), right) + ), + subtitle.map(st => + p( + cls("text-sm text-gray-500 truncate"), + st + ) + ) + ) + ) + ) + + def unordered( + children: Modifier[HtmlElement] + ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = + ul( + cls("relative z-0 divide-y divide-gray-200"), + role("list"), + children + ) + + def listSection( + header: String, + list: HtmlElement + ): Div = + div( + cls("relative"), + div( + cls( + "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" + ), + header + ), + list + ) + + def navigation(sections: Modifier[HtmlElement]): HtmlElement = + navTag( + cls("flex-1 min-h-0 overflow-y-auto"), + sections + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ModalComponentsModule.scala new file mode 100644 index 0000000..6027404 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ModalComponentsModule.scala @@ -0,0 +1,54 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait ModalComponentsModule: + object modal: + def modalDialog( + content: HtmlElement, + closeMod: Modifier[HtmlElement] + ): 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( + cls("fixed inset-0 transition-opacity"), + div(cls("absolute inset-0 bg-gray-500 opacity-75")), + closeMod + ) + ) + + div( + cls("fixed inset-0 z-20 overflow-y-auto"), + div( + cls("text-center sm:block sm:p-0"), + overlay, + browserCenteringModalTrick, + 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-7xl sm:align-middle" + ), + content + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/PageComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/PageComponentsModule.scala new file mode 100644 index 0000000..d5a422e --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/PageComponentsModule.scala @@ -0,0 +1,49 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait PageComponentsModule: + + object page: + def container( + children: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), + children + ) + + def singleColumn( + header: Modifier[HtmlElement] + )(children: Modifier[HtmlElement]*): HtmlElement = + div( + cls("p-8 bg-gray-100 h-full"), + header, + children + ) + + def pageHeader( + title: Modifier[HtmlElement], + right: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] = None + ): HtmlElement = + div( + cls("pb-5 border-b border-gray-200"), + div(cls("float-right"), right), + h1( + cls("text-2xl leading-6 font-medium text-gray-900"), + title + ), + subtitle.map( + p( + cls("text-sm font-medium text-gray-500"), + _ + ) + ) + ) + + def clickable: Modifier[HtmlElement] = + cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TableComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TableComponentsModule.scala new file mode 100644 index 0000000..417ea56 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TableComponentsModule.scala @@ -0,0 +1,86 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L +import com.raquo.laminar.api.L.{*, given} + +trait TableComponentsModule: + + object tables: + + def tableSection( + title: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] = None, + actions: Modifier[HtmlElement]* + )( + table: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls("px-4 sm:px-6 lg:px-8"), + div( + cls("sm:flex sm:items-center"), + div( + cls("sm:flex-auto"), + h1(cls("text-base font-semibold leading-6 text-gray-900"), title), + subtitle.map(st => p(cls("mt-2 text-sm text-gray-700"), st)) + ), + div(cls("mt-4 sm:ml-16 sm:mt-0 sm:flex-none"), actions) + ), + div( + cls("mt-8 flow-root"), + div( + cls("-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"), + div( + cls("inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"), + table + ) + ) + ) + ) + + def simpleTable(header: Modifier[HtmlElement]*)( + body: Modifier[HtmlElement]* + ): HtmlElement = + table( + cls("min-w-full divide-y divide-gray-300"), + thead(header), + tbody(cls("divide-y divide-gray-200"), body) + ) + + def headerRow( + mods: Modifier[HtmlElement]* + )(cells: HtmlElement*): HtmlElement = + tr( + cells.zipWithIndex.map((c, i) => + if i == 0 then c.amend(cls("py-3.5 pl-4 pr-3 sm:pl-0")) + else if i == cells.length - 1 then + c.amend(cls("py-3.5 pr-4 pl-3 sm:pr-0")) + else c.amend(cls("py-3.5 px-3")) + ) + ) + + def dataRow( + mods: Modifier[HtmlElement]* + )(cells: HtmlElement*): HtmlElement = + tr( + mods, + cells.zipWithIndex.map((c, i) => + if i == 0 then c.amend(cls("py-4 pl-4 pr-3 sm:pl-0")) + else if i == cells.length - 1 then + c.amend(cls("py-4 pr-4 pl-3 sm:pr-0")) + else c.amend(cls("py-4 px-3")) + ) + ) + + def headerCell(content: Modifier[HtmlElement]): HtmlElement = + th( + cls("text-left text-sm font-semibold text-gray-900"), + content + ) + + def dataCell(content: Modifier[HtmlElement]): HtmlElement = + td( + cls("whitespace-nowrap text-sm text-gray-500"), + content + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala index a2c637c..bfa063a 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala @@ -1,13 +1,11 @@ package works.iterative.ui.components.laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorDef import com.raquo.laminar.tags.HtmlTag case class Bin[Source, +Value]( label: String, description: Option[String | HtmlElement], - color: ColorDef, valueOf: Source => Value ): def map[NewValue]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala deleted file mode 100644 index 3aac0bf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala +++ /dev/null @@ -1,34 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ButtonComponentsModule: - def buttons: ButtonComponents - - trait ButtonComponents: - def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala deleted file mode 100644 index 646e73d..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala +++ /dev/null @@ -1,191 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.color.ColorDef -import com.raquo.laminar.tags.HtmlTag -import org.scalajs.dom.html.UList -import com.raquo.laminar.nodes.ReactiveHtmlElement -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind - -trait DashboardComponentsModule: - def dashboard: DashboardComponents - - // Only methods that create HtmlElements - trait DashboardComponents(using ComponentContext): - def section(label: String, children: Modifier[HtmlElement]): Div - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI - def cardInitials( - initials: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div - def count(label: String, value: Int, color: ColorDef): Span - def counts(children: HtmlElement*): Span - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - -trait DefaultDashboardComponentsModule(using ComponentContext) - extends DashboardComponentsModule: - override val dashboard: DashboardComponents = new DashboardComponents: - def section(label: String, children: Modifier[HtmlElement]): Div = - div( - cls("mt-3"), - h2( - cls("text-gray-500 text-xs font-medium uppercase tracking-wide"), - label - ), - children - ) - - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] = - ul( - cls( - "mt-3 grid gap-5 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" - ), - children - ) - - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI = - li( - div( - cls("col-span-1 flex shadow-sm rounded-md"), - initials, - div( - cls( - "flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate" - ), - div( - cls("flex-1 px-4 py-2 text-sm truncate"), - div( - cls("text-gray-900 font-medium hover:text-gray-600 truncate"), - label - ), - p(cls("text-gray-500"), children) - ) - ) - ) - ) - - def cardInitials( - id: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div = - div( - cls( - "flex-shrink-0 flex flex-col items-center justify-center w-16 text-white text-sm font-medium rounded-l-md" - ), - color.map(_(600).bg), - div(id), - children - ) - - def count(label: String, value: Int, color: ColorDef): Span = - span(color.text, title(label), s"${value}") - - def counts(children: HtmlElement*): Span = - span(interleave(children, span(" / "))) - - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div = - div( - cls("flex flex-col mt-2"), - div( - cls( - "align-middle min-w-full shadow sm:rounded-lg" - ), - L.table( - cls("min-w-full border-separate border-spacing-0"), - styleAttr("border-spacing: 0"), // Tailwind somehow doesn't work - thead( - tr( - cls("border-t border-gray-200"), - headers.map(h => - th( - cls( - "sticky top-0 z-10 px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - h - ) - ) - ) - ), - tbody(cls("bg-white divide-y divide-gray-100"), sections) - ) - ) - ) - - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - td( - cls("bg-gray-50 border-gray-200 border-t border-b"), - cls( - "sticky top-14 z-10 bg-gray-50 px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - colSpan(cs), - label - ), - mods - ) - - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - mods, - td( - cls( - "px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900 truncate" - ), - label - ), - children.map(e => - td( - cls( - "whitespace-nowrap px-2 py-2 text-sm text-gray-500" - ), - e - ) - ) - ) - - private def interleave( - a: Seq[HtmlElement], - separator: => HtmlElement - ): Seq[HtmlElement] = - if a.size < 2 then a - else a.init.flatMap(n => Seq(n, separator)) :+ a.last diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala deleted file mode 100644 index a45beaf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.FileRef - -trait DetailComponentsModule: - - def details: DetailComponents - - trait DetailComponents: - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement - - def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement - - def files(fs: List[FileRef]): HtmlElement - - def file(f: FileRef): HtmlElement - -trait DefaultDetailComponentsModule(using ctx: ComponentContext) - extends DetailComponentsModule: - self: IconsModule => - - override val details: DetailComponents = new DetailComponents: - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement = - div( - div( - cls := "px-4 sm:px-0", - h3( - cls := "text-base font-semibold leading-7 text-gray-900", - title - ), - subtitle.map(st => - p( - cls := "mt-1 max-w-2xl text-sm leading-6 text-gray-500", - st - ) - ) - // TODO: actions - ), - div( - cls := "mt-6 border-t border-gray-100", - dl( - cls := "divide-y divide-gray-100", - fields - ) - ) - ) - - override def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0", - dt( - cls := "text-sm font-medium leading-6 text-gray-900", - title - ), - dd( - cls := "mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0", - content - ) - ) - - override def files(fs: List[FileRef]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-100 rounded-md border border-gray-200", - fs.map(file) - ) - - override def file(f: FileRef): HtmlElement = - li( - cls := "flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6", - div( - cls := "flex w-0 flex-1 items-center", - icons.`paper-clip-solid`(), - div( - cls := "ml-4 flex min-w-0 flex-1 gap-2", - span( - cls := "truncate font-medium", - f.name - ), - f.size.map(size => span(cls := "flex-shrink-0 text-gray-400", size)) - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - "Uložit" - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala new file mode 100644 index 0000000..f6c583f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait FormComponents(using ctx: ComponentContext) extends LocalDateSelectModule: + def searchIcon: SvgElement + + def renderLocalDateSelect( + id: String, + labelText: Option[String], + placeholderText: Option[String], + mods: LocalDateSelect => Modifier[HtmlElement] + ): HtmlElement = + div( + labelText, + input( + idAttr(id), + nameAttr(id), + autoComplete("date"), + tpe("date"), + cls( + "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ), + placeholderText.map(placeholder(_)), + mods(localDateSelect) + ) + ) + + def searchField( + id: String, + placeholderText: Option[String] = None + )( + mods: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls := "flex-1 min-w-0", + label(ctx.messages("forms.search.label"), forId(id), cls("sr-only")), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + searchIcon + ), + input( + tpe := "search", + nameAttr := "search", + idAttr := id, + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholderText + .orElse( + ctx.messages + .opt( + s"forms.search.${id}.placeholder", + s"form.search.placeholder" + ) + ) + .map(placeholder(_)), + mods + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala deleted file mode 100644 index 7714490..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala +++ /dev/null @@ -1,82 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -/** Form components - * - * A form is a collection of sections, each of which contains a collection of - * fields. - * - * Form -> * Section -> * Field - * -> * Action - * - * Each Field has an `id`, `label` and `input` element. The `id` is used to - * link the label to the input. - * - * The `input` element can be a simple text input, or a more complex component - * such as a date picker. The caller is expected to make the input work. - */ -trait FormComponentsModule extends LocalDateSelectModule: - def forms: FormComponents - - trait FormComponents: - - /** Layout the sections and actions of a form */ - def form(mods: Modifier[HtmlElement]*)(sections: Modifier[HtmlElement]*)( - actions: Modifier[HtmlElement]* - ): HtmlElement - - /** Section layout, with a title, optional subtitle and any content. */ - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label and any content */ - def field(label: Modifier[HtmlElement])( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label, input and optional help text */ - def field( - id: String, - label: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement - - def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout an inline form element */ - def inlineForm(mods: Modifier[HtmlElement]*): HtmlElement - - /** An input field with a search icon */ - def searchField(id: String, placeholderText: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement - - /** LocalDate input */ - def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala deleted file mode 100644 index 8acbe7f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala +++ /dev/null @@ -1,28 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.Tag - -trait GenericComponentsModule: - - def generic: GenericComponents - - trait GenericComponents: - def tag(name: String, color: ColorKind): HtmlElement - def tag(t: Tag): HtmlElement = tag(t.value, t.color) - -trait DefaultGenericComponentsModule(using ComponentContext) - extends GenericComponentsModule: - override val generic: GenericComponents = new GenericComponents: - override def tag(name: String, color: ColorKind): HtmlElement = - p( - cls( - "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" - ), - color(800).text, - color(100).bg, - name - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala deleted file mode 100644 index 8cfc1d9..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala +++ /dev/null @@ -1,123 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.CustomAttrs - -trait IconsModule: - def icons: Icons - - trait Icons: - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement - def close(mods: Modifier[SvgElement]*): SvgElement - def `search-solid`(mods: Modifier[SvgElement]*): SvgElement - def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement - def `document-chart-bar-outline`(mods: Modifier[SvgElement]*): SvgElement - def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement - -trait DefaultIconsModule(using ComponentContext) extends IconsModule: - override val icons: Icons = new Icons: - import svg.* - - object aria: - val hidden = CustomAttrs.svg.ariaHidden - - private def withDefault( - mods: Seq[Modifier[SvgElement]], - default: Modifier[SvgElement] - ): Modifier[SvgElement] = - if mods.isEmpty then default else mods - - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-8 w-8"), - 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" - ) - ) - ) - - override def close(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-2 w-2"), - stroke := "currentColor", - fill := "none", - viewBox := "0 0 8 8", - path( - strokeLineCap := "round", - strokeWidth := "1.5", - d := "M1 1l6 6m0-6L1 7" - ) - ) - - override def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 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 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") - ) - ) - - override def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 text-gray-400")), - 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") - ) - ) - - override def `document-chart-bar-outline`( - mods: Modifier[SvgElement]* - ): SvgElement = - svg( - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - strokeWidth := "1.5", - stroke := "currentColor", - withDefault(mods, cls := "h-6 w-6"), - path( - strokeLineCap := "round", - strokeLineJoin := "round", - d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" - ) - ) - - override def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-5 w-5"), - cls := "flex-shrink-0 text-gray-400", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", - clipRule := "evenodd" - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala deleted file mode 100644 index 95ab654..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait LayoutModule: - def layout: LayoutComponents - - trait LayoutComponents: - def card(content: Modifier[HtmlElement]*): HtmlElement - -trait DefaultLayoutModule(using ctx: ComponentContext) extends LayoutModule: - override val layout: LayoutComponents = new LayoutComponents: - override def card(content: Modifier[HtmlElement]*): HtmlElement = - div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala deleted file mode 100644 index 5946974..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom.html.Paragraph -import com.raquo.laminar.nodes.TextNode - -trait ListComponentsModule: - - def list: ListComponents - - trait ListComponents(using ComponentContext): - def label(text: String, color: ColorKind): HtmlElement - def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI - def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] - def listSection(header: String, list: HtmlElement): Div - def navigation(sections: Modifier[HtmlElement]): HtmlElement - -trait DefaultListComponentsModule(using ComponentContext) - extends ListComponentsModule: - self: GenericComponentsModule => - - override val list: ListComponents = new ListComponents: - override def label( - text: String, - color: ColorKind - ): HtmlElement = generic.tag(text, color) - - override def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI = - li( - cls("group"), - div( - cls( - "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" - ), - avatar.map(a => - div( - cls("flex-shrink-0"), - div( - cls( - "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" - ), - a - ) - ) - ), - div( - cls("flex-1 min-w-0"), - p( - cls("text-sm font-medium text-gray-900"), - title, - span(cls("float-right"), right) - ), - subtitle.map(st => - p( - cls("text-sm text-gray-500 truncate"), - st - ) - ) - ) - ) - ) - - override def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = - ul( - cls("relative z-0 divide-y divide-gray-200"), - role("list"), - children - ) - - override def listSection( - header: String, - list: HtmlElement - ): Div = - div( - cls("relative"), - div( - cls( - "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" - ), - header - ), - list - ) - - override def navigation(sections: Modifier[HtmlElement]): HtmlElement = - navTag( - cls("flex-1 min-h-0 overflow-y-auto"), - sections - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala deleted file mode 100644 index 5b5490f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ModalComponentsModule: - - def modal: ModalComponents - - trait ModalComponents: - def modalDialog( - content: HtmlElement, - onClose: Modifier[HtmlElement] - ): HtmlElement - -trait DefaultModalComponentsModule(using ctx: ComponentContext) - extends ModalComponentsModule: - - override val modal: ModalComponents = new ModalComponents: - - override def modalDialog( - content: HtmlElement, - closeMod: Modifier[HtmlElement] - ): 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( - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")), - closeMod - ) - ) - - div( - cls("fixed inset-0 z-20 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - 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-7xl sm:align-middle" - ), - content - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala deleted file mode 100644 index b713d8a..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala +++ /dev/null @@ -1,71 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait PageComponentsModule: - - val page: PageComponents - - trait PageComponents: - def container( - children: Modifier[HtmlElement]* - ): HtmlElement - - def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement - - def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement] = emptyMod, - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement - - /** Visage for clickable text, like links - */ - def clickable: Modifier[HtmlElement] - -trait DefaultPageComponentsModule(using ComponentContext) - extends PageComponentsModule: - - override val page: PageComponents = new PageComponents: - override def container( - children: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), - children - ) - - override def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement = - div( - cls("p-8 bg-gray-100 h-full"), - header, - children - ) - - override def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement = - div( - cls("pb-5 border-b border-gray-200"), - div(cls("float-right"), right), - h1( - cls("text-2xl leading-6 font-medium text-gray-900"), - title - ), - subtitle.map( - p( - cls("text-sm font-medium text-gray-500"), - _ - ) - ) - ) - - override def clickable: Modifier[HtmlElement] = - cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala deleted file mode 100644 index f70ee87..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala +++ /dev/null @@ -1,108 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} - -trait TableComponentsModule: - def tables: TableComponents - - trait TableComponents: - def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )(table: Modifier[HtmlElement]*): HtmlElement - - def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement - - def headerRow(mods: Modifier[HtmlElement]*)( - cells: HtmlElement* - ): HtmlElement - - def dataRow(mods: Modifier[HtmlElement]*)(cells: HtmlElement*): HtmlElement - - def headerCell(content: Modifier[HtmlElement]): HtmlElement - - def dataCell(content: Modifier[HtmlElement]): HtmlElement - -trait DefaultTableComponentsModule extends TableComponentsModule: - - override lazy val tables: TableComponents = new TableComponents: - - override def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )( - table: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("px-4 sm:px-6 lg:px-8"), - div( - cls("sm:flex sm:items-center"), - div( - cls("sm:flex-auto"), - h1(cls("text-base font-semibold leading-6 text-gray-900"), title), - subtitle.map(st => p(cls("mt-2 text-sm text-gray-700"), st)) - ), - div(cls("mt-4 sm:ml-16 sm:mt-0 sm:flex-none"), actions) - ), - div( - cls("mt-8 flow-root"), - div( - cls("-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"), - div( - cls("inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"), - table - ) - ) - ) - ) - - override def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement = - table( - cls("min-w-full divide-y divide-gray-300"), - thead(header), - tbody(cls("divide-y divide-gray-200"), body) - ) - - override def headerRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-3.5 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-3.5 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-3.5 px-3")) - ) - ) - - override def dataRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - mods, - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-4 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-4 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-4 px-3")) - ) - ) - - override def headerCell(content: Modifier[HtmlElement]): HtmlElement = - th( - cls("text-left text-sm font-semibold text-gray-900"), - content - ) - - override def dataCell(content: Modifier[HtmlElement]): HtmlElement = - td( - cls("whitespace-nowrap text-sm text-gray-500"), - content - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala index b937d45..12d01ae 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala @@ -3,7 +3,7 @@ import com.raquo.laminar.api.L.{*, given} import works.iterative.ui.components.laminar.HtmlTabular import works.iterative.ui.model.tables.Tabular -import works.iterative.ui.components.laminar.TableComponentsModule +import works.iterative.ui.components.laminar.tailwind.ui.TableComponentsModule import works.iterative.core.UserMessage import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given import works.iterative.ui.components.tailwind.ComponentContext diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala new file mode 100644 index 0000000..87f3e8d --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala @@ -0,0 +1,54 @@ +package works.iterative.ui.components.laminar.tailwind.color + +import com.raquo.laminar.api.L.{*, given} + +/** Complete color definition that can be rendered to CSS. + * + * Includes the area, kind and weight of the color. + */ +case class Color(area: ColorArea, color: ColorDef): + def toCSS: String = s"${area.name}-${color.toCSS}" + +object Color: + import ColorDef.given + + given colorToCSS: Conversion[Color, HtmlMod] with + def apply(c: Color) = cls(c.toCSS) + + given colorToSVGCSS: Conversion[Color, SvgMod] with + def apply(c: Color) = svg.cls(c.toCSS) + + given colorSignalToCSS: Conversion[Signal[Color], HtmlMod] with + def apply(c: Signal[Color]) = cls <-- c.map(_.toCSS) + + given colorSignalToSVGCSS: Conversion[Signal[Color], SvgMod] with + def apply(c: Signal[Color]) = svg.cls <-- c.map(_.toCSS) + + def current = ColorDef(ColorKind.current) + def inherit = ColorDef(ColorKind.inherit) + def transp = ColorDef(ColorKind.transp) + def auto = ColorDef(ColorKind.auto) + def black = ColorDef(ColorKind.black) + def white = ColorDef(ColorKind.white) + def slate(weight: ColorWeight) = ColorDef(ColorKind.slate, weight) + def gray(weight: ColorWeight) = ColorDef(ColorKind.gray, weight) + def zinc(weight: ColorWeight) = ColorDef(ColorKind.zinc, weight) + def neutral(weight: ColorWeight) = ColorDef(ColorKind.neutral, weight) + def stone(weight: ColorWeight) = ColorDef(ColorKind.stone, weight) + def red(weight: ColorWeight) = ColorDef(ColorKind.red, weight) + def orange(weight: ColorWeight) = ColorDef(ColorKind.orange, weight) + def amber(weight: ColorWeight) = ColorDef(ColorKind.amber, weight) + def yellow(weight: ColorWeight) = ColorDef(ColorKind.yellow, weight) + def lime(weight: ColorWeight) = ColorDef(ColorKind.lime, weight) + def green(weight: ColorWeight) = ColorDef(ColorKind.green, weight) + def emerald(weight: ColorWeight) = ColorDef(ColorKind.emerald, weight) + def teal(weight: ColorWeight) = ColorDef(ColorKind.teal, weight) + def cyan(weight: ColorWeight) = ColorDef(ColorKind.cyan, weight) + def sky(weight: ColorWeight) = ColorDef(ColorKind.sky, weight) + def blue(weight: ColorWeight) = ColorDef(ColorKind.blue, weight) + def indigo(weight: ColorWeight) = ColorDef(ColorKind.indigo, weight) + def violet(weight: ColorWeight) = ColorDef(ColorKind.violet, weight) + def purple(weight: ColorWeight) = ColorDef(ColorKind.purple, weight) + def fuchsia(weight: ColorWeight) = ColorDef(ColorKind.fuchsia, weight) + def pink(weight: ColorWeight) = ColorDef(ColorKind.pink, weight) + def rose(weight: ColorWeight) = ColorDef(ColorKind.rose, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala new file mode 100644 index 0000000..4130840 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala @@ -0,0 +1,16 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines the area the color should apply to, eg. background, text, border, + * etc. + */ +enum ColorArea(val name: String): + case bg extends ColorArea("bg") + case text extends ColorArea("text") + case decoration extends ColorArea("decoration") + case border extends ColorArea("border") + case outline extends ColorArea("outline") + case divide extends ColorArea("divide") + case ring extends ColorArea("ring") + case ringOffset extends ColorArea("ring-offset") + case shadow extends ColorArea("shadow") + case accent extends ColorArea("accent") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala new file mode 100644 index 0000000..fab9404 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala @@ -0,0 +1,43 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** A combination of ColorKind and ColorWeight, if applicable. + * + * By applying area we get the full Color definition. + */ +sealed trait ColorDef: + def toCSS: String + + def bg = Color(ColorArea.bg, this) + def text = Color(ColorArea.text, this) + def decoration = Color(ColorArea.decoration, this) + def border = Color(ColorArea.border, this) + def outline = Color(ColorArea.outline, this) + def divide = Color(ColorArea.divide, this) + def ring = Color(ColorArea.ring, this) + def ringOffset = Color(ColorArea.ringOffset, this) + def shadow = Color(ColorArea.shadow, this) + def accent = Color(ColorArea.accent, this) + +// TODO: create a macro that will output the tailwind class as string directly during compilation, where possible. +object ColorDef: + case class WeightedColorDef( + kind: ColorKind, + weight: ColorWeight + ) extends ColorDef: + override def toCSS: String = s"${kind.name}-${weight.value}" + + case class UnweightedColorDef( + kind: ColorKind + ) extends ColorDef: + override def toCSS: String = kind.name + + // TODO: check that the kind is valid unweighted kind + // that means current, inherit, auto, transparent, black, white + // tried using implicit evidence, but the type inference for enumerations + // tends to generalize to the enum, instead of the real type + def apply[T <: ColorKind](kind: T)(using + ev: T <:< ColorKind.Unweighted + ): ColorDef = + UnweightedColorDef(kind) + def apply(kind: ColorKind, weight: ColorWeight): ColorDef = + WeightedColorDef(kind, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala new file mode 100644 index 0000000..865166b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala @@ -0,0 +1,47 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines what color should be used, without specifying the area or weight. + */ +sealed abstract class ColorKind private (val name: String): + def apply(weight: ColorWeight): ColorDef = + ColorDef.WeightedColorDef(this, weight) + +object ColorKind: + trait Unweighted: + self: ColorKind => + override def apply(weight: ColorWeight): ColorDef = + ColorDef.UnweightedColorDef(self) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case object current extends ColorKind("current") with Unweighted + case object inherit extends ColorKind("inherit") with Unweighted + // Not present in for all methods + case object transp extends ColorKind("transparent") with Unweighted + // Seen in accent, not preset otherwise + case object auto extends ColorKind("auto") with Unweighted + // Black and white do not have weight + case object black extends ColorKind("black") with Unweighted + case object white extends ColorKind("white") with Unweighted + case object slate extends ColorKind("slate") + case object gray extends ColorKind("gray") + case object zinc extends ColorKind("zinc") + case object neutral extends ColorKind("neutral") + case object stone extends ColorKind("stone") + case object red extends ColorKind("red") + case object orange extends ColorKind("orange") + case object amber extends ColorKind("amber") + case object yellow extends ColorKind("yellow") + case object lime extends ColorKind("lime") + case object green extends ColorKind("green") + case object emerald extends ColorKind("emerald") + case object teal extends ColorKind("teal") + case object cyan extends ColorKind("cyan") + case object sky extends ColorKind("sky") + case object blue extends ColorKind("blue") + case object indigo extends ColorKind("indigo") + case object violet extends ColorKind("violet") + case object purple extends ColorKind("purple") + case object fuchsia extends ColorKind("fuchsia") + case object pink extends ColorKind("pink") + case object rose extends ColorKind("rose") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala new file mode 100644 index 0000000..6f1a4cc --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala @@ -0,0 +1,33 @@ +package works.iterative.ui.components.laminar.tailwind.color + +opaque type ColorWeight = String + +extension (c: ColorWeight) def value: String = c + +/** Defines weight of a color, eg. 50, 100, 200, etc. + * + * Tailwind-like. + */ +object ColorWeight: + inline given int50: Conversion[50, ColorWeight] with + inline def apply(i: 50) = "50" + inline given int100: Conversion[100, ColorWeight] with + inline def apply(i: 100) = "100" + inline given int200: Conversion[200, ColorWeight] with + inline def apply(i: 200) = "200" + inline given int300: Conversion[300, ColorWeight] with + inline def apply(i: 300) = "300" + inline given int400: Conversion[400, ColorWeight] with + inline def apply(i: 400) = "400" + inline given int500: Conversion[500, ColorWeight] with + inline def apply(i: 500) = "500" + inline given int600: Conversion[600, ColorWeight] with + inline def apply(i: 600) = "600" + inline given int700: Conversion[700, ColorWeight] with + inline def apply(i: 700) = "700" + inline given int800: Conversion[800, ColorWeight] with + inline def apply(i: 800) = "800" + inline given int900: Conversion[900, ColorWeight] with + inline def apply(i: 900) = "900" + inline given int950: Conversion[950, ColorWeight] with + inline def apply(i: 950) = "950" diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala new file mode 100644 index 0000000..21e21f0 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala @@ -0,0 +1,12 @@ +package works.iterative.ui.components.laminar.tailwind + +/** We need a generic Color model that can be used both on server and on client. + * + * We have adopted the Tailwind model for now. There is nothing inherently + * Tailwind-specific in the implementation, but all the values are taken from + * their palette and the area model is very HTML biased. + * + * Still, I think that it is a good starting point for exploration and will + * satisfy our current needs. + */ +package object color {} diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" new file mode 100644 index 0000000..5c4e218 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" @@ -0,0 +1,21 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.model.color.ColorKind +import works.iterative.ui.components.tailwind.ComponentContext +import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given +import works.iterative.ui.model.Tag + +trait TailwindUIBadgesComponentsModule(using ComponentContext): + + object badges: + override def tag(name: String, color: ColorKind): HtmlElement = + p( + cls( + "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" + ), + color(800).text, + color(100).bg, + name + ) diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" new file mode 100644 index 0000000..0c9ca25 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" @@ -0,0 +1,24 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} + +trait TailwindUIHeadingsComponentsModule: + val headings: HeadingsComponents = new HeadingsComponents + + class HeadingsComponents: + def section(title: Node, actions: HtmlElement*): HtmlElement = + div( + cls("border-b border-gray-200 pb-5"), + div( + cls("sm:flex sm:items-center sm:justify-between"), + h3(cls("text-base font-semibold leading-6 text-gray-900"), title), + div( + cls("mt-3 flex sm:ml-4 sm:mt-0"), + actions match + case Nil => emptyMod + case first :: rest => + first :: rest.map(_.amend(cls("ml-3"))) + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala new file mode 100644 index 0000000..16ca01b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala @@ -0,0 +1,19 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.laminar.tailwind.color.ColorKind + +trait BadgeComponentsModule: + + object badges: + def pill(name: String, color: ColorKind): HtmlElement = + p( + cls( + "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" + ), + color(800).text, + color(100).bg, + name + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala new file mode 100644 index 0000000..ebb2968 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} + +trait ButtonComponentsModule: + + object buttons: + + private inline def srHelp(text: String): Modifier[HtmlElement] = + span(cls := "sr-only", text) + + def primaryButton( + id: String, + text: Modifier[HtmlElement], + icon: Option[SvgElement] = None, + buttonType: String = "submit" + )(mods: Modifier[HtmlElement]*): HtmlElement = + button( + tpe(buttonType), + cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + icon, + text, + mods + ) + + def secondaryButton( + id: String, + text: Modifier[HtmlElement], + icon: Option[SvgElement] = None, + buttonType: String = "button" + )(mods: Modifier[HtmlElement]*): HtmlElement = + button( + tpe(buttonType), + cls( + "ml-2 bg-white inline-flex justify-center 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" + ), + icon, + text, + mods + ) + + def inlineButton(id: String, icon: SvgElement)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + button( + tpe := "button", + cls := "ml-1 inline-flex h-4 w-4 flex-shrink-0 rounded-full p-1 text-gray-400 hover:bg-gray-200 hover:text-gray-500", + srHelp(id), + icon + ) + + def iconButton(id: String, icon: SvgElement, srText: Option[String] = None)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + button( + tpe := "button", + cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", + icon, + srText.map(srHelp(_)) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala new file mode 100644 index 0000000..eb6dd5f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala @@ -0,0 +1,101 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import org.scalajs.dom.html +import com.raquo.laminar.modifiers.KeyUpdater + +trait FormComponentsModule: + self: IconsModule => + + object forms: + + def section( + title: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] + )(content: Modifier[HtmlElement]*): HtmlElement = + div( + cls("space-y-6 sm:space-y-5"), + div( + h3(cls("text-lg leading-6 font-medium text-gray-900"), title), + subtitle.map(st => p(cls("mt-1 max-w-2xl text-sm text-gray-500"), st)) + ), + div(cls("mt-6 sm:mt-5 space-y-6 sm:space-y-5"), content) + ) + + def label(labelText: String, forId: Option[String] = None)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + L.label( + cls("block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"), + forId.map(id => L.forId(id)), + labelText, + mods + ) + + def field( + label: Modifier[HtmlElement] + )(content: Modifier[HtmlElement]*): 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, + div(cls("mt-1 sm:mt-0 sm:col-span-2"), content) + ) + + def field( + id: String, + labelText: String, + input: HtmlElement, + help: Option[String] + ): HtmlElement = + field( + label(labelText, Some(id))() + )( + input.amend(idAttr(id)), + help.map(h => p(cls("mt-2 text-sm text-gray-500"), h)) + ) + + def form(mods: Modifier[HtmlElement]*)( + sections: Modifier[HtmlElement]* + )(actions: Modifier[HtmlElement]*): HtmlElement = + L.form( + cls("space-y-8 divide-y divide-gray-200"), + mods, + sections, + div( + cls("pt-5"), + div(cls("flex justify-end"), actions) + ) + ) + + def inlineForm( + mods: Modifier[HtmlElement]* + ): HtmlElement = + L.form(cls("flex space-x-4"), mods) + + def inputField( + id: String, + labelText: String, + placeholderText: Option[String] = None, + inputType: String = "text", + helpText: Option[String] = None + ): HtmlElement = + field( + id, + labelText, + input( + tpe(inputType), + cls( + "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:max-w-xs sm:text-sm sm:leading-6" + ), + placeholderText.map(placeholder(_)) + ), + helpText + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala new file mode 100644 index 0000000..52da07e --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala @@ -0,0 +1,22 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} + +trait HeadingsComponentsModule: + object headings: + def section(title: Node, actions: HtmlElement*): HtmlElement = + div( + cls("border-b border-gray-200 pb-5"), + div( + cls("sm:flex sm:items-center sm:justify-between"), + h3(cls("text-base font-semibold leading-6 text-gray-900"), title), + div( + cls("mt-3 flex sm:ml-4 sm:mt-0"), + actions match + case Nil => emptyMod + case first :: rest => + first :: rest.map(_.amend(cls("ml-3"))) + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala new file mode 100644 index 0000000..c543150 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala @@ -0,0 +1,112 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext +import works.iterative.ui.components.tailwind.CustomAttrs + +trait IconsModule: + object icons: + import svg.* + + object aria: + val hidden = CustomAttrs.svg.ariaHidden + + private def withDefault( + mods: Seq[Modifier[SvgElement]], + default: Modifier[SvgElement] + ): Modifier[SvgElement] = + if mods.isEmpty then default else mods + + def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-8 w-8"), + 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" + ) + ) + ) + + def close(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-2 w-2"), + stroke := "currentColor", + fill := "none", + viewBox := "0 0 8 8", + path( + strokeLineCap := "round", + strokeWidth := "1.5", + d := "M1 1l6 6m0-6L1 7" + ) + ) + + def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls("h-5 w-5 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 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") + ) + ) + + def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls("h-5 w-5 text-gray-400")), + 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") + ) + ) + + def `document-chart-bar-outline`( + mods: Modifier[SvgElement]* + ): SvgElement = + svg( + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + strokeWidth := "1.5", + stroke := "currentColor", + withDefault(mods, cls := "h-6 w-6"), + path( + strokeLineCap := "round", + strokeLineJoin := "round", + d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" + ) + ) + + def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-5 w-5"), + cls := "flex-shrink-0 text-gray-400", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", + clipRule := "evenodd" + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala new file mode 100644 index 0000000..02cbdb6 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala @@ -0,0 +1,11 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait LayoutModule: + object layout: + def card(content: Modifier[HtmlElement]*): HtmlElement = + div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala new file mode 100644 index 0000000..b652da9 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala @@ -0,0 +1,88 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.laminar.nodes.ReactiveHtmlElement +import org.scalajs.dom.html.Paragraph +import com.raquo.laminar.nodes.TextNode +import works.iterative.ui.components.laminar.tailwind.color.ColorKind + +trait ListComponentsModule: + self: BadgeComponentsModule => + + object list: + def label( + text: String, + color: ColorKind + ): HtmlElement = badges.pill(text, color) + + def item( + title: String, + subtitle: Option[String], + right: Modifier[HtmlElement] = emptyMod, + avatar: Option[Modifier[HtmlElement]] = None + ): LI = + li( + cls("group"), + div( + cls( + "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" + ), + avatar.map(a => + div( + cls("flex-shrink-0"), + div( + cls( + "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" + ), + a + ) + ) + ), + div( + cls("flex-1 min-w-0"), + p( + cls("text-sm font-medium text-gray-900"), + title, + span(cls("float-right"), right) + ), + subtitle.map(st => + p( + cls("text-sm text-gray-500 truncate"), + st + ) + ) + ) + ) + ) + + def unordered( + children: Modifier[HtmlElement] + ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = + ul( + cls("relative z-0 divide-y divide-gray-200"), + role("list"), + children + ) + + def listSection( + header: String, + list: HtmlElement + ): Div = + div( + cls("relative"), + div( + cls( + "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" + ), + header + ), + list + ) + + def navigation(sections: Modifier[HtmlElement]): HtmlElement = + navTag( + cls("flex-1 min-h-0 overflow-y-auto"), + sections + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ModalComponentsModule.scala new file mode 100644 index 0000000..6027404 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ModalComponentsModule.scala @@ -0,0 +1,54 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait ModalComponentsModule: + object modal: + def modalDialog( + content: HtmlElement, + closeMod: Modifier[HtmlElement] + ): 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( + cls("fixed inset-0 transition-opacity"), + div(cls("absolute inset-0 bg-gray-500 opacity-75")), + closeMod + ) + ) + + div( + cls("fixed inset-0 z-20 overflow-y-auto"), + div( + cls("text-center sm:block sm:p-0"), + overlay, + browserCenteringModalTrick, + 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-7xl sm:align-middle" + ), + content + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/PageComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/PageComponentsModule.scala new file mode 100644 index 0000000..d5a422e --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/PageComponentsModule.scala @@ -0,0 +1,49 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait PageComponentsModule: + + object page: + def container( + children: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), + children + ) + + def singleColumn( + header: Modifier[HtmlElement] + )(children: Modifier[HtmlElement]*): HtmlElement = + div( + cls("p-8 bg-gray-100 h-full"), + header, + children + ) + + def pageHeader( + title: Modifier[HtmlElement], + right: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] = None + ): HtmlElement = + div( + cls("pb-5 border-b border-gray-200"), + div(cls("float-right"), right), + h1( + cls("text-2xl leading-6 font-medium text-gray-900"), + title + ), + subtitle.map( + p( + cls("text-sm font-medium text-gray-500"), + _ + ) + ) + ) + + def clickable: Modifier[HtmlElement] = + cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TableComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TableComponentsModule.scala new file mode 100644 index 0000000..417ea56 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TableComponentsModule.scala @@ -0,0 +1,86 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L +import com.raquo.laminar.api.L.{*, given} + +trait TableComponentsModule: + + object tables: + + def tableSection( + title: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] = None, + actions: Modifier[HtmlElement]* + )( + table: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls("px-4 sm:px-6 lg:px-8"), + div( + cls("sm:flex sm:items-center"), + div( + cls("sm:flex-auto"), + h1(cls("text-base font-semibold leading-6 text-gray-900"), title), + subtitle.map(st => p(cls("mt-2 text-sm text-gray-700"), st)) + ), + div(cls("mt-4 sm:ml-16 sm:mt-0 sm:flex-none"), actions) + ), + div( + cls("mt-8 flow-root"), + div( + cls("-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"), + div( + cls("inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"), + table + ) + ) + ) + ) + + def simpleTable(header: Modifier[HtmlElement]*)( + body: Modifier[HtmlElement]* + ): HtmlElement = + table( + cls("min-w-full divide-y divide-gray-300"), + thead(header), + tbody(cls("divide-y divide-gray-200"), body) + ) + + def headerRow( + mods: Modifier[HtmlElement]* + )(cells: HtmlElement*): HtmlElement = + tr( + cells.zipWithIndex.map((c, i) => + if i == 0 then c.amend(cls("py-3.5 pl-4 pr-3 sm:pl-0")) + else if i == cells.length - 1 then + c.amend(cls("py-3.5 pr-4 pl-3 sm:pr-0")) + else c.amend(cls("py-3.5 px-3")) + ) + ) + + def dataRow( + mods: Modifier[HtmlElement]* + )(cells: HtmlElement*): HtmlElement = + tr( + mods, + cells.zipWithIndex.map((c, i) => + if i == 0 then c.amend(cls("py-4 pl-4 pr-3 sm:pl-0")) + else if i == cells.length - 1 then + c.amend(cls("py-4 pr-4 pl-3 sm:pr-0")) + else c.amend(cls("py-4 px-3")) + ) + ) + + def headerCell(content: Modifier[HtmlElement]): HtmlElement = + th( + cls("text-left text-sm font-semibold text-gray-900"), + content + ) + + def dataCell(content: Modifier[HtmlElement]): HtmlElement = + td( + cls("whitespace-nowrap text-sm text-gray-500"), + content + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TailwindUICatalogue.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TailwindUICatalogue.scala new file mode 100644 index 0000000..5027b10 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TailwindUICatalogue.scala @@ -0,0 +1,17 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} + +trait TailwindUICatalogueModule + extends BadgeComponentsModule + with ButtonComponentsModule + with HeadingsComponentsModule + with IconsModule + with FormComponentsModule + with LayoutModule + with ListComponentsModule + with ModalComponentsModule + with PageComponentsModule + with TableComponentsModule diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala index a2c637c..bfa063a 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala @@ -1,13 +1,11 @@ package works.iterative.ui.components.laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorDef import com.raquo.laminar.tags.HtmlTag case class Bin[Source, +Value]( label: String, description: Option[String | HtmlElement], - color: ColorDef, valueOf: Source => Value ): def map[NewValue]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala deleted file mode 100644 index 3aac0bf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala +++ /dev/null @@ -1,34 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ButtonComponentsModule: - def buttons: ButtonComponents - - trait ButtonComponents: - def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala deleted file mode 100644 index 646e73d..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala +++ /dev/null @@ -1,191 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.color.ColorDef -import com.raquo.laminar.tags.HtmlTag -import org.scalajs.dom.html.UList -import com.raquo.laminar.nodes.ReactiveHtmlElement -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind - -trait DashboardComponentsModule: - def dashboard: DashboardComponents - - // Only methods that create HtmlElements - trait DashboardComponents(using ComponentContext): - def section(label: String, children: Modifier[HtmlElement]): Div - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI - def cardInitials( - initials: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div - def count(label: String, value: Int, color: ColorDef): Span - def counts(children: HtmlElement*): Span - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - -trait DefaultDashboardComponentsModule(using ComponentContext) - extends DashboardComponentsModule: - override val dashboard: DashboardComponents = new DashboardComponents: - def section(label: String, children: Modifier[HtmlElement]): Div = - div( - cls("mt-3"), - h2( - cls("text-gray-500 text-xs font-medium uppercase tracking-wide"), - label - ), - children - ) - - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] = - ul( - cls( - "mt-3 grid gap-5 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" - ), - children - ) - - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI = - li( - div( - cls("col-span-1 flex shadow-sm rounded-md"), - initials, - div( - cls( - "flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate" - ), - div( - cls("flex-1 px-4 py-2 text-sm truncate"), - div( - cls("text-gray-900 font-medium hover:text-gray-600 truncate"), - label - ), - p(cls("text-gray-500"), children) - ) - ) - ) - ) - - def cardInitials( - id: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div = - div( - cls( - "flex-shrink-0 flex flex-col items-center justify-center w-16 text-white text-sm font-medium rounded-l-md" - ), - color.map(_(600).bg), - div(id), - children - ) - - def count(label: String, value: Int, color: ColorDef): Span = - span(color.text, title(label), s"${value}") - - def counts(children: HtmlElement*): Span = - span(interleave(children, span(" / "))) - - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div = - div( - cls("flex flex-col mt-2"), - div( - cls( - "align-middle min-w-full shadow sm:rounded-lg" - ), - L.table( - cls("min-w-full border-separate border-spacing-0"), - styleAttr("border-spacing: 0"), // Tailwind somehow doesn't work - thead( - tr( - cls("border-t border-gray-200"), - headers.map(h => - th( - cls( - "sticky top-0 z-10 px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - h - ) - ) - ) - ), - tbody(cls("bg-white divide-y divide-gray-100"), sections) - ) - ) - ) - - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - td( - cls("bg-gray-50 border-gray-200 border-t border-b"), - cls( - "sticky top-14 z-10 bg-gray-50 px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - colSpan(cs), - label - ), - mods - ) - - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - mods, - td( - cls( - "px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900 truncate" - ), - label - ), - children.map(e => - td( - cls( - "whitespace-nowrap px-2 py-2 text-sm text-gray-500" - ), - e - ) - ) - ) - - private def interleave( - a: Seq[HtmlElement], - separator: => HtmlElement - ): Seq[HtmlElement] = - if a.size < 2 then a - else a.init.flatMap(n => Seq(n, separator)) :+ a.last diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala deleted file mode 100644 index a45beaf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.FileRef - -trait DetailComponentsModule: - - def details: DetailComponents - - trait DetailComponents: - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement - - def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement - - def files(fs: List[FileRef]): HtmlElement - - def file(f: FileRef): HtmlElement - -trait DefaultDetailComponentsModule(using ctx: ComponentContext) - extends DetailComponentsModule: - self: IconsModule => - - override val details: DetailComponents = new DetailComponents: - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement = - div( - div( - cls := "px-4 sm:px-0", - h3( - cls := "text-base font-semibold leading-7 text-gray-900", - title - ), - subtitle.map(st => - p( - cls := "mt-1 max-w-2xl text-sm leading-6 text-gray-500", - st - ) - ) - // TODO: actions - ), - div( - cls := "mt-6 border-t border-gray-100", - dl( - cls := "divide-y divide-gray-100", - fields - ) - ) - ) - - override def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0", - dt( - cls := "text-sm font-medium leading-6 text-gray-900", - title - ), - dd( - cls := "mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0", - content - ) - ) - - override def files(fs: List[FileRef]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-100 rounded-md border border-gray-200", - fs.map(file) - ) - - override def file(f: FileRef): HtmlElement = - li( - cls := "flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6", - div( - cls := "flex w-0 flex-1 items-center", - icons.`paper-clip-solid`(), - div( - cls := "ml-4 flex min-w-0 flex-1 gap-2", - span( - cls := "truncate font-medium", - f.name - ), - f.size.map(size => span(cls := "flex-shrink-0 text-gray-400", size)) - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - "Uložit" - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala new file mode 100644 index 0000000..f6c583f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait FormComponents(using ctx: ComponentContext) extends LocalDateSelectModule: + def searchIcon: SvgElement + + def renderLocalDateSelect( + id: String, + labelText: Option[String], + placeholderText: Option[String], + mods: LocalDateSelect => Modifier[HtmlElement] + ): HtmlElement = + div( + labelText, + input( + idAttr(id), + nameAttr(id), + autoComplete("date"), + tpe("date"), + cls( + "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ), + placeholderText.map(placeholder(_)), + mods(localDateSelect) + ) + ) + + def searchField( + id: String, + placeholderText: Option[String] = None + )( + mods: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls := "flex-1 min-w-0", + label(ctx.messages("forms.search.label"), forId(id), cls("sr-only")), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + searchIcon + ), + input( + tpe := "search", + nameAttr := "search", + idAttr := id, + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholderText + .orElse( + ctx.messages + .opt( + s"forms.search.${id}.placeholder", + s"form.search.placeholder" + ) + ) + .map(placeholder(_)), + mods + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala deleted file mode 100644 index 7714490..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala +++ /dev/null @@ -1,82 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -/** Form components - * - * A form is a collection of sections, each of which contains a collection of - * fields. - * - * Form -> * Section -> * Field - * -> * Action - * - * Each Field has an `id`, `label` and `input` element. The `id` is used to - * link the label to the input. - * - * The `input` element can be a simple text input, or a more complex component - * such as a date picker. The caller is expected to make the input work. - */ -trait FormComponentsModule extends LocalDateSelectModule: - def forms: FormComponents - - trait FormComponents: - - /** Layout the sections and actions of a form */ - def form(mods: Modifier[HtmlElement]*)(sections: Modifier[HtmlElement]*)( - actions: Modifier[HtmlElement]* - ): HtmlElement - - /** Section layout, with a title, optional subtitle and any content. */ - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label and any content */ - def field(label: Modifier[HtmlElement])( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label, input and optional help text */ - def field( - id: String, - label: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement - - def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout an inline form element */ - def inlineForm(mods: Modifier[HtmlElement]*): HtmlElement - - /** An input field with a search icon */ - def searchField(id: String, placeholderText: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement - - /** LocalDate input */ - def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala deleted file mode 100644 index 8acbe7f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala +++ /dev/null @@ -1,28 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.Tag - -trait GenericComponentsModule: - - def generic: GenericComponents - - trait GenericComponents: - def tag(name: String, color: ColorKind): HtmlElement - def tag(t: Tag): HtmlElement = tag(t.value, t.color) - -trait DefaultGenericComponentsModule(using ComponentContext) - extends GenericComponentsModule: - override val generic: GenericComponents = new GenericComponents: - override def tag(name: String, color: ColorKind): HtmlElement = - p( - cls( - "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" - ), - color(800).text, - color(100).bg, - name - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala deleted file mode 100644 index 8cfc1d9..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala +++ /dev/null @@ -1,123 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.CustomAttrs - -trait IconsModule: - def icons: Icons - - trait Icons: - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement - def close(mods: Modifier[SvgElement]*): SvgElement - def `search-solid`(mods: Modifier[SvgElement]*): SvgElement - def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement - def `document-chart-bar-outline`(mods: Modifier[SvgElement]*): SvgElement - def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement - -trait DefaultIconsModule(using ComponentContext) extends IconsModule: - override val icons: Icons = new Icons: - import svg.* - - object aria: - val hidden = CustomAttrs.svg.ariaHidden - - private def withDefault( - mods: Seq[Modifier[SvgElement]], - default: Modifier[SvgElement] - ): Modifier[SvgElement] = - if mods.isEmpty then default else mods - - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-8 w-8"), - 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" - ) - ) - ) - - override def close(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-2 w-2"), - stroke := "currentColor", - fill := "none", - viewBox := "0 0 8 8", - path( - strokeLineCap := "round", - strokeWidth := "1.5", - d := "M1 1l6 6m0-6L1 7" - ) - ) - - override def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 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 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") - ) - ) - - override def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 text-gray-400")), - 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") - ) - ) - - override def `document-chart-bar-outline`( - mods: Modifier[SvgElement]* - ): SvgElement = - svg( - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - strokeWidth := "1.5", - stroke := "currentColor", - withDefault(mods, cls := "h-6 w-6"), - path( - strokeLineCap := "round", - strokeLineJoin := "round", - d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" - ) - ) - - override def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-5 w-5"), - cls := "flex-shrink-0 text-gray-400", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", - clipRule := "evenodd" - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala deleted file mode 100644 index 95ab654..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait LayoutModule: - def layout: LayoutComponents - - trait LayoutComponents: - def card(content: Modifier[HtmlElement]*): HtmlElement - -trait DefaultLayoutModule(using ctx: ComponentContext) extends LayoutModule: - override val layout: LayoutComponents = new LayoutComponents: - override def card(content: Modifier[HtmlElement]*): HtmlElement = - div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala deleted file mode 100644 index 5946974..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom.html.Paragraph -import com.raquo.laminar.nodes.TextNode - -trait ListComponentsModule: - - def list: ListComponents - - trait ListComponents(using ComponentContext): - def label(text: String, color: ColorKind): HtmlElement - def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI - def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] - def listSection(header: String, list: HtmlElement): Div - def navigation(sections: Modifier[HtmlElement]): HtmlElement - -trait DefaultListComponentsModule(using ComponentContext) - extends ListComponentsModule: - self: GenericComponentsModule => - - override val list: ListComponents = new ListComponents: - override def label( - text: String, - color: ColorKind - ): HtmlElement = generic.tag(text, color) - - override def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI = - li( - cls("group"), - div( - cls( - "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" - ), - avatar.map(a => - div( - cls("flex-shrink-0"), - div( - cls( - "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" - ), - a - ) - ) - ), - div( - cls("flex-1 min-w-0"), - p( - cls("text-sm font-medium text-gray-900"), - title, - span(cls("float-right"), right) - ), - subtitle.map(st => - p( - cls("text-sm text-gray-500 truncate"), - st - ) - ) - ) - ) - ) - - override def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = - ul( - cls("relative z-0 divide-y divide-gray-200"), - role("list"), - children - ) - - override def listSection( - header: String, - list: HtmlElement - ): Div = - div( - cls("relative"), - div( - cls( - "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" - ), - header - ), - list - ) - - override def navigation(sections: Modifier[HtmlElement]): HtmlElement = - navTag( - cls("flex-1 min-h-0 overflow-y-auto"), - sections - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala deleted file mode 100644 index 5b5490f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ModalComponentsModule: - - def modal: ModalComponents - - trait ModalComponents: - def modalDialog( - content: HtmlElement, - onClose: Modifier[HtmlElement] - ): HtmlElement - -trait DefaultModalComponentsModule(using ctx: ComponentContext) - extends ModalComponentsModule: - - override val modal: ModalComponents = new ModalComponents: - - override def modalDialog( - content: HtmlElement, - closeMod: Modifier[HtmlElement] - ): 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( - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")), - closeMod - ) - ) - - div( - cls("fixed inset-0 z-20 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - 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-7xl sm:align-middle" - ), - content - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala deleted file mode 100644 index b713d8a..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala +++ /dev/null @@ -1,71 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait PageComponentsModule: - - val page: PageComponents - - trait PageComponents: - def container( - children: Modifier[HtmlElement]* - ): HtmlElement - - def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement - - def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement] = emptyMod, - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement - - /** Visage for clickable text, like links - */ - def clickable: Modifier[HtmlElement] - -trait DefaultPageComponentsModule(using ComponentContext) - extends PageComponentsModule: - - override val page: PageComponents = new PageComponents: - override def container( - children: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), - children - ) - - override def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement = - div( - cls("p-8 bg-gray-100 h-full"), - header, - children - ) - - override def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement = - div( - cls("pb-5 border-b border-gray-200"), - div(cls("float-right"), right), - h1( - cls("text-2xl leading-6 font-medium text-gray-900"), - title - ), - subtitle.map( - p( - cls("text-sm font-medium text-gray-500"), - _ - ) - ) - ) - - override def clickable: Modifier[HtmlElement] = - cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala deleted file mode 100644 index f70ee87..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala +++ /dev/null @@ -1,108 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} - -trait TableComponentsModule: - def tables: TableComponents - - trait TableComponents: - def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )(table: Modifier[HtmlElement]*): HtmlElement - - def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement - - def headerRow(mods: Modifier[HtmlElement]*)( - cells: HtmlElement* - ): HtmlElement - - def dataRow(mods: Modifier[HtmlElement]*)(cells: HtmlElement*): HtmlElement - - def headerCell(content: Modifier[HtmlElement]): HtmlElement - - def dataCell(content: Modifier[HtmlElement]): HtmlElement - -trait DefaultTableComponentsModule extends TableComponentsModule: - - override lazy val tables: TableComponents = new TableComponents: - - override def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )( - table: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("px-4 sm:px-6 lg:px-8"), - div( - cls("sm:flex sm:items-center"), - div( - cls("sm:flex-auto"), - h1(cls("text-base font-semibold leading-6 text-gray-900"), title), - subtitle.map(st => p(cls("mt-2 text-sm text-gray-700"), st)) - ), - div(cls("mt-4 sm:ml-16 sm:mt-0 sm:flex-none"), actions) - ), - div( - cls("mt-8 flow-root"), - div( - cls("-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"), - div( - cls("inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"), - table - ) - ) - ) - ) - - override def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement = - table( - cls("min-w-full divide-y divide-gray-300"), - thead(header), - tbody(cls("divide-y divide-gray-200"), body) - ) - - override def headerRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-3.5 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-3.5 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-3.5 px-3")) - ) - ) - - override def dataRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - mods, - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-4 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-4 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-4 px-3")) - ) - ) - - override def headerCell(content: Modifier[HtmlElement]): HtmlElement = - th( - cls("text-left text-sm font-semibold text-gray-900"), - content - ) - - override def dataCell(content: Modifier[HtmlElement]): HtmlElement = - td( - cls("whitespace-nowrap text-sm text-gray-500"), - content - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala index b937d45..12d01ae 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala @@ -3,7 +3,7 @@ import com.raquo.laminar.api.L.{*, given} import works.iterative.ui.components.laminar.HtmlTabular import works.iterative.ui.model.tables.Tabular -import works.iterative.ui.components.laminar.TableComponentsModule +import works.iterative.ui.components.laminar.tailwind.ui.TableComponentsModule import works.iterative.core.UserMessage import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given import works.iterative.ui.components.tailwind.ComponentContext diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala new file mode 100644 index 0000000..87f3e8d --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala @@ -0,0 +1,54 @@ +package works.iterative.ui.components.laminar.tailwind.color + +import com.raquo.laminar.api.L.{*, given} + +/** Complete color definition that can be rendered to CSS. + * + * Includes the area, kind and weight of the color. + */ +case class Color(area: ColorArea, color: ColorDef): + def toCSS: String = s"${area.name}-${color.toCSS}" + +object Color: + import ColorDef.given + + given colorToCSS: Conversion[Color, HtmlMod] with + def apply(c: Color) = cls(c.toCSS) + + given colorToSVGCSS: Conversion[Color, SvgMod] with + def apply(c: Color) = svg.cls(c.toCSS) + + given colorSignalToCSS: Conversion[Signal[Color], HtmlMod] with + def apply(c: Signal[Color]) = cls <-- c.map(_.toCSS) + + given colorSignalToSVGCSS: Conversion[Signal[Color], SvgMod] with + def apply(c: Signal[Color]) = svg.cls <-- c.map(_.toCSS) + + def current = ColorDef(ColorKind.current) + def inherit = ColorDef(ColorKind.inherit) + def transp = ColorDef(ColorKind.transp) + def auto = ColorDef(ColorKind.auto) + def black = ColorDef(ColorKind.black) + def white = ColorDef(ColorKind.white) + def slate(weight: ColorWeight) = ColorDef(ColorKind.slate, weight) + def gray(weight: ColorWeight) = ColorDef(ColorKind.gray, weight) + def zinc(weight: ColorWeight) = ColorDef(ColorKind.zinc, weight) + def neutral(weight: ColorWeight) = ColorDef(ColorKind.neutral, weight) + def stone(weight: ColorWeight) = ColorDef(ColorKind.stone, weight) + def red(weight: ColorWeight) = ColorDef(ColorKind.red, weight) + def orange(weight: ColorWeight) = ColorDef(ColorKind.orange, weight) + def amber(weight: ColorWeight) = ColorDef(ColorKind.amber, weight) + def yellow(weight: ColorWeight) = ColorDef(ColorKind.yellow, weight) + def lime(weight: ColorWeight) = ColorDef(ColorKind.lime, weight) + def green(weight: ColorWeight) = ColorDef(ColorKind.green, weight) + def emerald(weight: ColorWeight) = ColorDef(ColorKind.emerald, weight) + def teal(weight: ColorWeight) = ColorDef(ColorKind.teal, weight) + def cyan(weight: ColorWeight) = ColorDef(ColorKind.cyan, weight) + def sky(weight: ColorWeight) = ColorDef(ColorKind.sky, weight) + def blue(weight: ColorWeight) = ColorDef(ColorKind.blue, weight) + def indigo(weight: ColorWeight) = ColorDef(ColorKind.indigo, weight) + def violet(weight: ColorWeight) = ColorDef(ColorKind.violet, weight) + def purple(weight: ColorWeight) = ColorDef(ColorKind.purple, weight) + def fuchsia(weight: ColorWeight) = ColorDef(ColorKind.fuchsia, weight) + def pink(weight: ColorWeight) = ColorDef(ColorKind.pink, weight) + def rose(weight: ColorWeight) = ColorDef(ColorKind.rose, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala new file mode 100644 index 0000000..4130840 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala @@ -0,0 +1,16 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines the area the color should apply to, eg. background, text, border, + * etc. + */ +enum ColorArea(val name: String): + case bg extends ColorArea("bg") + case text extends ColorArea("text") + case decoration extends ColorArea("decoration") + case border extends ColorArea("border") + case outline extends ColorArea("outline") + case divide extends ColorArea("divide") + case ring extends ColorArea("ring") + case ringOffset extends ColorArea("ring-offset") + case shadow extends ColorArea("shadow") + case accent extends ColorArea("accent") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala new file mode 100644 index 0000000..fab9404 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala @@ -0,0 +1,43 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** A combination of ColorKind and ColorWeight, if applicable. + * + * By applying area we get the full Color definition. + */ +sealed trait ColorDef: + def toCSS: String + + def bg = Color(ColorArea.bg, this) + def text = Color(ColorArea.text, this) + def decoration = Color(ColorArea.decoration, this) + def border = Color(ColorArea.border, this) + def outline = Color(ColorArea.outline, this) + def divide = Color(ColorArea.divide, this) + def ring = Color(ColorArea.ring, this) + def ringOffset = Color(ColorArea.ringOffset, this) + def shadow = Color(ColorArea.shadow, this) + def accent = Color(ColorArea.accent, this) + +// TODO: create a macro that will output the tailwind class as string directly during compilation, where possible. +object ColorDef: + case class WeightedColorDef( + kind: ColorKind, + weight: ColorWeight + ) extends ColorDef: + override def toCSS: String = s"${kind.name}-${weight.value}" + + case class UnweightedColorDef( + kind: ColorKind + ) extends ColorDef: + override def toCSS: String = kind.name + + // TODO: check that the kind is valid unweighted kind + // that means current, inherit, auto, transparent, black, white + // tried using implicit evidence, but the type inference for enumerations + // tends to generalize to the enum, instead of the real type + def apply[T <: ColorKind](kind: T)(using + ev: T <:< ColorKind.Unweighted + ): ColorDef = + UnweightedColorDef(kind) + def apply(kind: ColorKind, weight: ColorWeight): ColorDef = + WeightedColorDef(kind, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala new file mode 100644 index 0000000..865166b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala @@ -0,0 +1,47 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines what color should be used, without specifying the area or weight. + */ +sealed abstract class ColorKind private (val name: String): + def apply(weight: ColorWeight): ColorDef = + ColorDef.WeightedColorDef(this, weight) + +object ColorKind: + trait Unweighted: + self: ColorKind => + override def apply(weight: ColorWeight): ColorDef = + ColorDef.UnweightedColorDef(self) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case object current extends ColorKind("current") with Unweighted + case object inherit extends ColorKind("inherit") with Unweighted + // Not present in for all methods + case object transp extends ColorKind("transparent") with Unweighted + // Seen in accent, not preset otherwise + case object auto extends ColorKind("auto") with Unweighted + // Black and white do not have weight + case object black extends ColorKind("black") with Unweighted + case object white extends ColorKind("white") with Unweighted + case object slate extends ColorKind("slate") + case object gray extends ColorKind("gray") + case object zinc extends ColorKind("zinc") + case object neutral extends ColorKind("neutral") + case object stone extends ColorKind("stone") + case object red extends ColorKind("red") + case object orange extends ColorKind("orange") + case object amber extends ColorKind("amber") + case object yellow extends ColorKind("yellow") + case object lime extends ColorKind("lime") + case object green extends ColorKind("green") + case object emerald extends ColorKind("emerald") + case object teal extends ColorKind("teal") + case object cyan extends ColorKind("cyan") + case object sky extends ColorKind("sky") + case object blue extends ColorKind("blue") + case object indigo extends ColorKind("indigo") + case object violet extends ColorKind("violet") + case object purple extends ColorKind("purple") + case object fuchsia extends ColorKind("fuchsia") + case object pink extends ColorKind("pink") + case object rose extends ColorKind("rose") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala new file mode 100644 index 0000000..6f1a4cc --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala @@ -0,0 +1,33 @@ +package works.iterative.ui.components.laminar.tailwind.color + +opaque type ColorWeight = String + +extension (c: ColorWeight) def value: String = c + +/** Defines weight of a color, eg. 50, 100, 200, etc. + * + * Tailwind-like. + */ +object ColorWeight: + inline given int50: Conversion[50, ColorWeight] with + inline def apply(i: 50) = "50" + inline given int100: Conversion[100, ColorWeight] with + inline def apply(i: 100) = "100" + inline given int200: Conversion[200, ColorWeight] with + inline def apply(i: 200) = "200" + inline given int300: Conversion[300, ColorWeight] with + inline def apply(i: 300) = "300" + inline given int400: Conversion[400, ColorWeight] with + inline def apply(i: 400) = "400" + inline given int500: Conversion[500, ColorWeight] with + inline def apply(i: 500) = "500" + inline given int600: Conversion[600, ColorWeight] with + inline def apply(i: 600) = "600" + inline given int700: Conversion[700, ColorWeight] with + inline def apply(i: 700) = "700" + inline given int800: Conversion[800, ColorWeight] with + inline def apply(i: 800) = "800" + inline given int900: Conversion[900, ColorWeight] with + inline def apply(i: 900) = "900" + inline given int950: Conversion[950, ColorWeight] with + inline def apply(i: 950) = "950" diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala new file mode 100644 index 0000000..21e21f0 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala @@ -0,0 +1,12 @@ +package works.iterative.ui.components.laminar.tailwind + +/** We need a generic Color model that can be used both on server and on client. + * + * We have adopted the Tailwind model for now. There is nothing inherently + * Tailwind-specific in the implementation, but all the values are taken from + * their palette and the area model is very HTML biased. + * + * Still, I think that it is a good starting point for exploration and will + * satisfy our current needs. + */ +package object color {} diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" new file mode 100644 index 0000000..5c4e218 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" @@ -0,0 +1,21 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.model.color.ColorKind +import works.iterative.ui.components.tailwind.ComponentContext +import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given +import works.iterative.ui.model.Tag + +trait TailwindUIBadgesComponentsModule(using ComponentContext): + + object badges: + override def tag(name: String, color: ColorKind): HtmlElement = + p( + cls( + "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" + ), + color(800).text, + color(100).bg, + name + ) diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" new file mode 100644 index 0000000..0c9ca25 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" @@ -0,0 +1,24 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} + +trait TailwindUIHeadingsComponentsModule: + val headings: HeadingsComponents = new HeadingsComponents + + class HeadingsComponents: + def section(title: Node, actions: HtmlElement*): HtmlElement = + div( + cls("border-b border-gray-200 pb-5"), + div( + cls("sm:flex sm:items-center sm:justify-between"), + h3(cls("text-base font-semibold leading-6 text-gray-900"), title), + div( + cls("mt-3 flex sm:ml-4 sm:mt-0"), + actions match + case Nil => emptyMod + case first :: rest => + first :: rest.map(_.amend(cls("ml-3"))) + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala new file mode 100644 index 0000000..16ca01b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala @@ -0,0 +1,19 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.laminar.tailwind.color.ColorKind + +trait BadgeComponentsModule: + + object badges: + def pill(name: String, color: ColorKind): HtmlElement = + p( + cls( + "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" + ), + color(800).text, + color(100).bg, + name + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala new file mode 100644 index 0000000..ebb2968 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} + +trait ButtonComponentsModule: + + object buttons: + + private inline def srHelp(text: String): Modifier[HtmlElement] = + span(cls := "sr-only", text) + + def primaryButton( + id: String, + text: Modifier[HtmlElement], + icon: Option[SvgElement] = None, + buttonType: String = "submit" + )(mods: Modifier[HtmlElement]*): HtmlElement = + button( + tpe(buttonType), + cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + icon, + text, + mods + ) + + def secondaryButton( + id: String, + text: Modifier[HtmlElement], + icon: Option[SvgElement] = None, + buttonType: String = "button" + )(mods: Modifier[HtmlElement]*): HtmlElement = + button( + tpe(buttonType), + cls( + "ml-2 bg-white inline-flex justify-center 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" + ), + icon, + text, + mods + ) + + def inlineButton(id: String, icon: SvgElement)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + button( + tpe := "button", + cls := "ml-1 inline-flex h-4 w-4 flex-shrink-0 rounded-full p-1 text-gray-400 hover:bg-gray-200 hover:text-gray-500", + srHelp(id), + icon + ) + + def iconButton(id: String, icon: SvgElement, srText: Option[String] = None)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + button( + tpe := "button", + cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", + icon, + srText.map(srHelp(_)) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala new file mode 100644 index 0000000..eb6dd5f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala @@ -0,0 +1,101 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import org.scalajs.dom.html +import com.raquo.laminar.modifiers.KeyUpdater + +trait FormComponentsModule: + self: IconsModule => + + object forms: + + def section( + title: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] + )(content: Modifier[HtmlElement]*): HtmlElement = + div( + cls("space-y-6 sm:space-y-5"), + div( + h3(cls("text-lg leading-6 font-medium text-gray-900"), title), + subtitle.map(st => p(cls("mt-1 max-w-2xl text-sm text-gray-500"), st)) + ), + div(cls("mt-6 sm:mt-5 space-y-6 sm:space-y-5"), content) + ) + + def label(labelText: String, forId: Option[String] = None)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + L.label( + cls("block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"), + forId.map(id => L.forId(id)), + labelText, + mods + ) + + def field( + label: Modifier[HtmlElement] + )(content: Modifier[HtmlElement]*): 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, + div(cls("mt-1 sm:mt-0 sm:col-span-2"), content) + ) + + def field( + id: String, + labelText: String, + input: HtmlElement, + help: Option[String] + ): HtmlElement = + field( + label(labelText, Some(id))() + )( + input.amend(idAttr(id)), + help.map(h => p(cls("mt-2 text-sm text-gray-500"), h)) + ) + + def form(mods: Modifier[HtmlElement]*)( + sections: Modifier[HtmlElement]* + )(actions: Modifier[HtmlElement]*): HtmlElement = + L.form( + cls("space-y-8 divide-y divide-gray-200"), + mods, + sections, + div( + cls("pt-5"), + div(cls("flex justify-end"), actions) + ) + ) + + def inlineForm( + mods: Modifier[HtmlElement]* + ): HtmlElement = + L.form(cls("flex space-x-4"), mods) + + def inputField( + id: String, + labelText: String, + placeholderText: Option[String] = None, + inputType: String = "text", + helpText: Option[String] = None + ): HtmlElement = + field( + id, + labelText, + input( + tpe(inputType), + cls( + "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:max-w-xs sm:text-sm sm:leading-6" + ), + placeholderText.map(placeholder(_)) + ), + helpText + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala new file mode 100644 index 0000000..52da07e --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala @@ -0,0 +1,22 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} + +trait HeadingsComponentsModule: + object headings: + def section(title: Node, actions: HtmlElement*): HtmlElement = + div( + cls("border-b border-gray-200 pb-5"), + div( + cls("sm:flex sm:items-center sm:justify-between"), + h3(cls("text-base font-semibold leading-6 text-gray-900"), title), + div( + cls("mt-3 flex sm:ml-4 sm:mt-0"), + actions match + case Nil => emptyMod + case first :: rest => + first :: rest.map(_.amend(cls("ml-3"))) + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala new file mode 100644 index 0000000..c543150 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala @@ -0,0 +1,112 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext +import works.iterative.ui.components.tailwind.CustomAttrs + +trait IconsModule: + object icons: + import svg.* + + object aria: + val hidden = CustomAttrs.svg.ariaHidden + + private def withDefault( + mods: Seq[Modifier[SvgElement]], + default: Modifier[SvgElement] + ): Modifier[SvgElement] = + if mods.isEmpty then default else mods + + def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-8 w-8"), + 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" + ) + ) + ) + + def close(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-2 w-2"), + stroke := "currentColor", + fill := "none", + viewBox := "0 0 8 8", + path( + strokeLineCap := "round", + strokeWidth := "1.5", + d := "M1 1l6 6m0-6L1 7" + ) + ) + + def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls("h-5 w-5 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 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") + ) + ) + + def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls("h-5 w-5 text-gray-400")), + 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") + ) + ) + + def `document-chart-bar-outline`( + mods: Modifier[SvgElement]* + ): SvgElement = + svg( + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + strokeWidth := "1.5", + stroke := "currentColor", + withDefault(mods, cls := "h-6 w-6"), + path( + strokeLineCap := "round", + strokeLineJoin := "round", + d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" + ) + ) + + def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-5 w-5"), + cls := "flex-shrink-0 text-gray-400", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", + clipRule := "evenodd" + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala new file mode 100644 index 0000000..02cbdb6 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala @@ -0,0 +1,11 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait LayoutModule: + object layout: + def card(content: Modifier[HtmlElement]*): HtmlElement = + div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala new file mode 100644 index 0000000..b652da9 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala @@ -0,0 +1,88 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.laminar.nodes.ReactiveHtmlElement +import org.scalajs.dom.html.Paragraph +import com.raquo.laminar.nodes.TextNode +import works.iterative.ui.components.laminar.tailwind.color.ColorKind + +trait ListComponentsModule: + self: BadgeComponentsModule => + + object list: + def label( + text: String, + color: ColorKind + ): HtmlElement = badges.pill(text, color) + + def item( + title: String, + subtitle: Option[String], + right: Modifier[HtmlElement] = emptyMod, + avatar: Option[Modifier[HtmlElement]] = None + ): LI = + li( + cls("group"), + div( + cls( + "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" + ), + avatar.map(a => + div( + cls("flex-shrink-0"), + div( + cls( + "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" + ), + a + ) + ) + ), + div( + cls("flex-1 min-w-0"), + p( + cls("text-sm font-medium text-gray-900"), + title, + span(cls("float-right"), right) + ), + subtitle.map(st => + p( + cls("text-sm text-gray-500 truncate"), + st + ) + ) + ) + ) + ) + + def unordered( + children: Modifier[HtmlElement] + ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = + ul( + cls("relative z-0 divide-y divide-gray-200"), + role("list"), + children + ) + + def listSection( + header: String, + list: HtmlElement + ): Div = + div( + cls("relative"), + div( + cls( + "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" + ), + header + ), + list + ) + + def navigation(sections: Modifier[HtmlElement]): HtmlElement = + navTag( + cls("flex-1 min-h-0 overflow-y-auto"), + sections + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ModalComponentsModule.scala new file mode 100644 index 0000000..6027404 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ModalComponentsModule.scala @@ -0,0 +1,54 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait ModalComponentsModule: + object modal: + def modalDialog( + content: HtmlElement, + closeMod: Modifier[HtmlElement] + ): 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( + cls("fixed inset-0 transition-opacity"), + div(cls("absolute inset-0 bg-gray-500 opacity-75")), + closeMod + ) + ) + + div( + cls("fixed inset-0 z-20 overflow-y-auto"), + div( + cls("text-center sm:block sm:p-0"), + overlay, + browserCenteringModalTrick, + 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-7xl sm:align-middle" + ), + content + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/PageComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/PageComponentsModule.scala new file mode 100644 index 0000000..d5a422e --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/PageComponentsModule.scala @@ -0,0 +1,49 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait PageComponentsModule: + + object page: + def container( + children: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), + children + ) + + def singleColumn( + header: Modifier[HtmlElement] + )(children: Modifier[HtmlElement]*): HtmlElement = + div( + cls("p-8 bg-gray-100 h-full"), + header, + children + ) + + def pageHeader( + title: Modifier[HtmlElement], + right: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] = None + ): HtmlElement = + div( + cls("pb-5 border-b border-gray-200"), + div(cls("float-right"), right), + h1( + cls("text-2xl leading-6 font-medium text-gray-900"), + title + ), + subtitle.map( + p( + cls("text-sm font-medium text-gray-500"), + _ + ) + ) + ) + + def clickable: Modifier[HtmlElement] = + cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TableComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TableComponentsModule.scala new file mode 100644 index 0000000..417ea56 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TableComponentsModule.scala @@ -0,0 +1,86 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L +import com.raquo.laminar.api.L.{*, given} + +trait TableComponentsModule: + + object tables: + + def tableSection( + title: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] = None, + actions: Modifier[HtmlElement]* + )( + table: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls("px-4 sm:px-6 lg:px-8"), + div( + cls("sm:flex sm:items-center"), + div( + cls("sm:flex-auto"), + h1(cls("text-base font-semibold leading-6 text-gray-900"), title), + subtitle.map(st => p(cls("mt-2 text-sm text-gray-700"), st)) + ), + div(cls("mt-4 sm:ml-16 sm:mt-0 sm:flex-none"), actions) + ), + div( + cls("mt-8 flow-root"), + div( + cls("-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"), + div( + cls("inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"), + table + ) + ) + ) + ) + + def simpleTable(header: Modifier[HtmlElement]*)( + body: Modifier[HtmlElement]* + ): HtmlElement = + table( + cls("min-w-full divide-y divide-gray-300"), + thead(header), + tbody(cls("divide-y divide-gray-200"), body) + ) + + def headerRow( + mods: Modifier[HtmlElement]* + )(cells: HtmlElement*): HtmlElement = + tr( + cells.zipWithIndex.map((c, i) => + if i == 0 then c.amend(cls("py-3.5 pl-4 pr-3 sm:pl-0")) + else if i == cells.length - 1 then + c.amend(cls("py-3.5 pr-4 pl-3 sm:pr-0")) + else c.amend(cls("py-3.5 px-3")) + ) + ) + + def dataRow( + mods: Modifier[HtmlElement]* + )(cells: HtmlElement*): HtmlElement = + tr( + mods, + cells.zipWithIndex.map((c, i) => + if i == 0 then c.amend(cls("py-4 pl-4 pr-3 sm:pl-0")) + else if i == cells.length - 1 then + c.amend(cls("py-4 pr-4 pl-3 sm:pr-0")) + else c.amend(cls("py-4 px-3")) + ) + ) + + def headerCell(content: Modifier[HtmlElement]): HtmlElement = + th( + cls("text-left text-sm font-semibold text-gray-900"), + content + ) + + def dataCell(content: Modifier[HtmlElement]): HtmlElement = + td( + cls("whitespace-nowrap text-sm text-gray-500"), + content + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TailwindUICatalogue.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TailwindUICatalogue.scala new file mode 100644 index 0000000..5027b10 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TailwindUICatalogue.scala @@ -0,0 +1,17 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} + +trait TailwindUICatalogueModule + extends BadgeComponentsModule + with ButtonComponentsModule + with HeadingsComponentsModule + with IconsModule + with FormComponentsModule + with LayoutModule + with ListComponentsModule + with ModalComponentsModule + with PageComponentsModule + with TableComponentsModule diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/LabelsOnLeftFormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/LabelsOnLeftFormComponentsModule.scala deleted file mode 100644 index 210bbf8..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/LabelsOnLeftFormComponentsModule.scala +++ /dev/null @@ -1,155 +0,0 @@ -package works.iterative.ui.components.laminar -package tailwindui - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -trait LabelsOnLeftFormComponentsModule(using ctx: ComponentContext) - extends FormComponentsModule: - self: IconsModule => - override val forms = new FormComponents: - - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )(content: Modifier[HtmlElement]*): HtmlElement = - div( - cls("space-y-6 sm:space-y-5"), - div( - h3(cls("text-lg leading-6 font-medium text-gray-900"), title), - subtitle.map(st => p(cls("mt-1 max-w-2xl text-sm text-gray-500"), st)) - ), - div(cls("mt-6 sm:mt-5 space-y-6 sm:space-y-5"), content) - ) - - override def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement = - L.label( - cls("block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"), - forId.map(id => L.forId(id)), - labelText, - mods - ) - - override def field( - label: Modifier[HtmlElement] - )(content: Modifier[HtmlElement]*): 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, - div(cls("mt-1 sm:mt-0 sm:col-span-2"), content) - ) - - override def field( - id: String, - labelText: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement = - field( - label(labelText, Some(id))() - )( - input.amend(idAttr(id)), - help.map(h => p(cls("mt-2 text-sm text-gray-500"), h)) - ) - - override def form(mods: Modifier[HtmlElement]*)( - sections: Modifier[HtmlElement]* - )(actions: Modifier[HtmlElement]*): HtmlElement = - L.form( - cls("space-y-8 divide-y divide-gray-200"), - mods, - sections, - div( - cls("pt-5"), - div(cls("flex justify-end"), actions) - ) - ) - - override def inlineForm( - mods: Modifier[HtmlElement]* - ): HtmlElement = - L.form(cls("flex space-x-4"), mods) - - override def searchField( - id: String, - placeholderText: Option[String] = None - )( - mods: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "flex-1 min-w-0", - label(ctx.messages("forms.search.label"), Some(id))(cls("sr-only")), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - icons.`search-solid`() - ), - input( - tpe := "search", - nameAttr := "search", - idAttr := id, - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholderText - .orElse( - ctx.messages - .opt( - s"forms.search.${id}.placeholder", - s"form.search.placeholder" - ) - ) - .map(placeholder(_)), - mods - ) - ) - ) - - override def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement = - field( - id, - labelText, - input( - tpe(inputType), - cls( - "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:max-w-xs sm:text-sm sm:leading-6" - ), - placeholderText.map(placeholder(_)) - ), - helpText - ) - - override def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement = - div( - labelText, - input( - idAttr(id), - nameAttr(id), - autoComplete("date"), - tpe("date"), - cls( - "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ), - placeholderText.map(placeholder(_)), - mods(localDateSelect) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala index a2c637c..bfa063a 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala @@ -1,13 +1,11 @@ package works.iterative.ui.components.laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorDef import com.raquo.laminar.tags.HtmlTag case class Bin[Source, +Value]( label: String, description: Option[String | HtmlElement], - color: ColorDef, valueOf: Source => Value ): def map[NewValue]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala deleted file mode 100644 index 3aac0bf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala +++ /dev/null @@ -1,34 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ButtonComponentsModule: - def buttons: ButtonComponents - - trait ButtonComponents: - def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala deleted file mode 100644 index 646e73d..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala +++ /dev/null @@ -1,191 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.color.ColorDef -import com.raquo.laminar.tags.HtmlTag -import org.scalajs.dom.html.UList -import com.raquo.laminar.nodes.ReactiveHtmlElement -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind - -trait DashboardComponentsModule: - def dashboard: DashboardComponents - - // Only methods that create HtmlElements - trait DashboardComponents(using ComponentContext): - def section(label: String, children: Modifier[HtmlElement]): Div - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI - def cardInitials( - initials: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div - def count(label: String, value: Int, color: ColorDef): Span - def counts(children: HtmlElement*): Span - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - -trait DefaultDashboardComponentsModule(using ComponentContext) - extends DashboardComponentsModule: - override val dashboard: DashboardComponents = new DashboardComponents: - def section(label: String, children: Modifier[HtmlElement]): Div = - div( - cls("mt-3"), - h2( - cls("text-gray-500 text-xs font-medium uppercase tracking-wide"), - label - ), - children - ) - - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] = - ul( - cls( - "mt-3 grid gap-5 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" - ), - children - ) - - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI = - li( - div( - cls("col-span-1 flex shadow-sm rounded-md"), - initials, - div( - cls( - "flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate" - ), - div( - cls("flex-1 px-4 py-2 text-sm truncate"), - div( - cls("text-gray-900 font-medium hover:text-gray-600 truncate"), - label - ), - p(cls("text-gray-500"), children) - ) - ) - ) - ) - - def cardInitials( - id: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div = - div( - cls( - "flex-shrink-0 flex flex-col items-center justify-center w-16 text-white text-sm font-medium rounded-l-md" - ), - color.map(_(600).bg), - div(id), - children - ) - - def count(label: String, value: Int, color: ColorDef): Span = - span(color.text, title(label), s"${value}") - - def counts(children: HtmlElement*): Span = - span(interleave(children, span(" / "))) - - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div = - div( - cls("flex flex-col mt-2"), - div( - cls( - "align-middle min-w-full shadow sm:rounded-lg" - ), - L.table( - cls("min-w-full border-separate border-spacing-0"), - styleAttr("border-spacing: 0"), // Tailwind somehow doesn't work - thead( - tr( - cls("border-t border-gray-200"), - headers.map(h => - th( - cls( - "sticky top-0 z-10 px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - h - ) - ) - ) - ), - tbody(cls("bg-white divide-y divide-gray-100"), sections) - ) - ) - ) - - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - td( - cls("bg-gray-50 border-gray-200 border-t border-b"), - cls( - "sticky top-14 z-10 bg-gray-50 px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - colSpan(cs), - label - ), - mods - ) - - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - mods, - td( - cls( - "px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900 truncate" - ), - label - ), - children.map(e => - td( - cls( - "whitespace-nowrap px-2 py-2 text-sm text-gray-500" - ), - e - ) - ) - ) - - private def interleave( - a: Seq[HtmlElement], - separator: => HtmlElement - ): Seq[HtmlElement] = - if a.size < 2 then a - else a.init.flatMap(n => Seq(n, separator)) :+ a.last diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala deleted file mode 100644 index a45beaf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.FileRef - -trait DetailComponentsModule: - - def details: DetailComponents - - trait DetailComponents: - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement - - def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement - - def files(fs: List[FileRef]): HtmlElement - - def file(f: FileRef): HtmlElement - -trait DefaultDetailComponentsModule(using ctx: ComponentContext) - extends DetailComponentsModule: - self: IconsModule => - - override val details: DetailComponents = new DetailComponents: - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement = - div( - div( - cls := "px-4 sm:px-0", - h3( - cls := "text-base font-semibold leading-7 text-gray-900", - title - ), - subtitle.map(st => - p( - cls := "mt-1 max-w-2xl text-sm leading-6 text-gray-500", - st - ) - ) - // TODO: actions - ), - div( - cls := "mt-6 border-t border-gray-100", - dl( - cls := "divide-y divide-gray-100", - fields - ) - ) - ) - - override def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0", - dt( - cls := "text-sm font-medium leading-6 text-gray-900", - title - ), - dd( - cls := "mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0", - content - ) - ) - - override def files(fs: List[FileRef]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-100 rounded-md border border-gray-200", - fs.map(file) - ) - - override def file(f: FileRef): HtmlElement = - li( - cls := "flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6", - div( - cls := "flex w-0 flex-1 items-center", - icons.`paper-clip-solid`(), - div( - cls := "ml-4 flex min-w-0 flex-1 gap-2", - span( - cls := "truncate font-medium", - f.name - ), - f.size.map(size => span(cls := "flex-shrink-0 text-gray-400", size)) - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - "Uložit" - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala new file mode 100644 index 0000000..f6c583f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait FormComponents(using ctx: ComponentContext) extends LocalDateSelectModule: + def searchIcon: SvgElement + + def renderLocalDateSelect( + id: String, + labelText: Option[String], + placeholderText: Option[String], + mods: LocalDateSelect => Modifier[HtmlElement] + ): HtmlElement = + div( + labelText, + input( + idAttr(id), + nameAttr(id), + autoComplete("date"), + tpe("date"), + cls( + "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ), + placeholderText.map(placeholder(_)), + mods(localDateSelect) + ) + ) + + def searchField( + id: String, + placeholderText: Option[String] = None + )( + mods: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls := "flex-1 min-w-0", + label(ctx.messages("forms.search.label"), forId(id), cls("sr-only")), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + searchIcon + ), + input( + tpe := "search", + nameAttr := "search", + idAttr := id, + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholderText + .orElse( + ctx.messages + .opt( + s"forms.search.${id}.placeholder", + s"form.search.placeholder" + ) + ) + .map(placeholder(_)), + mods + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala deleted file mode 100644 index 7714490..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala +++ /dev/null @@ -1,82 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -/** Form components - * - * A form is a collection of sections, each of which contains a collection of - * fields. - * - * Form -> * Section -> * Field - * -> * Action - * - * Each Field has an `id`, `label` and `input` element. The `id` is used to - * link the label to the input. - * - * The `input` element can be a simple text input, or a more complex component - * such as a date picker. The caller is expected to make the input work. - */ -trait FormComponentsModule extends LocalDateSelectModule: - def forms: FormComponents - - trait FormComponents: - - /** Layout the sections and actions of a form */ - def form(mods: Modifier[HtmlElement]*)(sections: Modifier[HtmlElement]*)( - actions: Modifier[HtmlElement]* - ): HtmlElement - - /** Section layout, with a title, optional subtitle and any content. */ - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label and any content */ - def field(label: Modifier[HtmlElement])( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label, input and optional help text */ - def field( - id: String, - label: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement - - def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout an inline form element */ - def inlineForm(mods: Modifier[HtmlElement]*): HtmlElement - - /** An input field with a search icon */ - def searchField(id: String, placeholderText: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement - - /** LocalDate input */ - def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala deleted file mode 100644 index 8acbe7f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala +++ /dev/null @@ -1,28 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.Tag - -trait GenericComponentsModule: - - def generic: GenericComponents - - trait GenericComponents: - def tag(name: String, color: ColorKind): HtmlElement - def tag(t: Tag): HtmlElement = tag(t.value, t.color) - -trait DefaultGenericComponentsModule(using ComponentContext) - extends GenericComponentsModule: - override val generic: GenericComponents = new GenericComponents: - override def tag(name: String, color: ColorKind): HtmlElement = - p( - cls( - "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" - ), - color(800).text, - color(100).bg, - name - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala deleted file mode 100644 index 8cfc1d9..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala +++ /dev/null @@ -1,123 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.CustomAttrs - -trait IconsModule: - def icons: Icons - - trait Icons: - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement - def close(mods: Modifier[SvgElement]*): SvgElement - def `search-solid`(mods: Modifier[SvgElement]*): SvgElement - def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement - def `document-chart-bar-outline`(mods: Modifier[SvgElement]*): SvgElement - def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement - -trait DefaultIconsModule(using ComponentContext) extends IconsModule: - override val icons: Icons = new Icons: - import svg.* - - object aria: - val hidden = CustomAttrs.svg.ariaHidden - - private def withDefault( - mods: Seq[Modifier[SvgElement]], - default: Modifier[SvgElement] - ): Modifier[SvgElement] = - if mods.isEmpty then default else mods - - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-8 w-8"), - 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" - ) - ) - ) - - override def close(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-2 w-2"), - stroke := "currentColor", - fill := "none", - viewBox := "0 0 8 8", - path( - strokeLineCap := "round", - strokeWidth := "1.5", - d := "M1 1l6 6m0-6L1 7" - ) - ) - - override def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 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 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") - ) - ) - - override def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 text-gray-400")), - 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") - ) - ) - - override def `document-chart-bar-outline`( - mods: Modifier[SvgElement]* - ): SvgElement = - svg( - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - strokeWidth := "1.5", - stroke := "currentColor", - withDefault(mods, cls := "h-6 w-6"), - path( - strokeLineCap := "round", - strokeLineJoin := "round", - d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" - ) - ) - - override def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-5 w-5"), - cls := "flex-shrink-0 text-gray-400", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", - clipRule := "evenodd" - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala deleted file mode 100644 index 95ab654..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait LayoutModule: - def layout: LayoutComponents - - trait LayoutComponents: - def card(content: Modifier[HtmlElement]*): HtmlElement - -trait DefaultLayoutModule(using ctx: ComponentContext) extends LayoutModule: - override val layout: LayoutComponents = new LayoutComponents: - override def card(content: Modifier[HtmlElement]*): HtmlElement = - div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala deleted file mode 100644 index 5946974..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom.html.Paragraph -import com.raquo.laminar.nodes.TextNode - -trait ListComponentsModule: - - def list: ListComponents - - trait ListComponents(using ComponentContext): - def label(text: String, color: ColorKind): HtmlElement - def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI - def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] - def listSection(header: String, list: HtmlElement): Div - def navigation(sections: Modifier[HtmlElement]): HtmlElement - -trait DefaultListComponentsModule(using ComponentContext) - extends ListComponentsModule: - self: GenericComponentsModule => - - override val list: ListComponents = new ListComponents: - override def label( - text: String, - color: ColorKind - ): HtmlElement = generic.tag(text, color) - - override def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI = - li( - cls("group"), - div( - cls( - "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" - ), - avatar.map(a => - div( - cls("flex-shrink-0"), - div( - cls( - "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" - ), - a - ) - ) - ), - div( - cls("flex-1 min-w-0"), - p( - cls("text-sm font-medium text-gray-900"), - title, - span(cls("float-right"), right) - ), - subtitle.map(st => - p( - cls("text-sm text-gray-500 truncate"), - st - ) - ) - ) - ) - ) - - override def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = - ul( - cls("relative z-0 divide-y divide-gray-200"), - role("list"), - children - ) - - override def listSection( - header: String, - list: HtmlElement - ): Div = - div( - cls("relative"), - div( - cls( - "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" - ), - header - ), - list - ) - - override def navigation(sections: Modifier[HtmlElement]): HtmlElement = - navTag( - cls("flex-1 min-h-0 overflow-y-auto"), - sections - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala deleted file mode 100644 index 5b5490f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ModalComponentsModule: - - def modal: ModalComponents - - trait ModalComponents: - def modalDialog( - content: HtmlElement, - onClose: Modifier[HtmlElement] - ): HtmlElement - -trait DefaultModalComponentsModule(using ctx: ComponentContext) - extends ModalComponentsModule: - - override val modal: ModalComponents = new ModalComponents: - - override def modalDialog( - content: HtmlElement, - closeMod: Modifier[HtmlElement] - ): 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( - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")), - closeMod - ) - ) - - div( - cls("fixed inset-0 z-20 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - 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-7xl sm:align-middle" - ), - content - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala deleted file mode 100644 index b713d8a..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala +++ /dev/null @@ -1,71 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait PageComponentsModule: - - val page: PageComponents - - trait PageComponents: - def container( - children: Modifier[HtmlElement]* - ): HtmlElement - - def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement - - def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement] = emptyMod, - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement - - /** Visage for clickable text, like links - */ - def clickable: Modifier[HtmlElement] - -trait DefaultPageComponentsModule(using ComponentContext) - extends PageComponentsModule: - - override val page: PageComponents = new PageComponents: - override def container( - children: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), - children - ) - - override def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement = - div( - cls("p-8 bg-gray-100 h-full"), - header, - children - ) - - override def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement = - div( - cls("pb-5 border-b border-gray-200"), - div(cls("float-right"), right), - h1( - cls("text-2xl leading-6 font-medium text-gray-900"), - title - ), - subtitle.map( - p( - cls("text-sm font-medium text-gray-500"), - _ - ) - ) - ) - - override def clickable: Modifier[HtmlElement] = - cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala deleted file mode 100644 index f70ee87..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala +++ /dev/null @@ -1,108 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} - -trait TableComponentsModule: - def tables: TableComponents - - trait TableComponents: - def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )(table: Modifier[HtmlElement]*): HtmlElement - - def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement - - def headerRow(mods: Modifier[HtmlElement]*)( - cells: HtmlElement* - ): HtmlElement - - def dataRow(mods: Modifier[HtmlElement]*)(cells: HtmlElement*): HtmlElement - - def headerCell(content: Modifier[HtmlElement]): HtmlElement - - def dataCell(content: Modifier[HtmlElement]): HtmlElement - -trait DefaultTableComponentsModule extends TableComponentsModule: - - override lazy val tables: TableComponents = new TableComponents: - - override def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )( - table: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("px-4 sm:px-6 lg:px-8"), - div( - cls("sm:flex sm:items-center"), - div( - cls("sm:flex-auto"), - h1(cls("text-base font-semibold leading-6 text-gray-900"), title), - subtitle.map(st => p(cls("mt-2 text-sm text-gray-700"), st)) - ), - div(cls("mt-4 sm:ml-16 sm:mt-0 sm:flex-none"), actions) - ), - div( - cls("mt-8 flow-root"), - div( - cls("-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"), - div( - cls("inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"), - table - ) - ) - ) - ) - - override def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement = - table( - cls("min-w-full divide-y divide-gray-300"), - thead(header), - tbody(cls("divide-y divide-gray-200"), body) - ) - - override def headerRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-3.5 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-3.5 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-3.5 px-3")) - ) - ) - - override def dataRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - mods, - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-4 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-4 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-4 px-3")) - ) - ) - - override def headerCell(content: Modifier[HtmlElement]): HtmlElement = - th( - cls("text-left text-sm font-semibold text-gray-900"), - content - ) - - override def dataCell(content: Modifier[HtmlElement]): HtmlElement = - td( - cls("whitespace-nowrap text-sm text-gray-500"), - content - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala index b937d45..12d01ae 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala @@ -3,7 +3,7 @@ import com.raquo.laminar.api.L.{*, given} import works.iterative.ui.components.laminar.HtmlTabular import works.iterative.ui.model.tables.Tabular -import works.iterative.ui.components.laminar.TableComponentsModule +import works.iterative.ui.components.laminar.tailwind.ui.TableComponentsModule import works.iterative.core.UserMessage import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given import works.iterative.ui.components.tailwind.ComponentContext diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala new file mode 100644 index 0000000..87f3e8d --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala @@ -0,0 +1,54 @@ +package works.iterative.ui.components.laminar.tailwind.color + +import com.raquo.laminar.api.L.{*, given} + +/** Complete color definition that can be rendered to CSS. + * + * Includes the area, kind and weight of the color. + */ +case class Color(area: ColorArea, color: ColorDef): + def toCSS: String = s"${area.name}-${color.toCSS}" + +object Color: + import ColorDef.given + + given colorToCSS: Conversion[Color, HtmlMod] with + def apply(c: Color) = cls(c.toCSS) + + given colorToSVGCSS: Conversion[Color, SvgMod] with + def apply(c: Color) = svg.cls(c.toCSS) + + given colorSignalToCSS: Conversion[Signal[Color], HtmlMod] with + def apply(c: Signal[Color]) = cls <-- c.map(_.toCSS) + + given colorSignalToSVGCSS: Conversion[Signal[Color], SvgMod] with + def apply(c: Signal[Color]) = svg.cls <-- c.map(_.toCSS) + + def current = ColorDef(ColorKind.current) + def inherit = ColorDef(ColorKind.inherit) + def transp = ColorDef(ColorKind.transp) + def auto = ColorDef(ColorKind.auto) + def black = ColorDef(ColorKind.black) + def white = ColorDef(ColorKind.white) + def slate(weight: ColorWeight) = ColorDef(ColorKind.slate, weight) + def gray(weight: ColorWeight) = ColorDef(ColorKind.gray, weight) + def zinc(weight: ColorWeight) = ColorDef(ColorKind.zinc, weight) + def neutral(weight: ColorWeight) = ColorDef(ColorKind.neutral, weight) + def stone(weight: ColorWeight) = ColorDef(ColorKind.stone, weight) + def red(weight: ColorWeight) = ColorDef(ColorKind.red, weight) + def orange(weight: ColorWeight) = ColorDef(ColorKind.orange, weight) + def amber(weight: ColorWeight) = ColorDef(ColorKind.amber, weight) + def yellow(weight: ColorWeight) = ColorDef(ColorKind.yellow, weight) + def lime(weight: ColorWeight) = ColorDef(ColorKind.lime, weight) + def green(weight: ColorWeight) = ColorDef(ColorKind.green, weight) + def emerald(weight: ColorWeight) = ColorDef(ColorKind.emerald, weight) + def teal(weight: ColorWeight) = ColorDef(ColorKind.teal, weight) + def cyan(weight: ColorWeight) = ColorDef(ColorKind.cyan, weight) + def sky(weight: ColorWeight) = ColorDef(ColorKind.sky, weight) + def blue(weight: ColorWeight) = ColorDef(ColorKind.blue, weight) + def indigo(weight: ColorWeight) = ColorDef(ColorKind.indigo, weight) + def violet(weight: ColorWeight) = ColorDef(ColorKind.violet, weight) + def purple(weight: ColorWeight) = ColorDef(ColorKind.purple, weight) + def fuchsia(weight: ColorWeight) = ColorDef(ColorKind.fuchsia, weight) + def pink(weight: ColorWeight) = ColorDef(ColorKind.pink, weight) + def rose(weight: ColorWeight) = ColorDef(ColorKind.rose, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala new file mode 100644 index 0000000..4130840 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala @@ -0,0 +1,16 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines the area the color should apply to, eg. background, text, border, + * etc. + */ +enum ColorArea(val name: String): + case bg extends ColorArea("bg") + case text extends ColorArea("text") + case decoration extends ColorArea("decoration") + case border extends ColorArea("border") + case outline extends ColorArea("outline") + case divide extends ColorArea("divide") + case ring extends ColorArea("ring") + case ringOffset extends ColorArea("ring-offset") + case shadow extends ColorArea("shadow") + case accent extends ColorArea("accent") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala new file mode 100644 index 0000000..fab9404 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala @@ -0,0 +1,43 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** A combination of ColorKind and ColorWeight, if applicable. + * + * By applying area we get the full Color definition. + */ +sealed trait ColorDef: + def toCSS: String + + def bg = Color(ColorArea.bg, this) + def text = Color(ColorArea.text, this) + def decoration = Color(ColorArea.decoration, this) + def border = Color(ColorArea.border, this) + def outline = Color(ColorArea.outline, this) + def divide = Color(ColorArea.divide, this) + def ring = Color(ColorArea.ring, this) + def ringOffset = Color(ColorArea.ringOffset, this) + def shadow = Color(ColorArea.shadow, this) + def accent = Color(ColorArea.accent, this) + +// TODO: create a macro that will output the tailwind class as string directly during compilation, where possible. +object ColorDef: + case class WeightedColorDef( + kind: ColorKind, + weight: ColorWeight + ) extends ColorDef: + override def toCSS: String = s"${kind.name}-${weight.value}" + + case class UnweightedColorDef( + kind: ColorKind + ) extends ColorDef: + override def toCSS: String = kind.name + + // TODO: check that the kind is valid unweighted kind + // that means current, inherit, auto, transparent, black, white + // tried using implicit evidence, but the type inference for enumerations + // tends to generalize to the enum, instead of the real type + def apply[T <: ColorKind](kind: T)(using + ev: T <:< ColorKind.Unweighted + ): ColorDef = + UnweightedColorDef(kind) + def apply(kind: ColorKind, weight: ColorWeight): ColorDef = + WeightedColorDef(kind, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala new file mode 100644 index 0000000..865166b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala @@ -0,0 +1,47 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines what color should be used, without specifying the area or weight. + */ +sealed abstract class ColorKind private (val name: String): + def apply(weight: ColorWeight): ColorDef = + ColorDef.WeightedColorDef(this, weight) + +object ColorKind: + trait Unweighted: + self: ColorKind => + override def apply(weight: ColorWeight): ColorDef = + ColorDef.UnweightedColorDef(self) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case object current extends ColorKind("current") with Unweighted + case object inherit extends ColorKind("inherit") with Unweighted + // Not present in for all methods + case object transp extends ColorKind("transparent") with Unweighted + // Seen in accent, not preset otherwise + case object auto extends ColorKind("auto") with Unweighted + // Black and white do not have weight + case object black extends ColorKind("black") with Unweighted + case object white extends ColorKind("white") with Unweighted + case object slate extends ColorKind("slate") + case object gray extends ColorKind("gray") + case object zinc extends ColorKind("zinc") + case object neutral extends ColorKind("neutral") + case object stone extends ColorKind("stone") + case object red extends ColorKind("red") + case object orange extends ColorKind("orange") + case object amber extends ColorKind("amber") + case object yellow extends ColorKind("yellow") + case object lime extends ColorKind("lime") + case object green extends ColorKind("green") + case object emerald extends ColorKind("emerald") + case object teal extends ColorKind("teal") + case object cyan extends ColorKind("cyan") + case object sky extends ColorKind("sky") + case object blue extends ColorKind("blue") + case object indigo extends ColorKind("indigo") + case object violet extends ColorKind("violet") + case object purple extends ColorKind("purple") + case object fuchsia extends ColorKind("fuchsia") + case object pink extends ColorKind("pink") + case object rose extends ColorKind("rose") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala new file mode 100644 index 0000000..6f1a4cc --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala @@ -0,0 +1,33 @@ +package works.iterative.ui.components.laminar.tailwind.color + +opaque type ColorWeight = String + +extension (c: ColorWeight) def value: String = c + +/** Defines weight of a color, eg. 50, 100, 200, etc. + * + * Tailwind-like. + */ +object ColorWeight: + inline given int50: Conversion[50, ColorWeight] with + inline def apply(i: 50) = "50" + inline given int100: Conversion[100, ColorWeight] with + inline def apply(i: 100) = "100" + inline given int200: Conversion[200, ColorWeight] with + inline def apply(i: 200) = "200" + inline given int300: Conversion[300, ColorWeight] with + inline def apply(i: 300) = "300" + inline given int400: Conversion[400, ColorWeight] with + inline def apply(i: 400) = "400" + inline given int500: Conversion[500, ColorWeight] with + inline def apply(i: 500) = "500" + inline given int600: Conversion[600, ColorWeight] with + inline def apply(i: 600) = "600" + inline given int700: Conversion[700, ColorWeight] with + inline def apply(i: 700) = "700" + inline given int800: Conversion[800, ColorWeight] with + inline def apply(i: 800) = "800" + inline given int900: Conversion[900, ColorWeight] with + inline def apply(i: 900) = "900" + inline given int950: Conversion[950, ColorWeight] with + inline def apply(i: 950) = "950" diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala new file mode 100644 index 0000000..21e21f0 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala @@ -0,0 +1,12 @@ +package works.iterative.ui.components.laminar.tailwind + +/** We need a generic Color model that can be used both on server and on client. + * + * We have adopted the Tailwind model for now. There is nothing inherently + * Tailwind-specific in the implementation, but all the values are taken from + * their palette and the area model is very HTML biased. + * + * Still, I think that it is a good starting point for exploration and will + * satisfy our current needs. + */ +package object color {} diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" new file mode 100644 index 0000000..5c4e218 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" @@ -0,0 +1,21 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.model.color.ColorKind +import works.iterative.ui.components.tailwind.ComponentContext +import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given +import works.iterative.ui.model.Tag + +trait TailwindUIBadgesComponentsModule(using ComponentContext): + + object badges: + override def tag(name: String, color: ColorKind): HtmlElement = + p( + cls( + "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" + ), + color(800).text, + color(100).bg, + name + ) diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" new file mode 100644 index 0000000..0c9ca25 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" @@ -0,0 +1,24 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} + +trait TailwindUIHeadingsComponentsModule: + val headings: HeadingsComponents = new HeadingsComponents + + class HeadingsComponents: + def section(title: Node, actions: HtmlElement*): HtmlElement = + div( + cls("border-b border-gray-200 pb-5"), + div( + cls("sm:flex sm:items-center sm:justify-between"), + h3(cls("text-base font-semibold leading-6 text-gray-900"), title), + div( + cls("mt-3 flex sm:ml-4 sm:mt-0"), + actions match + case Nil => emptyMod + case first :: rest => + first :: rest.map(_.amend(cls("ml-3"))) + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala new file mode 100644 index 0000000..16ca01b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala @@ -0,0 +1,19 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.laminar.tailwind.color.ColorKind + +trait BadgeComponentsModule: + + object badges: + def pill(name: String, color: ColorKind): HtmlElement = + p( + cls( + "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" + ), + color(800).text, + color(100).bg, + name + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala new file mode 100644 index 0000000..ebb2968 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} + +trait ButtonComponentsModule: + + object buttons: + + private inline def srHelp(text: String): Modifier[HtmlElement] = + span(cls := "sr-only", text) + + def primaryButton( + id: String, + text: Modifier[HtmlElement], + icon: Option[SvgElement] = None, + buttonType: String = "submit" + )(mods: Modifier[HtmlElement]*): HtmlElement = + button( + tpe(buttonType), + cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + icon, + text, + mods + ) + + def secondaryButton( + id: String, + text: Modifier[HtmlElement], + icon: Option[SvgElement] = None, + buttonType: String = "button" + )(mods: Modifier[HtmlElement]*): HtmlElement = + button( + tpe(buttonType), + cls( + "ml-2 bg-white inline-flex justify-center 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" + ), + icon, + text, + mods + ) + + def inlineButton(id: String, icon: SvgElement)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + button( + tpe := "button", + cls := "ml-1 inline-flex h-4 w-4 flex-shrink-0 rounded-full p-1 text-gray-400 hover:bg-gray-200 hover:text-gray-500", + srHelp(id), + icon + ) + + def iconButton(id: String, icon: SvgElement, srText: Option[String] = None)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + button( + tpe := "button", + cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", + icon, + srText.map(srHelp(_)) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala new file mode 100644 index 0000000..eb6dd5f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala @@ -0,0 +1,101 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import org.scalajs.dom.html +import com.raquo.laminar.modifiers.KeyUpdater + +trait FormComponentsModule: + self: IconsModule => + + object forms: + + def section( + title: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] + )(content: Modifier[HtmlElement]*): HtmlElement = + div( + cls("space-y-6 sm:space-y-5"), + div( + h3(cls("text-lg leading-6 font-medium text-gray-900"), title), + subtitle.map(st => p(cls("mt-1 max-w-2xl text-sm text-gray-500"), st)) + ), + div(cls("mt-6 sm:mt-5 space-y-6 sm:space-y-5"), content) + ) + + def label(labelText: String, forId: Option[String] = None)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + L.label( + cls("block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"), + forId.map(id => L.forId(id)), + labelText, + mods + ) + + def field( + label: Modifier[HtmlElement] + )(content: Modifier[HtmlElement]*): 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, + div(cls("mt-1 sm:mt-0 sm:col-span-2"), content) + ) + + def field( + id: String, + labelText: String, + input: HtmlElement, + help: Option[String] + ): HtmlElement = + field( + label(labelText, Some(id))() + )( + input.amend(idAttr(id)), + help.map(h => p(cls("mt-2 text-sm text-gray-500"), h)) + ) + + def form(mods: Modifier[HtmlElement]*)( + sections: Modifier[HtmlElement]* + )(actions: Modifier[HtmlElement]*): HtmlElement = + L.form( + cls("space-y-8 divide-y divide-gray-200"), + mods, + sections, + div( + cls("pt-5"), + div(cls("flex justify-end"), actions) + ) + ) + + def inlineForm( + mods: Modifier[HtmlElement]* + ): HtmlElement = + L.form(cls("flex space-x-4"), mods) + + def inputField( + id: String, + labelText: String, + placeholderText: Option[String] = None, + inputType: String = "text", + helpText: Option[String] = None + ): HtmlElement = + field( + id, + labelText, + input( + tpe(inputType), + cls( + "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:max-w-xs sm:text-sm sm:leading-6" + ), + placeholderText.map(placeholder(_)) + ), + helpText + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala new file mode 100644 index 0000000..52da07e --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala @@ -0,0 +1,22 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} + +trait HeadingsComponentsModule: + object headings: + def section(title: Node, actions: HtmlElement*): HtmlElement = + div( + cls("border-b border-gray-200 pb-5"), + div( + cls("sm:flex sm:items-center sm:justify-between"), + h3(cls("text-base font-semibold leading-6 text-gray-900"), title), + div( + cls("mt-3 flex sm:ml-4 sm:mt-0"), + actions match + case Nil => emptyMod + case first :: rest => + first :: rest.map(_.amend(cls("ml-3"))) + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala new file mode 100644 index 0000000..c543150 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala @@ -0,0 +1,112 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext +import works.iterative.ui.components.tailwind.CustomAttrs + +trait IconsModule: + object icons: + import svg.* + + object aria: + val hidden = CustomAttrs.svg.ariaHidden + + private def withDefault( + mods: Seq[Modifier[SvgElement]], + default: Modifier[SvgElement] + ): Modifier[SvgElement] = + if mods.isEmpty then default else mods + + def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-8 w-8"), + 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" + ) + ) + ) + + def close(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-2 w-2"), + stroke := "currentColor", + fill := "none", + viewBox := "0 0 8 8", + path( + strokeLineCap := "round", + strokeWidth := "1.5", + d := "M1 1l6 6m0-6L1 7" + ) + ) + + def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls("h-5 w-5 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 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") + ) + ) + + def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls("h-5 w-5 text-gray-400")), + 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") + ) + ) + + def `document-chart-bar-outline`( + mods: Modifier[SvgElement]* + ): SvgElement = + svg( + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + strokeWidth := "1.5", + stroke := "currentColor", + withDefault(mods, cls := "h-6 w-6"), + path( + strokeLineCap := "round", + strokeLineJoin := "round", + d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" + ) + ) + + def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-5 w-5"), + cls := "flex-shrink-0 text-gray-400", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", + clipRule := "evenodd" + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala new file mode 100644 index 0000000..02cbdb6 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala @@ -0,0 +1,11 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait LayoutModule: + object layout: + def card(content: Modifier[HtmlElement]*): HtmlElement = + div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala new file mode 100644 index 0000000..b652da9 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala @@ -0,0 +1,88 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.laminar.nodes.ReactiveHtmlElement +import org.scalajs.dom.html.Paragraph +import com.raquo.laminar.nodes.TextNode +import works.iterative.ui.components.laminar.tailwind.color.ColorKind + +trait ListComponentsModule: + self: BadgeComponentsModule => + + object list: + def label( + text: String, + color: ColorKind + ): HtmlElement = badges.pill(text, color) + + def item( + title: String, + subtitle: Option[String], + right: Modifier[HtmlElement] = emptyMod, + avatar: Option[Modifier[HtmlElement]] = None + ): LI = + li( + cls("group"), + div( + cls( + "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" + ), + avatar.map(a => + div( + cls("flex-shrink-0"), + div( + cls( + "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" + ), + a + ) + ) + ), + div( + cls("flex-1 min-w-0"), + p( + cls("text-sm font-medium text-gray-900"), + title, + span(cls("float-right"), right) + ), + subtitle.map(st => + p( + cls("text-sm text-gray-500 truncate"), + st + ) + ) + ) + ) + ) + + def unordered( + children: Modifier[HtmlElement] + ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = + ul( + cls("relative z-0 divide-y divide-gray-200"), + role("list"), + children + ) + + def listSection( + header: String, + list: HtmlElement + ): Div = + div( + cls("relative"), + div( + cls( + "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" + ), + header + ), + list + ) + + def navigation(sections: Modifier[HtmlElement]): HtmlElement = + navTag( + cls("flex-1 min-h-0 overflow-y-auto"), + sections + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ModalComponentsModule.scala new file mode 100644 index 0000000..6027404 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ModalComponentsModule.scala @@ -0,0 +1,54 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait ModalComponentsModule: + object modal: + def modalDialog( + content: HtmlElement, + closeMod: Modifier[HtmlElement] + ): 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( + cls("fixed inset-0 transition-opacity"), + div(cls("absolute inset-0 bg-gray-500 opacity-75")), + closeMod + ) + ) + + div( + cls("fixed inset-0 z-20 overflow-y-auto"), + div( + cls("text-center sm:block sm:p-0"), + overlay, + browserCenteringModalTrick, + 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-7xl sm:align-middle" + ), + content + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/PageComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/PageComponentsModule.scala new file mode 100644 index 0000000..d5a422e --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/PageComponentsModule.scala @@ -0,0 +1,49 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait PageComponentsModule: + + object page: + def container( + children: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), + children + ) + + def singleColumn( + header: Modifier[HtmlElement] + )(children: Modifier[HtmlElement]*): HtmlElement = + div( + cls("p-8 bg-gray-100 h-full"), + header, + children + ) + + def pageHeader( + title: Modifier[HtmlElement], + right: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] = None + ): HtmlElement = + div( + cls("pb-5 border-b border-gray-200"), + div(cls("float-right"), right), + h1( + cls("text-2xl leading-6 font-medium text-gray-900"), + title + ), + subtitle.map( + p( + cls("text-sm font-medium text-gray-500"), + _ + ) + ) + ) + + def clickable: Modifier[HtmlElement] = + cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TableComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TableComponentsModule.scala new file mode 100644 index 0000000..417ea56 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TableComponentsModule.scala @@ -0,0 +1,86 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L +import com.raquo.laminar.api.L.{*, given} + +trait TableComponentsModule: + + object tables: + + def tableSection( + title: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] = None, + actions: Modifier[HtmlElement]* + )( + table: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls("px-4 sm:px-6 lg:px-8"), + div( + cls("sm:flex sm:items-center"), + div( + cls("sm:flex-auto"), + h1(cls("text-base font-semibold leading-6 text-gray-900"), title), + subtitle.map(st => p(cls("mt-2 text-sm text-gray-700"), st)) + ), + div(cls("mt-4 sm:ml-16 sm:mt-0 sm:flex-none"), actions) + ), + div( + cls("mt-8 flow-root"), + div( + cls("-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"), + div( + cls("inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"), + table + ) + ) + ) + ) + + def simpleTable(header: Modifier[HtmlElement]*)( + body: Modifier[HtmlElement]* + ): HtmlElement = + table( + cls("min-w-full divide-y divide-gray-300"), + thead(header), + tbody(cls("divide-y divide-gray-200"), body) + ) + + def headerRow( + mods: Modifier[HtmlElement]* + )(cells: HtmlElement*): HtmlElement = + tr( + cells.zipWithIndex.map((c, i) => + if i == 0 then c.amend(cls("py-3.5 pl-4 pr-3 sm:pl-0")) + else if i == cells.length - 1 then + c.amend(cls("py-3.5 pr-4 pl-3 sm:pr-0")) + else c.amend(cls("py-3.5 px-3")) + ) + ) + + def dataRow( + mods: Modifier[HtmlElement]* + )(cells: HtmlElement*): HtmlElement = + tr( + mods, + cells.zipWithIndex.map((c, i) => + if i == 0 then c.amend(cls("py-4 pl-4 pr-3 sm:pl-0")) + else if i == cells.length - 1 then + c.amend(cls("py-4 pr-4 pl-3 sm:pr-0")) + else c.amend(cls("py-4 px-3")) + ) + ) + + def headerCell(content: Modifier[HtmlElement]): HtmlElement = + th( + cls("text-left text-sm font-semibold text-gray-900"), + content + ) + + def dataCell(content: Modifier[HtmlElement]): HtmlElement = + td( + cls("whitespace-nowrap text-sm text-gray-500"), + content + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TailwindUICatalogue.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TailwindUICatalogue.scala new file mode 100644 index 0000000..5027b10 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TailwindUICatalogue.scala @@ -0,0 +1,17 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} + +trait TailwindUICatalogueModule + extends BadgeComponentsModule + with ButtonComponentsModule + with HeadingsComponentsModule + with IconsModule + with FormComponentsModule + with LayoutModule + with ListComponentsModule + with ModalComponentsModule + with PageComponentsModule + with TableComponentsModule diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/LabelsOnLeftFormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/LabelsOnLeftFormComponentsModule.scala deleted file mode 100644 index 210bbf8..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/LabelsOnLeftFormComponentsModule.scala +++ /dev/null @@ -1,155 +0,0 @@ -package works.iterative.ui.components.laminar -package tailwindui - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -trait LabelsOnLeftFormComponentsModule(using ctx: ComponentContext) - extends FormComponentsModule: - self: IconsModule => - override val forms = new FormComponents: - - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )(content: Modifier[HtmlElement]*): HtmlElement = - div( - cls("space-y-6 sm:space-y-5"), - div( - h3(cls("text-lg leading-6 font-medium text-gray-900"), title), - subtitle.map(st => p(cls("mt-1 max-w-2xl text-sm text-gray-500"), st)) - ), - div(cls("mt-6 sm:mt-5 space-y-6 sm:space-y-5"), content) - ) - - override def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement = - L.label( - cls("block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"), - forId.map(id => L.forId(id)), - labelText, - mods - ) - - override def field( - label: Modifier[HtmlElement] - )(content: Modifier[HtmlElement]*): 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, - div(cls("mt-1 sm:mt-0 sm:col-span-2"), content) - ) - - override def field( - id: String, - labelText: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement = - field( - label(labelText, Some(id))() - )( - input.amend(idAttr(id)), - help.map(h => p(cls("mt-2 text-sm text-gray-500"), h)) - ) - - override def form(mods: Modifier[HtmlElement]*)( - sections: Modifier[HtmlElement]* - )(actions: Modifier[HtmlElement]*): HtmlElement = - L.form( - cls("space-y-8 divide-y divide-gray-200"), - mods, - sections, - div( - cls("pt-5"), - div(cls("flex justify-end"), actions) - ) - ) - - override def inlineForm( - mods: Modifier[HtmlElement]* - ): HtmlElement = - L.form(cls("flex space-x-4"), mods) - - override def searchField( - id: String, - placeholderText: Option[String] = None - )( - mods: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "flex-1 min-w-0", - label(ctx.messages("forms.search.label"), Some(id))(cls("sr-only")), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - icons.`search-solid`() - ), - input( - tpe := "search", - nameAttr := "search", - idAttr := id, - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholderText - .orElse( - ctx.messages - .opt( - s"forms.search.${id}.placeholder", - s"form.search.placeholder" - ) - ) - .map(placeholder(_)), - mods - ) - ) - ) - - override def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement = - field( - id, - labelText, - input( - tpe(inputType), - cls( - "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:max-w-xs sm:text-sm sm:leading-6" - ), - placeholderText.map(placeholder(_)) - ), - helpText - ) - - override def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement = - div( - labelText, - input( - idAttr(id), - nameAttr(id), - autoComplete("date"), - tpe("date"), - cls( - "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ), - placeholderText.map(placeholder(_)), - mods(localDateSelect) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/TailwindUIButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/TailwindUIButtonComponentsModule.scala deleted file mode 100644 index 4c44157..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/TailwindUIButtonComponentsModule.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar -package tailwindui - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait TailwindUIButtonComponentsModule(using ctx: ComponentContext) - extends ButtonComponentsModule: - - override val buttons = new ButtonComponents: - - private inline def srHelp(id: String): Modifier[HtmlElement] = - ctx.messages - .opt(s"form.button.${id}.screenReaderHelp") - .map(sr => span(cls := "sr-only", sr)) - - override def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )(mods: Modifier[HtmlElement]*): HtmlElement = - button( - tpe(buttonType), - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - icon, - text, - mods - ) - - override def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )(mods: Modifier[HtmlElement]*): HtmlElement = - button( - tpe(buttonType), - cls( - "ml-2 bg-white inline-flex justify-center 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" - ), - icon, - text, - mods - ) - - override def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement = - button( - tpe := "button", - cls := "ml-1 inline-flex h-4 w-4 flex-shrink-0 rounded-full p-1 text-gray-400 hover:bg-gray-200 hover:text-gray-500", - srHelp(id), - icon - ) - - override def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement = - button( - tpe := "button", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - icon, - srHelp(id) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala index a2c637c..bfa063a 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala @@ -1,13 +1,11 @@ package works.iterative.ui.components.laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorDef import com.raquo.laminar.tags.HtmlTag case class Bin[Source, +Value]( label: String, description: Option[String | HtmlElement], - color: ColorDef, valueOf: Source => Value ): def map[NewValue]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala deleted file mode 100644 index 3aac0bf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala +++ /dev/null @@ -1,34 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ButtonComponentsModule: - def buttons: ButtonComponents - - trait ButtonComponents: - def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala deleted file mode 100644 index 646e73d..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala +++ /dev/null @@ -1,191 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.color.ColorDef -import com.raquo.laminar.tags.HtmlTag -import org.scalajs.dom.html.UList -import com.raquo.laminar.nodes.ReactiveHtmlElement -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind - -trait DashboardComponentsModule: - def dashboard: DashboardComponents - - // Only methods that create HtmlElements - trait DashboardComponents(using ComponentContext): - def section(label: String, children: Modifier[HtmlElement]): Div - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI - def cardInitials( - initials: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div - def count(label: String, value: Int, color: ColorDef): Span - def counts(children: HtmlElement*): Span - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - -trait DefaultDashboardComponentsModule(using ComponentContext) - extends DashboardComponentsModule: - override val dashboard: DashboardComponents = new DashboardComponents: - def section(label: String, children: Modifier[HtmlElement]): Div = - div( - cls("mt-3"), - h2( - cls("text-gray-500 text-xs font-medium uppercase tracking-wide"), - label - ), - children - ) - - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] = - ul( - cls( - "mt-3 grid gap-5 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" - ), - children - ) - - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI = - li( - div( - cls("col-span-1 flex shadow-sm rounded-md"), - initials, - div( - cls( - "flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate" - ), - div( - cls("flex-1 px-4 py-2 text-sm truncate"), - div( - cls("text-gray-900 font-medium hover:text-gray-600 truncate"), - label - ), - p(cls("text-gray-500"), children) - ) - ) - ) - ) - - def cardInitials( - id: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div = - div( - cls( - "flex-shrink-0 flex flex-col items-center justify-center w-16 text-white text-sm font-medium rounded-l-md" - ), - color.map(_(600).bg), - div(id), - children - ) - - def count(label: String, value: Int, color: ColorDef): Span = - span(color.text, title(label), s"${value}") - - def counts(children: HtmlElement*): Span = - span(interleave(children, span(" / "))) - - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div = - div( - cls("flex flex-col mt-2"), - div( - cls( - "align-middle min-w-full shadow sm:rounded-lg" - ), - L.table( - cls("min-w-full border-separate border-spacing-0"), - styleAttr("border-spacing: 0"), // Tailwind somehow doesn't work - thead( - tr( - cls("border-t border-gray-200"), - headers.map(h => - th( - cls( - "sticky top-0 z-10 px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - h - ) - ) - ) - ), - tbody(cls("bg-white divide-y divide-gray-100"), sections) - ) - ) - ) - - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - td( - cls("bg-gray-50 border-gray-200 border-t border-b"), - cls( - "sticky top-14 z-10 bg-gray-50 px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - colSpan(cs), - label - ), - mods - ) - - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - mods, - td( - cls( - "px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900 truncate" - ), - label - ), - children.map(e => - td( - cls( - "whitespace-nowrap px-2 py-2 text-sm text-gray-500" - ), - e - ) - ) - ) - - private def interleave( - a: Seq[HtmlElement], - separator: => HtmlElement - ): Seq[HtmlElement] = - if a.size < 2 then a - else a.init.flatMap(n => Seq(n, separator)) :+ a.last diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala deleted file mode 100644 index a45beaf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.FileRef - -trait DetailComponentsModule: - - def details: DetailComponents - - trait DetailComponents: - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement - - def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement - - def files(fs: List[FileRef]): HtmlElement - - def file(f: FileRef): HtmlElement - -trait DefaultDetailComponentsModule(using ctx: ComponentContext) - extends DetailComponentsModule: - self: IconsModule => - - override val details: DetailComponents = new DetailComponents: - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement = - div( - div( - cls := "px-4 sm:px-0", - h3( - cls := "text-base font-semibold leading-7 text-gray-900", - title - ), - subtitle.map(st => - p( - cls := "mt-1 max-w-2xl text-sm leading-6 text-gray-500", - st - ) - ) - // TODO: actions - ), - div( - cls := "mt-6 border-t border-gray-100", - dl( - cls := "divide-y divide-gray-100", - fields - ) - ) - ) - - override def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0", - dt( - cls := "text-sm font-medium leading-6 text-gray-900", - title - ), - dd( - cls := "mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0", - content - ) - ) - - override def files(fs: List[FileRef]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-100 rounded-md border border-gray-200", - fs.map(file) - ) - - override def file(f: FileRef): HtmlElement = - li( - cls := "flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6", - div( - cls := "flex w-0 flex-1 items-center", - icons.`paper-clip-solid`(), - div( - cls := "ml-4 flex min-w-0 flex-1 gap-2", - span( - cls := "truncate font-medium", - f.name - ), - f.size.map(size => span(cls := "flex-shrink-0 text-gray-400", size)) - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - "Uložit" - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala new file mode 100644 index 0000000..f6c583f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait FormComponents(using ctx: ComponentContext) extends LocalDateSelectModule: + def searchIcon: SvgElement + + def renderLocalDateSelect( + id: String, + labelText: Option[String], + placeholderText: Option[String], + mods: LocalDateSelect => Modifier[HtmlElement] + ): HtmlElement = + div( + labelText, + input( + idAttr(id), + nameAttr(id), + autoComplete("date"), + tpe("date"), + cls( + "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ), + placeholderText.map(placeholder(_)), + mods(localDateSelect) + ) + ) + + def searchField( + id: String, + placeholderText: Option[String] = None + )( + mods: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls := "flex-1 min-w-0", + label(ctx.messages("forms.search.label"), forId(id), cls("sr-only")), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + searchIcon + ), + input( + tpe := "search", + nameAttr := "search", + idAttr := id, + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholderText + .orElse( + ctx.messages + .opt( + s"forms.search.${id}.placeholder", + s"form.search.placeholder" + ) + ) + .map(placeholder(_)), + mods + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala deleted file mode 100644 index 7714490..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala +++ /dev/null @@ -1,82 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -/** Form components - * - * A form is a collection of sections, each of which contains a collection of - * fields. - * - * Form -> * Section -> * Field - * -> * Action - * - * Each Field has an `id`, `label` and `input` element. The `id` is used to - * link the label to the input. - * - * The `input` element can be a simple text input, or a more complex component - * such as a date picker. The caller is expected to make the input work. - */ -trait FormComponentsModule extends LocalDateSelectModule: - def forms: FormComponents - - trait FormComponents: - - /** Layout the sections and actions of a form */ - def form(mods: Modifier[HtmlElement]*)(sections: Modifier[HtmlElement]*)( - actions: Modifier[HtmlElement]* - ): HtmlElement - - /** Section layout, with a title, optional subtitle and any content. */ - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label and any content */ - def field(label: Modifier[HtmlElement])( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label, input and optional help text */ - def field( - id: String, - label: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement - - def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout an inline form element */ - def inlineForm(mods: Modifier[HtmlElement]*): HtmlElement - - /** An input field with a search icon */ - def searchField(id: String, placeholderText: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement - - /** LocalDate input */ - def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala deleted file mode 100644 index 8acbe7f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala +++ /dev/null @@ -1,28 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.Tag - -trait GenericComponentsModule: - - def generic: GenericComponents - - trait GenericComponents: - def tag(name: String, color: ColorKind): HtmlElement - def tag(t: Tag): HtmlElement = tag(t.value, t.color) - -trait DefaultGenericComponentsModule(using ComponentContext) - extends GenericComponentsModule: - override val generic: GenericComponents = new GenericComponents: - override def tag(name: String, color: ColorKind): HtmlElement = - p( - cls( - "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" - ), - color(800).text, - color(100).bg, - name - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala deleted file mode 100644 index 8cfc1d9..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala +++ /dev/null @@ -1,123 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.CustomAttrs - -trait IconsModule: - def icons: Icons - - trait Icons: - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement - def close(mods: Modifier[SvgElement]*): SvgElement - def `search-solid`(mods: Modifier[SvgElement]*): SvgElement - def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement - def `document-chart-bar-outline`(mods: Modifier[SvgElement]*): SvgElement - def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement - -trait DefaultIconsModule(using ComponentContext) extends IconsModule: - override val icons: Icons = new Icons: - import svg.* - - object aria: - val hidden = CustomAttrs.svg.ariaHidden - - private def withDefault( - mods: Seq[Modifier[SvgElement]], - default: Modifier[SvgElement] - ): Modifier[SvgElement] = - if mods.isEmpty then default else mods - - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-8 w-8"), - 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" - ) - ) - ) - - override def close(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-2 w-2"), - stroke := "currentColor", - fill := "none", - viewBox := "0 0 8 8", - path( - strokeLineCap := "round", - strokeWidth := "1.5", - d := "M1 1l6 6m0-6L1 7" - ) - ) - - override def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 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 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") - ) - ) - - override def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 text-gray-400")), - 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") - ) - ) - - override def `document-chart-bar-outline`( - mods: Modifier[SvgElement]* - ): SvgElement = - svg( - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - strokeWidth := "1.5", - stroke := "currentColor", - withDefault(mods, cls := "h-6 w-6"), - path( - strokeLineCap := "round", - strokeLineJoin := "round", - d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" - ) - ) - - override def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-5 w-5"), - cls := "flex-shrink-0 text-gray-400", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", - clipRule := "evenodd" - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala deleted file mode 100644 index 95ab654..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait LayoutModule: - def layout: LayoutComponents - - trait LayoutComponents: - def card(content: Modifier[HtmlElement]*): HtmlElement - -trait DefaultLayoutModule(using ctx: ComponentContext) extends LayoutModule: - override val layout: LayoutComponents = new LayoutComponents: - override def card(content: Modifier[HtmlElement]*): HtmlElement = - div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala deleted file mode 100644 index 5946974..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom.html.Paragraph -import com.raquo.laminar.nodes.TextNode - -trait ListComponentsModule: - - def list: ListComponents - - trait ListComponents(using ComponentContext): - def label(text: String, color: ColorKind): HtmlElement - def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI - def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] - def listSection(header: String, list: HtmlElement): Div - def navigation(sections: Modifier[HtmlElement]): HtmlElement - -trait DefaultListComponentsModule(using ComponentContext) - extends ListComponentsModule: - self: GenericComponentsModule => - - override val list: ListComponents = new ListComponents: - override def label( - text: String, - color: ColorKind - ): HtmlElement = generic.tag(text, color) - - override def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI = - li( - cls("group"), - div( - cls( - "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" - ), - avatar.map(a => - div( - cls("flex-shrink-0"), - div( - cls( - "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" - ), - a - ) - ) - ), - div( - cls("flex-1 min-w-0"), - p( - cls("text-sm font-medium text-gray-900"), - title, - span(cls("float-right"), right) - ), - subtitle.map(st => - p( - cls("text-sm text-gray-500 truncate"), - st - ) - ) - ) - ) - ) - - override def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = - ul( - cls("relative z-0 divide-y divide-gray-200"), - role("list"), - children - ) - - override def listSection( - header: String, - list: HtmlElement - ): Div = - div( - cls("relative"), - div( - cls( - "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" - ), - header - ), - list - ) - - override def navigation(sections: Modifier[HtmlElement]): HtmlElement = - navTag( - cls("flex-1 min-h-0 overflow-y-auto"), - sections - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala deleted file mode 100644 index 5b5490f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ModalComponentsModule: - - def modal: ModalComponents - - trait ModalComponents: - def modalDialog( - content: HtmlElement, - onClose: Modifier[HtmlElement] - ): HtmlElement - -trait DefaultModalComponentsModule(using ctx: ComponentContext) - extends ModalComponentsModule: - - override val modal: ModalComponents = new ModalComponents: - - override def modalDialog( - content: HtmlElement, - closeMod: Modifier[HtmlElement] - ): 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( - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")), - closeMod - ) - ) - - div( - cls("fixed inset-0 z-20 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - 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-7xl sm:align-middle" - ), - content - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala deleted file mode 100644 index b713d8a..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala +++ /dev/null @@ -1,71 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait PageComponentsModule: - - val page: PageComponents - - trait PageComponents: - def container( - children: Modifier[HtmlElement]* - ): HtmlElement - - def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement - - def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement] = emptyMod, - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement - - /** Visage for clickable text, like links - */ - def clickable: Modifier[HtmlElement] - -trait DefaultPageComponentsModule(using ComponentContext) - extends PageComponentsModule: - - override val page: PageComponents = new PageComponents: - override def container( - children: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), - children - ) - - override def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement = - div( - cls("p-8 bg-gray-100 h-full"), - header, - children - ) - - override def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement = - div( - cls("pb-5 border-b border-gray-200"), - div(cls("float-right"), right), - h1( - cls("text-2xl leading-6 font-medium text-gray-900"), - title - ), - subtitle.map( - p( - cls("text-sm font-medium text-gray-500"), - _ - ) - ) - ) - - override def clickable: Modifier[HtmlElement] = - cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala deleted file mode 100644 index f70ee87..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala +++ /dev/null @@ -1,108 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} - -trait TableComponentsModule: - def tables: TableComponents - - trait TableComponents: - def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )(table: Modifier[HtmlElement]*): HtmlElement - - def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement - - def headerRow(mods: Modifier[HtmlElement]*)( - cells: HtmlElement* - ): HtmlElement - - def dataRow(mods: Modifier[HtmlElement]*)(cells: HtmlElement*): HtmlElement - - def headerCell(content: Modifier[HtmlElement]): HtmlElement - - def dataCell(content: Modifier[HtmlElement]): HtmlElement - -trait DefaultTableComponentsModule extends TableComponentsModule: - - override lazy val tables: TableComponents = new TableComponents: - - override def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )( - table: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("px-4 sm:px-6 lg:px-8"), - div( - cls("sm:flex sm:items-center"), - div( - cls("sm:flex-auto"), - h1(cls("text-base font-semibold leading-6 text-gray-900"), title), - subtitle.map(st => p(cls("mt-2 text-sm text-gray-700"), st)) - ), - div(cls("mt-4 sm:ml-16 sm:mt-0 sm:flex-none"), actions) - ), - div( - cls("mt-8 flow-root"), - div( - cls("-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"), - div( - cls("inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"), - table - ) - ) - ) - ) - - override def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement = - table( - cls("min-w-full divide-y divide-gray-300"), - thead(header), - tbody(cls("divide-y divide-gray-200"), body) - ) - - override def headerRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-3.5 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-3.5 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-3.5 px-3")) - ) - ) - - override def dataRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - mods, - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-4 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-4 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-4 px-3")) - ) - ) - - override def headerCell(content: Modifier[HtmlElement]): HtmlElement = - th( - cls("text-left text-sm font-semibold text-gray-900"), - content - ) - - override def dataCell(content: Modifier[HtmlElement]): HtmlElement = - td( - cls("whitespace-nowrap text-sm text-gray-500"), - content - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala index b937d45..12d01ae 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala @@ -3,7 +3,7 @@ import com.raquo.laminar.api.L.{*, given} import works.iterative.ui.components.laminar.HtmlTabular import works.iterative.ui.model.tables.Tabular -import works.iterative.ui.components.laminar.TableComponentsModule +import works.iterative.ui.components.laminar.tailwind.ui.TableComponentsModule import works.iterative.core.UserMessage import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given import works.iterative.ui.components.tailwind.ComponentContext diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala new file mode 100644 index 0000000..87f3e8d --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala @@ -0,0 +1,54 @@ +package works.iterative.ui.components.laminar.tailwind.color + +import com.raquo.laminar.api.L.{*, given} + +/** Complete color definition that can be rendered to CSS. + * + * Includes the area, kind and weight of the color. + */ +case class Color(area: ColorArea, color: ColorDef): + def toCSS: String = s"${area.name}-${color.toCSS}" + +object Color: + import ColorDef.given + + given colorToCSS: Conversion[Color, HtmlMod] with + def apply(c: Color) = cls(c.toCSS) + + given colorToSVGCSS: Conversion[Color, SvgMod] with + def apply(c: Color) = svg.cls(c.toCSS) + + given colorSignalToCSS: Conversion[Signal[Color], HtmlMod] with + def apply(c: Signal[Color]) = cls <-- c.map(_.toCSS) + + given colorSignalToSVGCSS: Conversion[Signal[Color], SvgMod] with + def apply(c: Signal[Color]) = svg.cls <-- c.map(_.toCSS) + + def current = ColorDef(ColorKind.current) + def inherit = ColorDef(ColorKind.inherit) + def transp = ColorDef(ColorKind.transp) + def auto = ColorDef(ColorKind.auto) + def black = ColorDef(ColorKind.black) + def white = ColorDef(ColorKind.white) + def slate(weight: ColorWeight) = ColorDef(ColorKind.slate, weight) + def gray(weight: ColorWeight) = ColorDef(ColorKind.gray, weight) + def zinc(weight: ColorWeight) = ColorDef(ColorKind.zinc, weight) + def neutral(weight: ColorWeight) = ColorDef(ColorKind.neutral, weight) + def stone(weight: ColorWeight) = ColorDef(ColorKind.stone, weight) + def red(weight: ColorWeight) = ColorDef(ColorKind.red, weight) + def orange(weight: ColorWeight) = ColorDef(ColorKind.orange, weight) + def amber(weight: ColorWeight) = ColorDef(ColorKind.amber, weight) + def yellow(weight: ColorWeight) = ColorDef(ColorKind.yellow, weight) + def lime(weight: ColorWeight) = ColorDef(ColorKind.lime, weight) + def green(weight: ColorWeight) = ColorDef(ColorKind.green, weight) + def emerald(weight: ColorWeight) = ColorDef(ColorKind.emerald, weight) + def teal(weight: ColorWeight) = ColorDef(ColorKind.teal, weight) + def cyan(weight: ColorWeight) = ColorDef(ColorKind.cyan, weight) + def sky(weight: ColorWeight) = ColorDef(ColorKind.sky, weight) + def blue(weight: ColorWeight) = ColorDef(ColorKind.blue, weight) + def indigo(weight: ColorWeight) = ColorDef(ColorKind.indigo, weight) + def violet(weight: ColorWeight) = ColorDef(ColorKind.violet, weight) + def purple(weight: ColorWeight) = ColorDef(ColorKind.purple, weight) + def fuchsia(weight: ColorWeight) = ColorDef(ColorKind.fuchsia, weight) + def pink(weight: ColorWeight) = ColorDef(ColorKind.pink, weight) + def rose(weight: ColorWeight) = ColorDef(ColorKind.rose, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala new file mode 100644 index 0000000..4130840 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala @@ -0,0 +1,16 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines the area the color should apply to, eg. background, text, border, + * etc. + */ +enum ColorArea(val name: String): + case bg extends ColorArea("bg") + case text extends ColorArea("text") + case decoration extends ColorArea("decoration") + case border extends ColorArea("border") + case outline extends ColorArea("outline") + case divide extends ColorArea("divide") + case ring extends ColorArea("ring") + case ringOffset extends ColorArea("ring-offset") + case shadow extends ColorArea("shadow") + case accent extends ColorArea("accent") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala new file mode 100644 index 0000000..fab9404 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala @@ -0,0 +1,43 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** A combination of ColorKind and ColorWeight, if applicable. + * + * By applying area we get the full Color definition. + */ +sealed trait ColorDef: + def toCSS: String + + def bg = Color(ColorArea.bg, this) + def text = Color(ColorArea.text, this) + def decoration = Color(ColorArea.decoration, this) + def border = Color(ColorArea.border, this) + def outline = Color(ColorArea.outline, this) + def divide = Color(ColorArea.divide, this) + def ring = Color(ColorArea.ring, this) + def ringOffset = Color(ColorArea.ringOffset, this) + def shadow = Color(ColorArea.shadow, this) + def accent = Color(ColorArea.accent, this) + +// TODO: create a macro that will output the tailwind class as string directly during compilation, where possible. +object ColorDef: + case class WeightedColorDef( + kind: ColorKind, + weight: ColorWeight + ) extends ColorDef: + override def toCSS: String = s"${kind.name}-${weight.value}" + + case class UnweightedColorDef( + kind: ColorKind + ) extends ColorDef: + override def toCSS: String = kind.name + + // TODO: check that the kind is valid unweighted kind + // that means current, inherit, auto, transparent, black, white + // tried using implicit evidence, but the type inference for enumerations + // tends to generalize to the enum, instead of the real type + def apply[T <: ColorKind](kind: T)(using + ev: T <:< ColorKind.Unweighted + ): ColorDef = + UnweightedColorDef(kind) + def apply(kind: ColorKind, weight: ColorWeight): ColorDef = + WeightedColorDef(kind, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala new file mode 100644 index 0000000..865166b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala @@ -0,0 +1,47 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines what color should be used, without specifying the area or weight. + */ +sealed abstract class ColorKind private (val name: String): + def apply(weight: ColorWeight): ColorDef = + ColorDef.WeightedColorDef(this, weight) + +object ColorKind: + trait Unweighted: + self: ColorKind => + override def apply(weight: ColorWeight): ColorDef = + ColorDef.UnweightedColorDef(self) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case object current extends ColorKind("current") with Unweighted + case object inherit extends ColorKind("inherit") with Unweighted + // Not present in for all methods + case object transp extends ColorKind("transparent") with Unweighted + // Seen in accent, not preset otherwise + case object auto extends ColorKind("auto") with Unweighted + // Black and white do not have weight + case object black extends ColorKind("black") with Unweighted + case object white extends ColorKind("white") with Unweighted + case object slate extends ColorKind("slate") + case object gray extends ColorKind("gray") + case object zinc extends ColorKind("zinc") + case object neutral extends ColorKind("neutral") + case object stone extends ColorKind("stone") + case object red extends ColorKind("red") + case object orange extends ColorKind("orange") + case object amber extends ColorKind("amber") + case object yellow extends ColorKind("yellow") + case object lime extends ColorKind("lime") + case object green extends ColorKind("green") + case object emerald extends ColorKind("emerald") + case object teal extends ColorKind("teal") + case object cyan extends ColorKind("cyan") + case object sky extends ColorKind("sky") + case object blue extends ColorKind("blue") + case object indigo extends ColorKind("indigo") + case object violet extends ColorKind("violet") + case object purple extends ColorKind("purple") + case object fuchsia extends ColorKind("fuchsia") + case object pink extends ColorKind("pink") + case object rose extends ColorKind("rose") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala new file mode 100644 index 0000000..6f1a4cc --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala @@ -0,0 +1,33 @@ +package works.iterative.ui.components.laminar.tailwind.color + +opaque type ColorWeight = String + +extension (c: ColorWeight) def value: String = c + +/** Defines weight of a color, eg. 50, 100, 200, etc. + * + * Tailwind-like. + */ +object ColorWeight: + inline given int50: Conversion[50, ColorWeight] with + inline def apply(i: 50) = "50" + inline given int100: Conversion[100, ColorWeight] with + inline def apply(i: 100) = "100" + inline given int200: Conversion[200, ColorWeight] with + inline def apply(i: 200) = "200" + inline given int300: Conversion[300, ColorWeight] with + inline def apply(i: 300) = "300" + inline given int400: Conversion[400, ColorWeight] with + inline def apply(i: 400) = "400" + inline given int500: Conversion[500, ColorWeight] with + inline def apply(i: 500) = "500" + inline given int600: Conversion[600, ColorWeight] with + inline def apply(i: 600) = "600" + inline given int700: Conversion[700, ColorWeight] with + inline def apply(i: 700) = "700" + inline given int800: Conversion[800, ColorWeight] with + inline def apply(i: 800) = "800" + inline given int900: Conversion[900, ColorWeight] with + inline def apply(i: 900) = "900" + inline given int950: Conversion[950, ColorWeight] with + inline def apply(i: 950) = "950" diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala new file mode 100644 index 0000000..21e21f0 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala @@ -0,0 +1,12 @@ +package works.iterative.ui.components.laminar.tailwind + +/** We need a generic Color model that can be used both on server and on client. + * + * We have adopted the Tailwind model for now. There is nothing inherently + * Tailwind-specific in the implementation, but all the values are taken from + * their palette and the area model is very HTML biased. + * + * Still, I think that it is a good starting point for exploration and will + * satisfy our current needs. + */ +package object color {} diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" new file mode 100644 index 0000000..5c4e218 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" @@ -0,0 +1,21 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.model.color.ColorKind +import works.iterative.ui.components.tailwind.ComponentContext +import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given +import works.iterative.ui.model.Tag + +trait TailwindUIBadgesComponentsModule(using ComponentContext): + + object badges: + override def tag(name: String, color: ColorKind): HtmlElement = + p( + cls( + "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" + ), + color(800).text, + color(100).bg, + name + ) diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" new file mode 100644 index 0000000..0c9ca25 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" @@ -0,0 +1,24 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} + +trait TailwindUIHeadingsComponentsModule: + val headings: HeadingsComponents = new HeadingsComponents + + class HeadingsComponents: + def section(title: Node, actions: HtmlElement*): HtmlElement = + div( + cls("border-b border-gray-200 pb-5"), + div( + cls("sm:flex sm:items-center sm:justify-between"), + h3(cls("text-base font-semibold leading-6 text-gray-900"), title), + div( + cls("mt-3 flex sm:ml-4 sm:mt-0"), + actions match + case Nil => emptyMod + case first :: rest => + first :: rest.map(_.amend(cls("ml-3"))) + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala new file mode 100644 index 0000000..16ca01b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala @@ -0,0 +1,19 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.laminar.tailwind.color.ColorKind + +trait BadgeComponentsModule: + + object badges: + def pill(name: String, color: ColorKind): HtmlElement = + p( + cls( + "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" + ), + color(800).text, + color(100).bg, + name + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala new file mode 100644 index 0000000..ebb2968 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} + +trait ButtonComponentsModule: + + object buttons: + + private inline def srHelp(text: String): Modifier[HtmlElement] = + span(cls := "sr-only", text) + + def primaryButton( + id: String, + text: Modifier[HtmlElement], + icon: Option[SvgElement] = None, + buttonType: String = "submit" + )(mods: Modifier[HtmlElement]*): HtmlElement = + button( + tpe(buttonType), + cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + icon, + text, + mods + ) + + def secondaryButton( + id: String, + text: Modifier[HtmlElement], + icon: Option[SvgElement] = None, + buttonType: String = "button" + )(mods: Modifier[HtmlElement]*): HtmlElement = + button( + tpe(buttonType), + cls( + "ml-2 bg-white inline-flex justify-center 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" + ), + icon, + text, + mods + ) + + def inlineButton(id: String, icon: SvgElement)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + button( + tpe := "button", + cls := "ml-1 inline-flex h-4 w-4 flex-shrink-0 rounded-full p-1 text-gray-400 hover:bg-gray-200 hover:text-gray-500", + srHelp(id), + icon + ) + + def iconButton(id: String, icon: SvgElement, srText: Option[String] = None)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + button( + tpe := "button", + cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", + icon, + srText.map(srHelp(_)) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala new file mode 100644 index 0000000..eb6dd5f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala @@ -0,0 +1,101 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import org.scalajs.dom.html +import com.raquo.laminar.modifiers.KeyUpdater + +trait FormComponentsModule: + self: IconsModule => + + object forms: + + def section( + title: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] + )(content: Modifier[HtmlElement]*): HtmlElement = + div( + cls("space-y-6 sm:space-y-5"), + div( + h3(cls("text-lg leading-6 font-medium text-gray-900"), title), + subtitle.map(st => p(cls("mt-1 max-w-2xl text-sm text-gray-500"), st)) + ), + div(cls("mt-6 sm:mt-5 space-y-6 sm:space-y-5"), content) + ) + + def label(labelText: String, forId: Option[String] = None)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + L.label( + cls("block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"), + forId.map(id => L.forId(id)), + labelText, + mods + ) + + def field( + label: Modifier[HtmlElement] + )(content: Modifier[HtmlElement]*): 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, + div(cls("mt-1 sm:mt-0 sm:col-span-2"), content) + ) + + def field( + id: String, + labelText: String, + input: HtmlElement, + help: Option[String] + ): HtmlElement = + field( + label(labelText, Some(id))() + )( + input.amend(idAttr(id)), + help.map(h => p(cls("mt-2 text-sm text-gray-500"), h)) + ) + + def form(mods: Modifier[HtmlElement]*)( + sections: Modifier[HtmlElement]* + )(actions: Modifier[HtmlElement]*): HtmlElement = + L.form( + cls("space-y-8 divide-y divide-gray-200"), + mods, + sections, + div( + cls("pt-5"), + div(cls("flex justify-end"), actions) + ) + ) + + def inlineForm( + mods: Modifier[HtmlElement]* + ): HtmlElement = + L.form(cls("flex space-x-4"), mods) + + def inputField( + id: String, + labelText: String, + placeholderText: Option[String] = None, + inputType: String = "text", + helpText: Option[String] = None + ): HtmlElement = + field( + id, + labelText, + input( + tpe(inputType), + cls( + "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:max-w-xs sm:text-sm sm:leading-6" + ), + placeholderText.map(placeholder(_)) + ), + helpText + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala new file mode 100644 index 0000000..52da07e --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala @@ -0,0 +1,22 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} + +trait HeadingsComponentsModule: + object headings: + def section(title: Node, actions: HtmlElement*): HtmlElement = + div( + cls("border-b border-gray-200 pb-5"), + div( + cls("sm:flex sm:items-center sm:justify-between"), + h3(cls("text-base font-semibold leading-6 text-gray-900"), title), + div( + cls("mt-3 flex sm:ml-4 sm:mt-0"), + actions match + case Nil => emptyMod + case first :: rest => + first :: rest.map(_.amend(cls("ml-3"))) + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala new file mode 100644 index 0000000..c543150 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala @@ -0,0 +1,112 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext +import works.iterative.ui.components.tailwind.CustomAttrs + +trait IconsModule: + object icons: + import svg.* + + object aria: + val hidden = CustomAttrs.svg.ariaHidden + + private def withDefault( + mods: Seq[Modifier[SvgElement]], + default: Modifier[SvgElement] + ): Modifier[SvgElement] = + if mods.isEmpty then default else mods + + def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-8 w-8"), + 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" + ) + ) + ) + + def close(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-2 w-2"), + stroke := "currentColor", + fill := "none", + viewBox := "0 0 8 8", + path( + strokeLineCap := "round", + strokeWidth := "1.5", + d := "M1 1l6 6m0-6L1 7" + ) + ) + + def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls("h-5 w-5 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 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") + ) + ) + + def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls("h-5 w-5 text-gray-400")), + 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") + ) + ) + + def `document-chart-bar-outline`( + mods: Modifier[SvgElement]* + ): SvgElement = + svg( + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + strokeWidth := "1.5", + stroke := "currentColor", + withDefault(mods, cls := "h-6 w-6"), + path( + strokeLineCap := "round", + strokeLineJoin := "round", + d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" + ) + ) + + def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-5 w-5"), + cls := "flex-shrink-0 text-gray-400", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", + clipRule := "evenodd" + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala new file mode 100644 index 0000000..02cbdb6 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala @@ -0,0 +1,11 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait LayoutModule: + object layout: + def card(content: Modifier[HtmlElement]*): HtmlElement = + div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala new file mode 100644 index 0000000..b652da9 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala @@ -0,0 +1,88 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.laminar.nodes.ReactiveHtmlElement +import org.scalajs.dom.html.Paragraph +import com.raquo.laminar.nodes.TextNode +import works.iterative.ui.components.laminar.tailwind.color.ColorKind + +trait ListComponentsModule: + self: BadgeComponentsModule => + + object list: + def label( + text: String, + color: ColorKind + ): HtmlElement = badges.pill(text, color) + + def item( + title: String, + subtitle: Option[String], + right: Modifier[HtmlElement] = emptyMod, + avatar: Option[Modifier[HtmlElement]] = None + ): LI = + li( + cls("group"), + div( + cls( + "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" + ), + avatar.map(a => + div( + cls("flex-shrink-0"), + div( + cls( + "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" + ), + a + ) + ) + ), + div( + cls("flex-1 min-w-0"), + p( + cls("text-sm font-medium text-gray-900"), + title, + span(cls("float-right"), right) + ), + subtitle.map(st => + p( + cls("text-sm text-gray-500 truncate"), + st + ) + ) + ) + ) + ) + + def unordered( + children: Modifier[HtmlElement] + ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = + ul( + cls("relative z-0 divide-y divide-gray-200"), + role("list"), + children + ) + + def listSection( + header: String, + list: HtmlElement + ): Div = + div( + cls("relative"), + div( + cls( + "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" + ), + header + ), + list + ) + + def navigation(sections: Modifier[HtmlElement]): HtmlElement = + navTag( + cls("flex-1 min-h-0 overflow-y-auto"), + sections + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ModalComponentsModule.scala new file mode 100644 index 0000000..6027404 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ModalComponentsModule.scala @@ -0,0 +1,54 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait ModalComponentsModule: + object modal: + def modalDialog( + content: HtmlElement, + closeMod: Modifier[HtmlElement] + ): 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( + cls("fixed inset-0 transition-opacity"), + div(cls("absolute inset-0 bg-gray-500 opacity-75")), + closeMod + ) + ) + + div( + cls("fixed inset-0 z-20 overflow-y-auto"), + div( + cls("text-center sm:block sm:p-0"), + overlay, + browserCenteringModalTrick, + 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-7xl sm:align-middle" + ), + content + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/PageComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/PageComponentsModule.scala new file mode 100644 index 0000000..d5a422e --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/PageComponentsModule.scala @@ -0,0 +1,49 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait PageComponentsModule: + + object page: + def container( + children: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), + children + ) + + def singleColumn( + header: Modifier[HtmlElement] + )(children: Modifier[HtmlElement]*): HtmlElement = + div( + cls("p-8 bg-gray-100 h-full"), + header, + children + ) + + def pageHeader( + title: Modifier[HtmlElement], + right: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] = None + ): HtmlElement = + div( + cls("pb-5 border-b border-gray-200"), + div(cls("float-right"), right), + h1( + cls("text-2xl leading-6 font-medium text-gray-900"), + title + ), + subtitle.map( + p( + cls("text-sm font-medium text-gray-500"), + _ + ) + ) + ) + + def clickable: Modifier[HtmlElement] = + cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TableComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TableComponentsModule.scala new file mode 100644 index 0000000..417ea56 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TableComponentsModule.scala @@ -0,0 +1,86 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L +import com.raquo.laminar.api.L.{*, given} + +trait TableComponentsModule: + + object tables: + + def tableSection( + title: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] = None, + actions: Modifier[HtmlElement]* + )( + table: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls("px-4 sm:px-6 lg:px-8"), + div( + cls("sm:flex sm:items-center"), + div( + cls("sm:flex-auto"), + h1(cls("text-base font-semibold leading-6 text-gray-900"), title), + subtitle.map(st => p(cls("mt-2 text-sm text-gray-700"), st)) + ), + div(cls("mt-4 sm:ml-16 sm:mt-0 sm:flex-none"), actions) + ), + div( + cls("mt-8 flow-root"), + div( + cls("-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"), + div( + cls("inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"), + table + ) + ) + ) + ) + + def simpleTable(header: Modifier[HtmlElement]*)( + body: Modifier[HtmlElement]* + ): HtmlElement = + table( + cls("min-w-full divide-y divide-gray-300"), + thead(header), + tbody(cls("divide-y divide-gray-200"), body) + ) + + def headerRow( + mods: Modifier[HtmlElement]* + )(cells: HtmlElement*): HtmlElement = + tr( + cells.zipWithIndex.map((c, i) => + if i == 0 then c.amend(cls("py-3.5 pl-4 pr-3 sm:pl-0")) + else if i == cells.length - 1 then + c.amend(cls("py-3.5 pr-4 pl-3 sm:pr-0")) + else c.amend(cls("py-3.5 px-3")) + ) + ) + + def dataRow( + mods: Modifier[HtmlElement]* + )(cells: HtmlElement*): HtmlElement = + tr( + mods, + cells.zipWithIndex.map((c, i) => + if i == 0 then c.amend(cls("py-4 pl-4 pr-3 sm:pl-0")) + else if i == cells.length - 1 then + c.amend(cls("py-4 pr-4 pl-3 sm:pr-0")) + else c.amend(cls("py-4 px-3")) + ) + ) + + def headerCell(content: Modifier[HtmlElement]): HtmlElement = + th( + cls("text-left text-sm font-semibold text-gray-900"), + content + ) + + def dataCell(content: Modifier[HtmlElement]): HtmlElement = + td( + cls("whitespace-nowrap text-sm text-gray-500"), + content + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TailwindUICatalogue.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TailwindUICatalogue.scala new file mode 100644 index 0000000..5027b10 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TailwindUICatalogue.scala @@ -0,0 +1,17 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} + +trait TailwindUICatalogueModule + extends BadgeComponentsModule + with ButtonComponentsModule + with HeadingsComponentsModule + with IconsModule + with FormComponentsModule + with LayoutModule + with ListComponentsModule + with ModalComponentsModule + with PageComponentsModule + with TableComponentsModule diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/LabelsOnLeftFormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/LabelsOnLeftFormComponentsModule.scala deleted file mode 100644 index 210bbf8..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/LabelsOnLeftFormComponentsModule.scala +++ /dev/null @@ -1,155 +0,0 @@ -package works.iterative.ui.components.laminar -package tailwindui - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -trait LabelsOnLeftFormComponentsModule(using ctx: ComponentContext) - extends FormComponentsModule: - self: IconsModule => - override val forms = new FormComponents: - - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )(content: Modifier[HtmlElement]*): HtmlElement = - div( - cls("space-y-6 sm:space-y-5"), - div( - h3(cls("text-lg leading-6 font-medium text-gray-900"), title), - subtitle.map(st => p(cls("mt-1 max-w-2xl text-sm text-gray-500"), st)) - ), - div(cls("mt-6 sm:mt-5 space-y-6 sm:space-y-5"), content) - ) - - override def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement = - L.label( - cls("block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"), - forId.map(id => L.forId(id)), - labelText, - mods - ) - - override def field( - label: Modifier[HtmlElement] - )(content: Modifier[HtmlElement]*): 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, - div(cls("mt-1 sm:mt-0 sm:col-span-2"), content) - ) - - override def field( - id: String, - labelText: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement = - field( - label(labelText, Some(id))() - )( - input.amend(idAttr(id)), - help.map(h => p(cls("mt-2 text-sm text-gray-500"), h)) - ) - - override def form(mods: Modifier[HtmlElement]*)( - sections: Modifier[HtmlElement]* - )(actions: Modifier[HtmlElement]*): HtmlElement = - L.form( - cls("space-y-8 divide-y divide-gray-200"), - mods, - sections, - div( - cls("pt-5"), - div(cls("flex justify-end"), actions) - ) - ) - - override def inlineForm( - mods: Modifier[HtmlElement]* - ): HtmlElement = - L.form(cls("flex space-x-4"), mods) - - override def searchField( - id: String, - placeholderText: Option[String] = None - )( - mods: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "flex-1 min-w-0", - label(ctx.messages("forms.search.label"), Some(id))(cls("sr-only")), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - icons.`search-solid`() - ), - input( - tpe := "search", - nameAttr := "search", - idAttr := id, - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholderText - .orElse( - ctx.messages - .opt( - s"forms.search.${id}.placeholder", - s"form.search.placeholder" - ) - ) - .map(placeholder(_)), - mods - ) - ) - ) - - override def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement = - field( - id, - labelText, - input( - tpe(inputType), - cls( - "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:max-w-xs sm:text-sm sm:leading-6" - ), - placeholderText.map(placeholder(_)) - ), - helpText - ) - - override def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement = - div( - labelText, - input( - idAttr(id), - nameAttr(id), - autoComplete("date"), - tpe("date"), - cls( - "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ), - placeholderText.map(placeholder(_)), - mods(localDateSelect) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/TailwindUIButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/TailwindUIButtonComponentsModule.scala deleted file mode 100644 index 4c44157..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/TailwindUIButtonComponentsModule.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar -package tailwindui - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait TailwindUIButtonComponentsModule(using ctx: ComponentContext) - extends ButtonComponentsModule: - - override val buttons = new ButtonComponents: - - private inline def srHelp(id: String): Modifier[HtmlElement] = - ctx.messages - .opt(s"form.button.${id}.screenReaderHelp") - .map(sr => span(cls := "sr-only", sr)) - - override def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )(mods: Modifier[HtmlElement]*): HtmlElement = - button( - tpe(buttonType), - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - icon, - text, - mods - ) - - override def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )(mods: Modifier[HtmlElement]*): HtmlElement = - button( - tpe(buttonType), - cls( - "ml-2 bg-white inline-flex justify-center 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" - ), - icon, - text, - mods - ) - - override def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement = - button( - tpe := "button", - cls := "ml-1 inline-flex h-4 w-4 flex-shrink-0 rounded-full p-1 text-gray-400 hover:bg-gray-200 hover:text-gray-500", - srHelp(id), - icon - ) - - override def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement = - button( - tpe := "button", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - icon, - srHelp(id) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/tailwind/laminar/LaminarExtensions.scala b/ui/js/src/main/scala/works/iterative/ui/components/tailwind/laminar/LaminarExtensions.scala index 4c5e68b..4c87800 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/tailwind/laminar/LaminarExtensions.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/tailwind/laminar/LaminarExtensions.scala @@ -2,23 +2,9 @@ package laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.Color import works.iterative.core.UserMessage object LaminarExtensions: - given colorToCSS: Conversion[Color, Modifier[HtmlElement]] with - def apply(c: Color) = cls(c.toCSS) - - given colorToSVGCSS: Conversion[Color, Modifier[SvgElement]] with - def apply(c: Color) = svg.cls(c.toCSS) - - given colorSignalToCSS: Conversion[Signal[Color], Modifier[HtmlElement]] with - def apply(c: Signal[Color]) = cls <-- c.map(_.toCSS) - - given colorSignalToSVGCSS: Conversion[Signal[Color], Modifier[SvgElement]] - with - def apply(c: Signal[Color]) = svg.cls <-- c.map(_.toCSS) - inline given userMessageToModifier(using ctx: ComponentContext ): Conversion[UserMessage, Modifier[HtmlElement]] with diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala index a2c637c..bfa063a 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala @@ -1,13 +1,11 @@ package works.iterative.ui.components.laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorDef import com.raquo.laminar.tags.HtmlTag case class Bin[Source, +Value]( label: String, description: Option[String | HtmlElement], - color: ColorDef, valueOf: Source => Value ): def map[NewValue]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala deleted file mode 100644 index 3aac0bf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala +++ /dev/null @@ -1,34 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ButtonComponentsModule: - def buttons: ButtonComponents - - trait ButtonComponents: - def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala deleted file mode 100644 index 646e73d..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala +++ /dev/null @@ -1,191 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.color.ColorDef -import com.raquo.laminar.tags.HtmlTag -import org.scalajs.dom.html.UList -import com.raquo.laminar.nodes.ReactiveHtmlElement -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind - -trait DashboardComponentsModule: - def dashboard: DashboardComponents - - // Only methods that create HtmlElements - trait DashboardComponents(using ComponentContext): - def section(label: String, children: Modifier[HtmlElement]): Div - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI - def cardInitials( - initials: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div - def count(label: String, value: Int, color: ColorDef): Span - def counts(children: HtmlElement*): Span - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - -trait DefaultDashboardComponentsModule(using ComponentContext) - extends DashboardComponentsModule: - override val dashboard: DashboardComponents = new DashboardComponents: - def section(label: String, children: Modifier[HtmlElement]): Div = - div( - cls("mt-3"), - h2( - cls("text-gray-500 text-xs font-medium uppercase tracking-wide"), - label - ), - children - ) - - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] = - ul( - cls( - "mt-3 grid gap-5 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" - ), - children - ) - - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI = - li( - div( - cls("col-span-1 flex shadow-sm rounded-md"), - initials, - div( - cls( - "flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate" - ), - div( - cls("flex-1 px-4 py-2 text-sm truncate"), - div( - cls("text-gray-900 font-medium hover:text-gray-600 truncate"), - label - ), - p(cls("text-gray-500"), children) - ) - ) - ) - ) - - def cardInitials( - id: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div = - div( - cls( - "flex-shrink-0 flex flex-col items-center justify-center w-16 text-white text-sm font-medium rounded-l-md" - ), - color.map(_(600).bg), - div(id), - children - ) - - def count(label: String, value: Int, color: ColorDef): Span = - span(color.text, title(label), s"${value}") - - def counts(children: HtmlElement*): Span = - span(interleave(children, span(" / "))) - - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div = - div( - cls("flex flex-col mt-2"), - div( - cls( - "align-middle min-w-full shadow sm:rounded-lg" - ), - L.table( - cls("min-w-full border-separate border-spacing-0"), - styleAttr("border-spacing: 0"), // Tailwind somehow doesn't work - thead( - tr( - cls("border-t border-gray-200"), - headers.map(h => - th( - cls( - "sticky top-0 z-10 px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - h - ) - ) - ) - ), - tbody(cls("bg-white divide-y divide-gray-100"), sections) - ) - ) - ) - - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - td( - cls("bg-gray-50 border-gray-200 border-t border-b"), - cls( - "sticky top-14 z-10 bg-gray-50 px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - colSpan(cs), - label - ), - mods - ) - - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - mods, - td( - cls( - "px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900 truncate" - ), - label - ), - children.map(e => - td( - cls( - "whitespace-nowrap px-2 py-2 text-sm text-gray-500" - ), - e - ) - ) - ) - - private def interleave( - a: Seq[HtmlElement], - separator: => HtmlElement - ): Seq[HtmlElement] = - if a.size < 2 then a - else a.init.flatMap(n => Seq(n, separator)) :+ a.last diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala deleted file mode 100644 index a45beaf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.FileRef - -trait DetailComponentsModule: - - def details: DetailComponents - - trait DetailComponents: - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement - - def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement - - def files(fs: List[FileRef]): HtmlElement - - def file(f: FileRef): HtmlElement - -trait DefaultDetailComponentsModule(using ctx: ComponentContext) - extends DetailComponentsModule: - self: IconsModule => - - override val details: DetailComponents = new DetailComponents: - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement = - div( - div( - cls := "px-4 sm:px-0", - h3( - cls := "text-base font-semibold leading-7 text-gray-900", - title - ), - subtitle.map(st => - p( - cls := "mt-1 max-w-2xl text-sm leading-6 text-gray-500", - st - ) - ) - // TODO: actions - ), - div( - cls := "mt-6 border-t border-gray-100", - dl( - cls := "divide-y divide-gray-100", - fields - ) - ) - ) - - override def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0", - dt( - cls := "text-sm font-medium leading-6 text-gray-900", - title - ), - dd( - cls := "mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0", - content - ) - ) - - override def files(fs: List[FileRef]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-100 rounded-md border border-gray-200", - fs.map(file) - ) - - override def file(f: FileRef): HtmlElement = - li( - cls := "flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6", - div( - cls := "flex w-0 flex-1 items-center", - icons.`paper-clip-solid`(), - div( - cls := "ml-4 flex min-w-0 flex-1 gap-2", - span( - cls := "truncate font-medium", - f.name - ), - f.size.map(size => span(cls := "flex-shrink-0 text-gray-400", size)) - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - "Uložit" - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala new file mode 100644 index 0000000..f6c583f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait FormComponents(using ctx: ComponentContext) extends LocalDateSelectModule: + def searchIcon: SvgElement + + def renderLocalDateSelect( + id: String, + labelText: Option[String], + placeholderText: Option[String], + mods: LocalDateSelect => Modifier[HtmlElement] + ): HtmlElement = + div( + labelText, + input( + idAttr(id), + nameAttr(id), + autoComplete("date"), + tpe("date"), + cls( + "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ), + placeholderText.map(placeholder(_)), + mods(localDateSelect) + ) + ) + + def searchField( + id: String, + placeholderText: Option[String] = None + )( + mods: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls := "flex-1 min-w-0", + label(ctx.messages("forms.search.label"), forId(id), cls("sr-only")), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + searchIcon + ), + input( + tpe := "search", + nameAttr := "search", + idAttr := id, + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholderText + .orElse( + ctx.messages + .opt( + s"forms.search.${id}.placeholder", + s"form.search.placeholder" + ) + ) + .map(placeholder(_)), + mods + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala deleted file mode 100644 index 7714490..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala +++ /dev/null @@ -1,82 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -/** Form components - * - * A form is a collection of sections, each of which contains a collection of - * fields. - * - * Form -> * Section -> * Field - * -> * Action - * - * Each Field has an `id`, `label` and `input` element. The `id` is used to - * link the label to the input. - * - * The `input` element can be a simple text input, or a more complex component - * such as a date picker. The caller is expected to make the input work. - */ -trait FormComponentsModule extends LocalDateSelectModule: - def forms: FormComponents - - trait FormComponents: - - /** Layout the sections and actions of a form */ - def form(mods: Modifier[HtmlElement]*)(sections: Modifier[HtmlElement]*)( - actions: Modifier[HtmlElement]* - ): HtmlElement - - /** Section layout, with a title, optional subtitle and any content. */ - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label and any content */ - def field(label: Modifier[HtmlElement])( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label, input and optional help text */ - def field( - id: String, - label: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement - - def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout an inline form element */ - def inlineForm(mods: Modifier[HtmlElement]*): HtmlElement - - /** An input field with a search icon */ - def searchField(id: String, placeholderText: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement - - /** LocalDate input */ - def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala deleted file mode 100644 index 8acbe7f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala +++ /dev/null @@ -1,28 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.Tag - -trait GenericComponentsModule: - - def generic: GenericComponents - - trait GenericComponents: - def tag(name: String, color: ColorKind): HtmlElement - def tag(t: Tag): HtmlElement = tag(t.value, t.color) - -trait DefaultGenericComponentsModule(using ComponentContext) - extends GenericComponentsModule: - override val generic: GenericComponents = new GenericComponents: - override def tag(name: String, color: ColorKind): HtmlElement = - p( - cls( - "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" - ), - color(800).text, - color(100).bg, - name - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala deleted file mode 100644 index 8cfc1d9..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala +++ /dev/null @@ -1,123 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.CustomAttrs - -trait IconsModule: - def icons: Icons - - trait Icons: - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement - def close(mods: Modifier[SvgElement]*): SvgElement - def `search-solid`(mods: Modifier[SvgElement]*): SvgElement - def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement - def `document-chart-bar-outline`(mods: Modifier[SvgElement]*): SvgElement - def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement - -trait DefaultIconsModule(using ComponentContext) extends IconsModule: - override val icons: Icons = new Icons: - import svg.* - - object aria: - val hidden = CustomAttrs.svg.ariaHidden - - private def withDefault( - mods: Seq[Modifier[SvgElement]], - default: Modifier[SvgElement] - ): Modifier[SvgElement] = - if mods.isEmpty then default else mods - - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-8 w-8"), - 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" - ) - ) - ) - - override def close(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-2 w-2"), - stroke := "currentColor", - fill := "none", - viewBox := "0 0 8 8", - path( - strokeLineCap := "round", - strokeWidth := "1.5", - d := "M1 1l6 6m0-6L1 7" - ) - ) - - override def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 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 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") - ) - ) - - override def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 text-gray-400")), - 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") - ) - ) - - override def `document-chart-bar-outline`( - mods: Modifier[SvgElement]* - ): SvgElement = - svg( - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - strokeWidth := "1.5", - stroke := "currentColor", - withDefault(mods, cls := "h-6 w-6"), - path( - strokeLineCap := "round", - strokeLineJoin := "round", - d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" - ) - ) - - override def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-5 w-5"), - cls := "flex-shrink-0 text-gray-400", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", - clipRule := "evenodd" - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala deleted file mode 100644 index 95ab654..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait LayoutModule: - def layout: LayoutComponents - - trait LayoutComponents: - def card(content: Modifier[HtmlElement]*): HtmlElement - -trait DefaultLayoutModule(using ctx: ComponentContext) extends LayoutModule: - override val layout: LayoutComponents = new LayoutComponents: - override def card(content: Modifier[HtmlElement]*): HtmlElement = - div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala deleted file mode 100644 index 5946974..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom.html.Paragraph -import com.raquo.laminar.nodes.TextNode - -trait ListComponentsModule: - - def list: ListComponents - - trait ListComponents(using ComponentContext): - def label(text: String, color: ColorKind): HtmlElement - def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI - def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] - def listSection(header: String, list: HtmlElement): Div - def navigation(sections: Modifier[HtmlElement]): HtmlElement - -trait DefaultListComponentsModule(using ComponentContext) - extends ListComponentsModule: - self: GenericComponentsModule => - - override val list: ListComponents = new ListComponents: - override def label( - text: String, - color: ColorKind - ): HtmlElement = generic.tag(text, color) - - override def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI = - li( - cls("group"), - div( - cls( - "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" - ), - avatar.map(a => - div( - cls("flex-shrink-0"), - div( - cls( - "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" - ), - a - ) - ) - ), - div( - cls("flex-1 min-w-0"), - p( - cls("text-sm font-medium text-gray-900"), - title, - span(cls("float-right"), right) - ), - subtitle.map(st => - p( - cls("text-sm text-gray-500 truncate"), - st - ) - ) - ) - ) - ) - - override def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = - ul( - cls("relative z-0 divide-y divide-gray-200"), - role("list"), - children - ) - - override def listSection( - header: String, - list: HtmlElement - ): Div = - div( - cls("relative"), - div( - cls( - "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" - ), - header - ), - list - ) - - override def navigation(sections: Modifier[HtmlElement]): HtmlElement = - navTag( - cls("flex-1 min-h-0 overflow-y-auto"), - sections - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala deleted file mode 100644 index 5b5490f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ModalComponentsModule: - - def modal: ModalComponents - - trait ModalComponents: - def modalDialog( - content: HtmlElement, - onClose: Modifier[HtmlElement] - ): HtmlElement - -trait DefaultModalComponentsModule(using ctx: ComponentContext) - extends ModalComponentsModule: - - override val modal: ModalComponents = new ModalComponents: - - override def modalDialog( - content: HtmlElement, - closeMod: Modifier[HtmlElement] - ): 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( - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")), - closeMod - ) - ) - - div( - cls("fixed inset-0 z-20 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - 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-7xl sm:align-middle" - ), - content - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala deleted file mode 100644 index b713d8a..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala +++ /dev/null @@ -1,71 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait PageComponentsModule: - - val page: PageComponents - - trait PageComponents: - def container( - children: Modifier[HtmlElement]* - ): HtmlElement - - def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement - - def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement] = emptyMod, - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement - - /** Visage for clickable text, like links - */ - def clickable: Modifier[HtmlElement] - -trait DefaultPageComponentsModule(using ComponentContext) - extends PageComponentsModule: - - override val page: PageComponents = new PageComponents: - override def container( - children: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), - children - ) - - override def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement = - div( - cls("p-8 bg-gray-100 h-full"), - header, - children - ) - - override def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement = - div( - cls("pb-5 border-b border-gray-200"), - div(cls("float-right"), right), - h1( - cls("text-2xl leading-6 font-medium text-gray-900"), - title - ), - subtitle.map( - p( - cls("text-sm font-medium text-gray-500"), - _ - ) - ) - ) - - override def clickable: Modifier[HtmlElement] = - cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala deleted file mode 100644 index f70ee87..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala +++ /dev/null @@ -1,108 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} - -trait TableComponentsModule: - def tables: TableComponents - - trait TableComponents: - def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )(table: Modifier[HtmlElement]*): HtmlElement - - def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement - - def headerRow(mods: Modifier[HtmlElement]*)( - cells: HtmlElement* - ): HtmlElement - - def dataRow(mods: Modifier[HtmlElement]*)(cells: HtmlElement*): HtmlElement - - def headerCell(content: Modifier[HtmlElement]): HtmlElement - - def dataCell(content: Modifier[HtmlElement]): HtmlElement - -trait DefaultTableComponentsModule extends TableComponentsModule: - - override lazy val tables: TableComponents = new TableComponents: - - override def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )( - table: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("px-4 sm:px-6 lg:px-8"), - div( - cls("sm:flex sm:items-center"), - div( - cls("sm:flex-auto"), - h1(cls("text-base font-semibold leading-6 text-gray-900"), title), - subtitle.map(st => p(cls("mt-2 text-sm text-gray-700"), st)) - ), - div(cls("mt-4 sm:ml-16 sm:mt-0 sm:flex-none"), actions) - ), - div( - cls("mt-8 flow-root"), - div( - cls("-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"), - div( - cls("inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"), - table - ) - ) - ) - ) - - override def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement = - table( - cls("min-w-full divide-y divide-gray-300"), - thead(header), - tbody(cls("divide-y divide-gray-200"), body) - ) - - override def headerRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-3.5 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-3.5 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-3.5 px-3")) - ) - ) - - override def dataRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - mods, - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-4 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-4 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-4 px-3")) - ) - ) - - override def headerCell(content: Modifier[HtmlElement]): HtmlElement = - th( - cls("text-left text-sm font-semibold text-gray-900"), - content - ) - - override def dataCell(content: Modifier[HtmlElement]): HtmlElement = - td( - cls("whitespace-nowrap text-sm text-gray-500"), - content - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala index b937d45..12d01ae 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala @@ -3,7 +3,7 @@ import com.raquo.laminar.api.L.{*, given} import works.iterative.ui.components.laminar.HtmlTabular import works.iterative.ui.model.tables.Tabular -import works.iterative.ui.components.laminar.TableComponentsModule +import works.iterative.ui.components.laminar.tailwind.ui.TableComponentsModule import works.iterative.core.UserMessage import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given import works.iterative.ui.components.tailwind.ComponentContext diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala new file mode 100644 index 0000000..87f3e8d --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala @@ -0,0 +1,54 @@ +package works.iterative.ui.components.laminar.tailwind.color + +import com.raquo.laminar.api.L.{*, given} + +/** Complete color definition that can be rendered to CSS. + * + * Includes the area, kind and weight of the color. + */ +case class Color(area: ColorArea, color: ColorDef): + def toCSS: String = s"${area.name}-${color.toCSS}" + +object Color: + import ColorDef.given + + given colorToCSS: Conversion[Color, HtmlMod] with + def apply(c: Color) = cls(c.toCSS) + + given colorToSVGCSS: Conversion[Color, SvgMod] with + def apply(c: Color) = svg.cls(c.toCSS) + + given colorSignalToCSS: Conversion[Signal[Color], HtmlMod] with + def apply(c: Signal[Color]) = cls <-- c.map(_.toCSS) + + given colorSignalToSVGCSS: Conversion[Signal[Color], SvgMod] with + def apply(c: Signal[Color]) = svg.cls <-- c.map(_.toCSS) + + def current = ColorDef(ColorKind.current) + def inherit = ColorDef(ColorKind.inherit) + def transp = ColorDef(ColorKind.transp) + def auto = ColorDef(ColorKind.auto) + def black = ColorDef(ColorKind.black) + def white = ColorDef(ColorKind.white) + def slate(weight: ColorWeight) = ColorDef(ColorKind.slate, weight) + def gray(weight: ColorWeight) = ColorDef(ColorKind.gray, weight) + def zinc(weight: ColorWeight) = ColorDef(ColorKind.zinc, weight) + def neutral(weight: ColorWeight) = ColorDef(ColorKind.neutral, weight) + def stone(weight: ColorWeight) = ColorDef(ColorKind.stone, weight) + def red(weight: ColorWeight) = ColorDef(ColorKind.red, weight) + def orange(weight: ColorWeight) = ColorDef(ColorKind.orange, weight) + def amber(weight: ColorWeight) = ColorDef(ColorKind.amber, weight) + def yellow(weight: ColorWeight) = ColorDef(ColorKind.yellow, weight) + def lime(weight: ColorWeight) = ColorDef(ColorKind.lime, weight) + def green(weight: ColorWeight) = ColorDef(ColorKind.green, weight) + def emerald(weight: ColorWeight) = ColorDef(ColorKind.emerald, weight) + def teal(weight: ColorWeight) = ColorDef(ColorKind.teal, weight) + def cyan(weight: ColorWeight) = ColorDef(ColorKind.cyan, weight) + def sky(weight: ColorWeight) = ColorDef(ColorKind.sky, weight) + def blue(weight: ColorWeight) = ColorDef(ColorKind.blue, weight) + def indigo(weight: ColorWeight) = ColorDef(ColorKind.indigo, weight) + def violet(weight: ColorWeight) = ColorDef(ColorKind.violet, weight) + def purple(weight: ColorWeight) = ColorDef(ColorKind.purple, weight) + def fuchsia(weight: ColorWeight) = ColorDef(ColorKind.fuchsia, weight) + def pink(weight: ColorWeight) = ColorDef(ColorKind.pink, weight) + def rose(weight: ColorWeight) = ColorDef(ColorKind.rose, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala new file mode 100644 index 0000000..4130840 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala @@ -0,0 +1,16 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines the area the color should apply to, eg. background, text, border, + * etc. + */ +enum ColorArea(val name: String): + case bg extends ColorArea("bg") + case text extends ColorArea("text") + case decoration extends ColorArea("decoration") + case border extends ColorArea("border") + case outline extends ColorArea("outline") + case divide extends ColorArea("divide") + case ring extends ColorArea("ring") + case ringOffset extends ColorArea("ring-offset") + case shadow extends ColorArea("shadow") + case accent extends ColorArea("accent") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala new file mode 100644 index 0000000..fab9404 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala @@ -0,0 +1,43 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** A combination of ColorKind and ColorWeight, if applicable. + * + * By applying area we get the full Color definition. + */ +sealed trait ColorDef: + def toCSS: String + + def bg = Color(ColorArea.bg, this) + def text = Color(ColorArea.text, this) + def decoration = Color(ColorArea.decoration, this) + def border = Color(ColorArea.border, this) + def outline = Color(ColorArea.outline, this) + def divide = Color(ColorArea.divide, this) + def ring = Color(ColorArea.ring, this) + def ringOffset = Color(ColorArea.ringOffset, this) + def shadow = Color(ColorArea.shadow, this) + def accent = Color(ColorArea.accent, this) + +// TODO: create a macro that will output the tailwind class as string directly during compilation, where possible. +object ColorDef: + case class WeightedColorDef( + kind: ColorKind, + weight: ColorWeight + ) extends ColorDef: + override def toCSS: String = s"${kind.name}-${weight.value}" + + case class UnweightedColorDef( + kind: ColorKind + ) extends ColorDef: + override def toCSS: String = kind.name + + // TODO: check that the kind is valid unweighted kind + // that means current, inherit, auto, transparent, black, white + // tried using implicit evidence, but the type inference for enumerations + // tends to generalize to the enum, instead of the real type + def apply[T <: ColorKind](kind: T)(using + ev: T <:< ColorKind.Unweighted + ): ColorDef = + UnweightedColorDef(kind) + def apply(kind: ColorKind, weight: ColorWeight): ColorDef = + WeightedColorDef(kind, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala new file mode 100644 index 0000000..865166b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala @@ -0,0 +1,47 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines what color should be used, without specifying the area or weight. + */ +sealed abstract class ColorKind private (val name: String): + def apply(weight: ColorWeight): ColorDef = + ColorDef.WeightedColorDef(this, weight) + +object ColorKind: + trait Unweighted: + self: ColorKind => + override def apply(weight: ColorWeight): ColorDef = + ColorDef.UnweightedColorDef(self) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case object current extends ColorKind("current") with Unweighted + case object inherit extends ColorKind("inherit") with Unweighted + // Not present in for all methods + case object transp extends ColorKind("transparent") with Unweighted + // Seen in accent, not preset otherwise + case object auto extends ColorKind("auto") with Unweighted + // Black and white do not have weight + case object black extends ColorKind("black") with Unweighted + case object white extends ColorKind("white") with Unweighted + case object slate extends ColorKind("slate") + case object gray extends ColorKind("gray") + case object zinc extends ColorKind("zinc") + case object neutral extends ColorKind("neutral") + case object stone extends ColorKind("stone") + case object red extends ColorKind("red") + case object orange extends ColorKind("orange") + case object amber extends ColorKind("amber") + case object yellow extends ColorKind("yellow") + case object lime extends ColorKind("lime") + case object green extends ColorKind("green") + case object emerald extends ColorKind("emerald") + case object teal extends ColorKind("teal") + case object cyan extends ColorKind("cyan") + case object sky extends ColorKind("sky") + case object blue extends ColorKind("blue") + case object indigo extends ColorKind("indigo") + case object violet extends ColorKind("violet") + case object purple extends ColorKind("purple") + case object fuchsia extends ColorKind("fuchsia") + case object pink extends ColorKind("pink") + case object rose extends ColorKind("rose") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala new file mode 100644 index 0000000..6f1a4cc --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala @@ -0,0 +1,33 @@ +package works.iterative.ui.components.laminar.tailwind.color + +opaque type ColorWeight = String + +extension (c: ColorWeight) def value: String = c + +/** Defines weight of a color, eg. 50, 100, 200, etc. + * + * Tailwind-like. + */ +object ColorWeight: + inline given int50: Conversion[50, ColorWeight] with + inline def apply(i: 50) = "50" + inline given int100: Conversion[100, ColorWeight] with + inline def apply(i: 100) = "100" + inline given int200: Conversion[200, ColorWeight] with + inline def apply(i: 200) = "200" + inline given int300: Conversion[300, ColorWeight] with + inline def apply(i: 300) = "300" + inline given int400: Conversion[400, ColorWeight] with + inline def apply(i: 400) = "400" + inline given int500: Conversion[500, ColorWeight] with + inline def apply(i: 500) = "500" + inline given int600: Conversion[600, ColorWeight] with + inline def apply(i: 600) = "600" + inline given int700: Conversion[700, ColorWeight] with + inline def apply(i: 700) = "700" + inline given int800: Conversion[800, ColorWeight] with + inline def apply(i: 800) = "800" + inline given int900: Conversion[900, ColorWeight] with + inline def apply(i: 900) = "900" + inline given int950: Conversion[950, ColorWeight] with + inline def apply(i: 950) = "950" diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala new file mode 100644 index 0000000..21e21f0 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala @@ -0,0 +1,12 @@ +package works.iterative.ui.components.laminar.tailwind + +/** We need a generic Color model that can be used both on server and on client. + * + * We have adopted the Tailwind model for now. There is nothing inherently + * Tailwind-specific in the implementation, but all the values are taken from + * their palette and the area model is very HTML biased. + * + * Still, I think that it is a good starting point for exploration and will + * satisfy our current needs. + */ +package object color {} diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" new file mode 100644 index 0000000..5c4e218 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" @@ -0,0 +1,21 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.model.color.ColorKind +import works.iterative.ui.components.tailwind.ComponentContext +import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given +import works.iterative.ui.model.Tag + +trait TailwindUIBadgesComponentsModule(using ComponentContext): + + object badges: + override def tag(name: String, color: ColorKind): HtmlElement = + p( + cls( + "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" + ), + color(800).text, + color(100).bg, + name + ) diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" new file mode 100644 index 0000000..0c9ca25 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" @@ -0,0 +1,24 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} + +trait TailwindUIHeadingsComponentsModule: + val headings: HeadingsComponents = new HeadingsComponents + + class HeadingsComponents: + def section(title: Node, actions: HtmlElement*): HtmlElement = + div( + cls("border-b border-gray-200 pb-5"), + div( + cls("sm:flex sm:items-center sm:justify-between"), + h3(cls("text-base font-semibold leading-6 text-gray-900"), title), + div( + cls("mt-3 flex sm:ml-4 sm:mt-0"), + actions match + case Nil => emptyMod + case first :: rest => + first :: rest.map(_.amend(cls("ml-3"))) + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala new file mode 100644 index 0000000..16ca01b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala @@ -0,0 +1,19 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.laminar.tailwind.color.ColorKind + +trait BadgeComponentsModule: + + object badges: + def pill(name: String, color: ColorKind): HtmlElement = + p( + cls( + "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" + ), + color(800).text, + color(100).bg, + name + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala new file mode 100644 index 0000000..ebb2968 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} + +trait ButtonComponentsModule: + + object buttons: + + private inline def srHelp(text: String): Modifier[HtmlElement] = + span(cls := "sr-only", text) + + def primaryButton( + id: String, + text: Modifier[HtmlElement], + icon: Option[SvgElement] = None, + buttonType: String = "submit" + )(mods: Modifier[HtmlElement]*): HtmlElement = + button( + tpe(buttonType), + cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + icon, + text, + mods + ) + + def secondaryButton( + id: String, + text: Modifier[HtmlElement], + icon: Option[SvgElement] = None, + buttonType: String = "button" + )(mods: Modifier[HtmlElement]*): HtmlElement = + button( + tpe(buttonType), + cls( + "ml-2 bg-white inline-flex justify-center 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" + ), + icon, + text, + mods + ) + + def inlineButton(id: String, icon: SvgElement)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + button( + tpe := "button", + cls := "ml-1 inline-flex h-4 w-4 flex-shrink-0 rounded-full p-1 text-gray-400 hover:bg-gray-200 hover:text-gray-500", + srHelp(id), + icon + ) + + def iconButton(id: String, icon: SvgElement, srText: Option[String] = None)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + button( + tpe := "button", + cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", + icon, + srText.map(srHelp(_)) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala new file mode 100644 index 0000000..eb6dd5f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala @@ -0,0 +1,101 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import org.scalajs.dom.html +import com.raquo.laminar.modifiers.KeyUpdater + +trait FormComponentsModule: + self: IconsModule => + + object forms: + + def section( + title: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] + )(content: Modifier[HtmlElement]*): HtmlElement = + div( + cls("space-y-6 sm:space-y-5"), + div( + h3(cls("text-lg leading-6 font-medium text-gray-900"), title), + subtitle.map(st => p(cls("mt-1 max-w-2xl text-sm text-gray-500"), st)) + ), + div(cls("mt-6 sm:mt-5 space-y-6 sm:space-y-5"), content) + ) + + def label(labelText: String, forId: Option[String] = None)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + L.label( + cls("block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"), + forId.map(id => L.forId(id)), + labelText, + mods + ) + + def field( + label: Modifier[HtmlElement] + )(content: Modifier[HtmlElement]*): 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, + div(cls("mt-1 sm:mt-0 sm:col-span-2"), content) + ) + + def field( + id: String, + labelText: String, + input: HtmlElement, + help: Option[String] + ): HtmlElement = + field( + label(labelText, Some(id))() + )( + input.amend(idAttr(id)), + help.map(h => p(cls("mt-2 text-sm text-gray-500"), h)) + ) + + def form(mods: Modifier[HtmlElement]*)( + sections: Modifier[HtmlElement]* + )(actions: Modifier[HtmlElement]*): HtmlElement = + L.form( + cls("space-y-8 divide-y divide-gray-200"), + mods, + sections, + div( + cls("pt-5"), + div(cls("flex justify-end"), actions) + ) + ) + + def inlineForm( + mods: Modifier[HtmlElement]* + ): HtmlElement = + L.form(cls("flex space-x-4"), mods) + + def inputField( + id: String, + labelText: String, + placeholderText: Option[String] = None, + inputType: String = "text", + helpText: Option[String] = None + ): HtmlElement = + field( + id, + labelText, + input( + tpe(inputType), + cls( + "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:max-w-xs sm:text-sm sm:leading-6" + ), + placeholderText.map(placeholder(_)) + ), + helpText + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala new file mode 100644 index 0000000..52da07e --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala @@ -0,0 +1,22 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} + +trait HeadingsComponentsModule: + object headings: + def section(title: Node, actions: HtmlElement*): HtmlElement = + div( + cls("border-b border-gray-200 pb-5"), + div( + cls("sm:flex sm:items-center sm:justify-between"), + h3(cls("text-base font-semibold leading-6 text-gray-900"), title), + div( + cls("mt-3 flex sm:ml-4 sm:mt-0"), + actions match + case Nil => emptyMod + case first :: rest => + first :: rest.map(_.amend(cls("ml-3"))) + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala new file mode 100644 index 0000000..c543150 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala @@ -0,0 +1,112 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext +import works.iterative.ui.components.tailwind.CustomAttrs + +trait IconsModule: + object icons: + import svg.* + + object aria: + val hidden = CustomAttrs.svg.ariaHidden + + private def withDefault( + mods: Seq[Modifier[SvgElement]], + default: Modifier[SvgElement] + ): Modifier[SvgElement] = + if mods.isEmpty then default else mods + + def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-8 w-8"), + 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" + ) + ) + ) + + def close(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-2 w-2"), + stroke := "currentColor", + fill := "none", + viewBox := "0 0 8 8", + path( + strokeLineCap := "round", + strokeWidth := "1.5", + d := "M1 1l6 6m0-6L1 7" + ) + ) + + def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls("h-5 w-5 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 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") + ) + ) + + def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls("h-5 w-5 text-gray-400")), + 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") + ) + ) + + def `document-chart-bar-outline`( + mods: Modifier[SvgElement]* + ): SvgElement = + svg( + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + strokeWidth := "1.5", + stroke := "currentColor", + withDefault(mods, cls := "h-6 w-6"), + path( + strokeLineCap := "round", + strokeLineJoin := "round", + d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" + ) + ) + + def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-5 w-5"), + cls := "flex-shrink-0 text-gray-400", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", + clipRule := "evenodd" + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala new file mode 100644 index 0000000..02cbdb6 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala @@ -0,0 +1,11 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait LayoutModule: + object layout: + def card(content: Modifier[HtmlElement]*): HtmlElement = + div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala new file mode 100644 index 0000000..b652da9 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala @@ -0,0 +1,88 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.laminar.nodes.ReactiveHtmlElement +import org.scalajs.dom.html.Paragraph +import com.raquo.laminar.nodes.TextNode +import works.iterative.ui.components.laminar.tailwind.color.ColorKind + +trait ListComponentsModule: + self: BadgeComponentsModule => + + object list: + def label( + text: String, + color: ColorKind + ): HtmlElement = badges.pill(text, color) + + def item( + title: String, + subtitle: Option[String], + right: Modifier[HtmlElement] = emptyMod, + avatar: Option[Modifier[HtmlElement]] = None + ): LI = + li( + cls("group"), + div( + cls( + "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" + ), + avatar.map(a => + div( + cls("flex-shrink-0"), + div( + cls( + "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" + ), + a + ) + ) + ), + div( + cls("flex-1 min-w-0"), + p( + cls("text-sm font-medium text-gray-900"), + title, + span(cls("float-right"), right) + ), + subtitle.map(st => + p( + cls("text-sm text-gray-500 truncate"), + st + ) + ) + ) + ) + ) + + def unordered( + children: Modifier[HtmlElement] + ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = + ul( + cls("relative z-0 divide-y divide-gray-200"), + role("list"), + children + ) + + def listSection( + header: String, + list: HtmlElement + ): Div = + div( + cls("relative"), + div( + cls( + "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" + ), + header + ), + list + ) + + def navigation(sections: Modifier[HtmlElement]): HtmlElement = + navTag( + cls("flex-1 min-h-0 overflow-y-auto"), + sections + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ModalComponentsModule.scala new file mode 100644 index 0000000..6027404 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ModalComponentsModule.scala @@ -0,0 +1,54 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait ModalComponentsModule: + object modal: + def modalDialog( + content: HtmlElement, + closeMod: Modifier[HtmlElement] + ): 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( + cls("fixed inset-0 transition-opacity"), + div(cls("absolute inset-0 bg-gray-500 opacity-75")), + closeMod + ) + ) + + div( + cls("fixed inset-0 z-20 overflow-y-auto"), + div( + cls("text-center sm:block sm:p-0"), + overlay, + browserCenteringModalTrick, + 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-7xl sm:align-middle" + ), + content + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/PageComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/PageComponentsModule.scala new file mode 100644 index 0000000..d5a422e --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/PageComponentsModule.scala @@ -0,0 +1,49 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait PageComponentsModule: + + object page: + def container( + children: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), + children + ) + + def singleColumn( + header: Modifier[HtmlElement] + )(children: Modifier[HtmlElement]*): HtmlElement = + div( + cls("p-8 bg-gray-100 h-full"), + header, + children + ) + + def pageHeader( + title: Modifier[HtmlElement], + right: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] = None + ): HtmlElement = + div( + cls("pb-5 border-b border-gray-200"), + div(cls("float-right"), right), + h1( + cls("text-2xl leading-6 font-medium text-gray-900"), + title + ), + subtitle.map( + p( + cls("text-sm font-medium text-gray-500"), + _ + ) + ) + ) + + def clickable: Modifier[HtmlElement] = + cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TableComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TableComponentsModule.scala new file mode 100644 index 0000000..417ea56 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TableComponentsModule.scala @@ -0,0 +1,86 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L +import com.raquo.laminar.api.L.{*, given} + +trait TableComponentsModule: + + object tables: + + def tableSection( + title: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] = None, + actions: Modifier[HtmlElement]* + )( + table: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls("px-4 sm:px-6 lg:px-8"), + div( + cls("sm:flex sm:items-center"), + div( + cls("sm:flex-auto"), + h1(cls("text-base font-semibold leading-6 text-gray-900"), title), + subtitle.map(st => p(cls("mt-2 text-sm text-gray-700"), st)) + ), + div(cls("mt-4 sm:ml-16 sm:mt-0 sm:flex-none"), actions) + ), + div( + cls("mt-8 flow-root"), + div( + cls("-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"), + div( + cls("inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"), + table + ) + ) + ) + ) + + def simpleTable(header: Modifier[HtmlElement]*)( + body: Modifier[HtmlElement]* + ): HtmlElement = + table( + cls("min-w-full divide-y divide-gray-300"), + thead(header), + tbody(cls("divide-y divide-gray-200"), body) + ) + + def headerRow( + mods: Modifier[HtmlElement]* + )(cells: HtmlElement*): HtmlElement = + tr( + cells.zipWithIndex.map((c, i) => + if i == 0 then c.amend(cls("py-3.5 pl-4 pr-3 sm:pl-0")) + else if i == cells.length - 1 then + c.amend(cls("py-3.5 pr-4 pl-3 sm:pr-0")) + else c.amend(cls("py-3.5 px-3")) + ) + ) + + def dataRow( + mods: Modifier[HtmlElement]* + )(cells: HtmlElement*): HtmlElement = + tr( + mods, + cells.zipWithIndex.map((c, i) => + if i == 0 then c.amend(cls("py-4 pl-4 pr-3 sm:pl-0")) + else if i == cells.length - 1 then + c.amend(cls("py-4 pr-4 pl-3 sm:pr-0")) + else c.amend(cls("py-4 px-3")) + ) + ) + + def headerCell(content: Modifier[HtmlElement]): HtmlElement = + th( + cls("text-left text-sm font-semibold text-gray-900"), + content + ) + + def dataCell(content: Modifier[HtmlElement]): HtmlElement = + td( + cls("whitespace-nowrap text-sm text-gray-500"), + content + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TailwindUICatalogue.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TailwindUICatalogue.scala new file mode 100644 index 0000000..5027b10 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TailwindUICatalogue.scala @@ -0,0 +1,17 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} + +trait TailwindUICatalogueModule + extends BadgeComponentsModule + with ButtonComponentsModule + with HeadingsComponentsModule + with IconsModule + with FormComponentsModule + with LayoutModule + with ListComponentsModule + with ModalComponentsModule + with PageComponentsModule + with TableComponentsModule diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/LabelsOnLeftFormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/LabelsOnLeftFormComponentsModule.scala deleted file mode 100644 index 210bbf8..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/LabelsOnLeftFormComponentsModule.scala +++ /dev/null @@ -1,155 +0,0 @@ -package works.iterative.ui.components.laminar -package tailwindui - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -trait LabelsOnLeftFormComponentsModule(using ctx: ComponentContext) - extends FormComponentsModule: - self: IconsModule => - override val forms = new FormComponents: - - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )(content: Modifier[HtmlElement]*): HtmlElement = - div( - cls("space-y-6 sm:space-y-5"), - div( - h3(cls("text-lg leading-6 font-medium text-gray-900"), title), - subtitle.map(st => p(cls("mt-1 max-w-2xl text-sm text-gray-500"), st)) - ), - div(cls("mt-6 sm:mt-5 space-y-6 sm:space-y-5"), content) - ) - - override def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement = - L.label( - cls("block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"), - forId.map(id => L.forId(id)), - labelText, - mods - ) - - override def field( - label: Modifier[HtmlElement] - )(content: Modifier[HtmlElement]*): 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, - div(cls("mt-1 sm:mt-0 sm:col-span-2"), content) - ) - - override def field( - id: String, - labelText: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement = - field( - label(labelText, Some(id))() - )( - input.amend(idAttr(id)), - help.map(h => p(cls("mt-2 text-sm text-gray-500"), h)) - ) - - override def form(mods: Modifier[HtmlElement]*)( - sections: Modifier[HtmlElement]* - )(actions: Modifier[HtmlElement]*): HtmlElement = - L.form( - cls("space-y-8 divide-y divide-gray-200"), - mods, - sections, - div( - cls("pt-5"), - div(cls("flex justify-end"), actions) - ) - ) - - override def inlineForm( - mods: Modifier[HtmlElement]* - ): HtmlElement = - L.form(cls("flex space-x-4"), mods) - - override def searchField( - id: String, - placeholderText: Option[String] = None - )( - mods: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "flex-1 min-w-0", - label(ctx.messages("forms.search.label"), Some(id))(cls("sr-only")), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - icons.`search-solid`() - ), - input( - tpe := "search", - nameAttr := "search", - idAttr := id, - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholderText - .orElse( - ctx.messages - .opt( - s"forms.search.${id}.placeholder", - s"form.search.placeholder" - ) - ) - .map(placeholder(_)), - mods - ) - ) - ) - - override def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement = - field( - id, - labelText, - input( - tpe(inputType), - cls( - "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:max-w-xs sm:text-sm sm:leading-6" - ), - placeholderText.map(placeholder(_)) - ), - helpText - ) - - override def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement = - div( - labelText, - input( - idAttr(id), - nameAttr(id), - autoComplete("date"), - tpe("date"), - cls( - "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ), - placeholderText.map(placeholder(_)), - mods(localDateSelect) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/TailwindUIButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/TailwindUIButtonComponentsModule.scala deleted file mode 100644 index 4c44157..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/TailwindUIButtonComponentsModule.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar -package tailwindui - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait TailwindUIButtonComponentsModule(using ctx: ComponentContext) - extends ButtonComponentsModule: - - override val buttons = new ButtonComponents: - - private inline def srHelp(id: String): Modifier[HtmlElement] = - ctx.messages - .opt(s"form.button.${id}.screenReaderHelp") - .map(sr => span(cls := "sr-only", sr)) - - override def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )(mods: Modifier[HtmlElement]*): HtmlElement = - button( - tpe(buttonType), - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - icon, - text, - mods - ) - - override def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )(mods: Modifier[HtmlElement]*): HtmlElement = - button( - tpe(buttonType), - cls( - "ml-2 bg-white inline-flex justify-center 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" - ), - icon, - text, - mods - ) - - override def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement = - button( - tpe := "button", - cls := "ml-1 inline-flex h-4 w-4 flex-shrink-0 rounded-full p-1 text-gray-400 hover:bg-gray-200 hover:text-gray-500", - srHelp(id), - icon - ) - - override def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement = - button( - tpe := "button", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - icon, - srHelp(id) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/tailwind/laminar/LaminarExtensions.scala b/ui/js/src/main/scala/works/iterative/ui/components/tailwind/laminar/LaminarExtensions.scala index 4c5e68b..4c87800 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/tailwind/laminar/LaminarExtensions.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/tailwind/laminar/LaminarExtensions.scala @@ -2,23 +2,9 @@ package laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.Color import works.iterative.core.UserMessage object LaminarExtensions: - given colorToCSS: Conversion[Color, Modifier[HtmlElement]] with - def apply(c: Color) = cls(c.toCSS) - - given colorToSVGCSS: Conversion[Color, Modifier[SvgElement]] with - def apply(c: Color) = svg.cls(c.toCSS) - - given colorSignalToCSS: Conversion[Signal[Color], Modifier[HtmlElement]] with - def apply(c: Signal[Color]) = cls <-- c.map(_.toCSS) - - given colorSignalToSVGCSS: Conversion[Signal[Color], Modifier[SvgElement]] - with - def apply(c: Signal[Color]) = svg.cls <-- c.map(_.toCSS) - inline given userMessageToModifier(using ctx: ComponentContext ): Conversion[UserMessage, Modifier[HtmlElement]] with diff --git a/ui/js/src/main/scala/works/iterative/ui/scenarios/Scenario.scala b/ui/js/src/main/scala/works/iterative/ui/scenarios/Scenario.scala index 99a212a..60de9f0 100644 --- a/ui/js/src/main/scala/works/iterative/ui/scenarios/Scenario.scala +++ b/ui/js/src/main/scala/works/iterative/ui/scenarios/Scenario.scala @@ -13,8 +13,6 @@ import works.iterative.core.MessageCatalogue import works.iterative.ui.components.tailwind.ComponentContext import works.iterative.ui.components.tailwind.StyleGuide -import works.iterative.ui.model.color.Color -import works.iterative.ui.model.color.ColorWeight object Scenario: type Id = String diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala index a2c637c..bfa063a 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala @@ -1,13 +1,11 @@ package works.iterative.ui.components.laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorDef import com.raquo.laminar.tags.HtmlTag case class Bin[Source, +Value]( label: String, description: Option[String | HtmlElement], - color: ColorDef, valueOf: Source => Value ): def map[NewValue]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala deleted file mode 100644 index 3aac0bf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala +++ /dev/null @@ -1,34 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ButtonComponentsModule: - def buttons: ButtonComponents - - trait ButtonComponents: - def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala deleted file mode 100644 index 646e73d..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala +++ /dev/null @@ -1,191 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.color.ColorDef -import com.raquo.laminar.tags.HtmlTag -import org.scalajs.dom.html.UList -import com.raquo.laminar.nodes.ReactiveHtmlElement -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind - -trait DashboardComponentsModule: - def dashboard: DashboardComponents - - // Only methods that create HtmlElements - trait DashboardComponents(using ComponentContext): - def section(label: String, children: Modifier[HtmlElement]): Div - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI - def cardInitials( - initials: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div - def count(label: String, value: Int, color: ColorDef): Span - def counts(children: HtmlElement*): Span - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - -trait DefaultDashboardComponentsModule(using ComponentContext) - extends DashboardComponentsModule: - override val dashboard: DashboardComponents = new DashboardComponents: - def section(label: String, children: Modifier[HtmlElement]): Div = - div( - cls("mt-3"), - h2( - cls("text-gray-500 text-xs font-medium uppercase tracking-wide"), - label - ), - children - ) - - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] = - ul( - cls( - "mt-3 grid gap-5 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" - ), - children - ) - - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI = - li( - div( - cls("col-span-1 flex shadow-sm rounded-md"), - initials, - div( - cls( - "flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate" - ), - div( - cls("flex-1 px-4 py-2 text-sm truncate"), - div( - cls("text-gray-900 font-medium hover:text-gray-600 truncate"), - label - ), - p(cls("text-gray-500"), children) - ) - ) - ) - ) - - def cardInitials( - id: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div = - div( - cls( - "flex-shrink-0 flex flex-col items-center justify-center w-16 text-white text-sm font-medium rounded-l-md" - ), - color.map(_(600).bg), - div(id), - children - ) - - def count(label: String, value: Int, color: ColorDef): Span = - span(color.text, title(label), s"${value}") - - def counts(children: HtmlElement*): Span = - span(interleave(children, span(" / "))) - - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div = - div( - cls("flex flex-col mt-2"), - div( - cls( - "align-middle min-w-full shadow sm:rounded-lg" - ), - L.table( - cls("min-w-full border-separate border-spacing-0"), - styleAttr("border-spacing: 0"), // Tailwind somehow doesn't work - thead( - tr( - cls("border-t border-gray-200"), - headers.map(h => - th( - cls( - "sticky top-0 z-10 px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - h - ) - ) - ) - ), - tbody(cls("bg-white divide-y divide-gray-100"), sections) - ) - ) - ) - - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - td( - cls("bg-gray-50 border-gray-200 border-t border-b"), - cls( - "sticky top-14 z-10 bg-gray-50 px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - colSpan(cs), - label - ), - mods - ) - - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - mods, - td( - cls( - "px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900 truncate" - ), - label - ), - children.map(e => - td( - cls( - "whitespace-nowrap px-2 py-2 text-sm text-gray-500" - ), - e - ) - ) - ) - - private def interleave( - a: Seq[HtmlElement], - separator: => HtmlElement - ): Seq[HtmlElement] = - if a.size < 2 then a - else a.init.flatMap(n => Seq(n, separator)) :+ a.last diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala deleted file mode 100644 index a45beaf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.FileRef - -trait DetailComponentsModule: - - def details: DetailComponents - - trait DetailComponents: - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement - - def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement - - def files(fs: List[FileRef]): HtmlElement - - def file(f: FileRef): HtmlElement - -trait DefaultDetailComponentsModule(using ctx: ComponentContext) - extends DetailComponentsModule: - self: IconsModule => - - override val details: DetailComponents = new DetailComponents: - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement = - div( - div( - cls := "px-4 sm:px-0", - h3( - cls := "text-base font-semibold leading-7 text-gray-900", - title - ), - subtitle.map(st => - p( - cls := "mt-1 max-w-2xl text-sm leading-6 text-gray-500", - st - ) - ) - // TODO: actions - ), - div( - cls := "mt-6 border-t border-gray-100", - dl( - cls := "divide-y divide-gray-100", - fields - ) - ) - ) - - override def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0", - dt( - cls := "text-sm font-medium leading-6 text-gray-900", - title - ), - dd( - cls := "mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0", - content - ) - ) - - override def files(fs: List[FileRef]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-100 rounded-md border border-gray-200", - fs.map(file) - ) - - override def file(f: FileRef): HtmlElement = - li( - cls := "flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6", - div( - cls := "flex w-0 flex-1 items-center", - icons.`paper-clip-solid`(), - div( - cls := "ml-4 flex min-w-0 flex-1 gap-2", - span( - cls := "truncate font-medium", - f.name - ), - f.size.map(size => span(cls := "flex-shrink-0 text-gray-400", size)) - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - "Uložit" - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala new file mode 100644 index 0000000..f6c583f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait FormComponents(using ctx: ComponentContext) extends LocalDateSelectModule: + def searchIcon: SvgElement + + def renderLocalDateSelect( + id: String, + labelText: Option[String], + placeholderText: Option[String], + mods: LocalDateSelect => Modifier[HtmlElement] + ): HtmlElement = + div( + labelText, + input( + idAttr(id), + nameAttr(id), + autoComplete("date"), + tpe("date"), + cls( + "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ), + placeholderText.map(placeholder(_)), + mods(localDateSelect) + ) + ) + + def searchField( + id: String, + placeholderText: Option[String] = None + )( + mods: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls := "flex-1 min-w-0", + label(ctx.messages("forms.search.label"), forId(id), cls("sr-only")), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + searchIcon + ), + input( + tpe := "search", + nameAttr := "search", + idAttr := id, + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholderText + .orElse( + ctx.messages + .opt( + s"forms.search.${id}.placeholder", + s"form.search.placeholder" + ) + ) + .map(placeholder(_)), + mods + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala deleted file mode 100644 index 7714490..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala +++ /dev/null @@ -1,82 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -/** Form components - * - * A form is a collection of sections, each of which contains a collection of - * fields. - * - * Form -> * Section -> * Field - * -> * Action - * - * Each Field has an `id`, `label` and `input` element. The `id` is used to - * link the label to the input. - * - * The `input` element can be a simple text input, or a more complex component - * such as a date picker. The caller is expected to make the input work. - */ -trait FormComponentsModule extends LocalDateSelectModule: - def forms: FormComponents - - trait FormComponents: - - /** Layout the sections and actions of a form */ - def form(mods: Modifier[HtmlElement]*)(sections: Modifier[HtmlElement]*)( - actions: Modifier[HtmlElement]* - ): HtmlElement - - /** Section layout, with a title, optional subtitle and any content. */ - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label and any content */ - def field(label: Modifier[HtmlElement])( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label, input and optional help text */ - def field( - id: String, - label: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement - - def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout an inline form element */ - def inlineForm(mods: Modifier[HtmlElement]*): HtmlElement - - /** An input field with a search icon */ - def searchField(id: String, placeholderText: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement - - /** LocalDate input */ - def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala deleted file mode 100644 index 8acbe7f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala +++ /dev/null @@ -1,28 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.Tag - -trait GenericComponentsModule: - - def generic: GenericComponents - - trait GenericComponents: - def tag(name: String, color: ColorKind): HtmlElement - def tag(t: Tag): HtmlElement = tag(t.value, t.color) - -trait DefaultGenericComponentsModule(using ComponentContext) - extends GenericComponentsModule: - override val generic: GenericComponents = new GenericComponents: - override def tag(name: String, color: ColorKind): HtmlElement = - p( - cls( - "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" - ), - color(800).text, - color(100).bg, - name - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala deleted file mode 100644 index 8cfc1d9..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala +++ /dev/null @@ -1,123 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.CustomAttrs - -trait IconsModule: - def icons: Icons - - trait Icons: - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement - def close(mods: Modifier[SvgElement]*): SvgElement - def `search-solid`(mods: Modifier[SvgElement]*): SvgElement - def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement - def `document-chart-bar-outline`(mods: Modifier[SvgElement]*): SvgElement - def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement - -trait DefaultIconsModule(using ComponentContext) extends IconsModule: - override val icons: Icons = new Icons: - import svg.* - - object aria: - val hidden = CustomAttrs.svg.ariaHidden - - private def withDefault( - mods: Seq[Modifier[SvgElement]], - default: Modifier[SvgElement] - ): Modifier[SvgElement] = - if mods.isEmpty then default else mods - - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-8 w-8"), - 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" - ) - ) - ) - - override def close(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-2 w-2"), - stroke := "currentColor", - fill := "none", - viewBox := "0 0 8 8", - path( - strokeLineCap := "round", - strokeWidth := "1.5", - d := "M1 1l6 6m0-6L1 7" - ) - ) - - override def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 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 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") - ) - ) - - override def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 text-gray-400")), - 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") - ) - ) - - override def `document-chart-bar-outline`( - mods: Modifier[SvgElement]* - ): SvgElement = - svg( - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - strokeWidth := "1.5", - stroke := "currentColor", - withDefault(mods, cls := "h-6 w-6"), - path( - strokeLineCap := "round", - strokeLineJoin := "round", - d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" - ) - ) - - override def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-5 w-5"), - cls := "flex-shrink-0 text-gray-400", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", - clipRule := "evenodd" - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala deleted file mode 100644 index 95ab654..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait LayoutModule: - def layout: LayoutComponents - - trait LayoutComponents: - def card(content: Modifier[HtmlElement]*): HtmlElement - -trait DefaultLayoutModule(using ctx: ComponentContext) extends LayoutModule: - override val layout: LayoutComponents = new LayoutComponents: - override def card(content: Modifier[HtmlElement]*): HtmlElement = - div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala deleted file mode 100644 index 5946974..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom.html.Paragraph -import com.raquo.laminar.nodes.TextNode - -trait ListComponentsModule: - - def list: ListComponents - - trait ListComponents(using ComponentContext): - def label(text: String, color: ColorKind): HtmlElement - def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI - def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] - def listSection(header: String, list: HtmlElement): Div - def navigation(sections: Modifier[HtmlElement]): HtmlElement - -trait DefaultListComponentsModule(using ComponentContext) - extends ListComponentsModule: - self: GenericComponentsModule => - - override val list: ListComponents = new ListComponents: - override def label( - text: String, - color: ColorKind - ): HtmlElement = generic.tag(text, color) - - override def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI = - li( - cls("group"), - div( - cls( - "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" - ), - avatar.map(a => - div( - cls("flex-shrink-0"), - div( - cls( - "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" - ), - a - ) - ) - ), - div( - cls("flex-1 min-w-0"), - p( - cls("text-sm font-medium text-gray-900"), - title, - span(cls("float-right"), right) - ), - subtitle.map(st => - p( - cls("text-sm text-gray-500 truncate"), - st - ) - ) - ) - ) - ) - - override def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = - ul( - cls("relative z-0 divide-y divide-gray-200"), - role("list"), - children - ) - - override def listSection( - header: String, - list: HtmlElement - ): Div = - div( - cls("relative"), - div( - cls( - "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" - ), - header - ), - list - ) - - override def navigation(sections: Modifier[HtmlElement]): HtmlElement = - navTag( - cls("flex-1 min-h-0 overflow-y-auto"), - sections - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala deleted file mode 100644 index 5b5490f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ModalComponentsModule: - - def modal: ModalComponents - - trait ModalComponents: - def modalDialog( - content: HtmlElement, - onClose: Modifier[HtmlElement] - ): HtmlElement - -trait DefaultModalComponentsModule(using ctx: ComponentContext) - extends ModalComponentsModule: - - override val modal: ModalComponents = new ModalComponents: - - override def modalDialog( - content: HtmlElement, - closeMod: Modifier[HtmlElement] - ): 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( - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")), - closeMod - ) - ) - - div( - cls("fixed inset-0 z-20 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - 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-7xl sm:align-middle" - ), - content - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala deleted file mode 100644 index b713d8a..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala +++ /dev/null @@ -1,71 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait PageComponentsModule: - - val page: PageComponents - - trait PageComponents: - def container( - children: Modifier[HtmlElement]* - ): HtmlElement - - def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement - - def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement] = emptyMod, - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement - - /** Visage for clickable text, like links - */ - def clickable: Modifier[HtmlElement] - -trait DefaultPageComponentsModule(using ComponentContext) - extends PageComponentsModule: - - override val page: PageComponents = new PageComponents: - override def container( - children: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), - children - ) - - override def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement = - div( - cls("p-8 bg-gray-100 h-full"), - header, - children - ) - - override def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement = - div( - cls("pb-5 border-b border-gray-200"), - div(cls("float-right"), right), - h1( - cls("text-2xl leading-6 font-medium text-gray-900"), - title - ), - subtitle.map( - p( - cls("text-sm font-medium text-gray-500"), - _ - ) - ) - ) - - override def clickable: Modifier[HtmlElement] = - cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala deleted file mode 100644 index f70ee87..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala +++ /dev/null @@ -1,108 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} - -trait TableComponentsModule: - def tables: TableComponents - - trait TableComponents: - def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )(table: Modifier[HtmlElement]*): HtmlElement - - def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement - - def headerRow(mods: Modifier[HtmlElement]*)( - cells: HtmlElement* - ): HtmlElement - - def dataRow(mods: Modifier[HtmlElement]*)(cells: HtmlElement*): HtmlElement - - def headerCell(content: Modifier[HtmlElement]): HtmlElement - - def dataCell(content: Modifier[HtmlElement]): HtmlElement - -trait DefaultTableComponentsModule extends TableComponentsModule: - - override lazy val tables: TableComponents = new TableComponents: - - override def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )( - table: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("px-4 sm:px-6 lg:px-8"), - div( - cls("sm:flex sm:items-center"), - div( - cls("sm:flex-auto"), - h1(cls("text-base font-semibold leading-6 text-gray-900"), title), - subtitle.map(st => p(cls("mt-2 text-sm text-gray-700"), st)) - ), - div(cls("mt-4 sm:ml-16 sm:mt-0 sm:flex-none"), actions) - ), - div( - cls("mt-8 flow-root"), - div( - cls("-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"), - div( - cls("inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"), - table - ) - ) - ) - ) - - override def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement = - table( - cls("min-w-full divide-y divide-gray-300"), - thead(header), - tbody(cls("divide-y divide-gray-200"), body) - ) - - override def headerRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-3.5 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-3.5 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-3.5 px-3")) - ) - ) - - override def dataRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - mods, - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-4 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-4 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-4 px-3")) - ) - ) - - override def headerCell(content: Modifier[HtmlElement]): HtmlElement = - th( - cls("text-left text-sm font-semibold text-gray-900"), - content - ) - - override def dataCell(content: Modifier[HtmlElement]): HtmlElement = - td( - cls("whitespace-nowrap text-sm text-gray-500"), - content - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala index b937d45..12d01ae 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala @@ -3,7 +3,7 @@ import com.raquo.laminar.api.L.{*, given} import works.iterative.ui.components.laminar.HtmlTabular import works.iterative.ui.model.tables.Tabular -import works.iterative.ui.components.laminar.TableComponentsModule +import works.iterative.ui.components.laminar.tailwind.ui.TableComponentsModule import works.iterative.core.UserMessage import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given import works.iterative.ui.components.tailwind.ComponentContext diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala new file mode 100644 index 0000000..87f3e8d --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala @@ -0,0 +1,54 @@ +package works.iterative.ui.components.laminar.tailwind.color + +import com.raquo.laminar.api.L.{*, given} + +/** Complete color definition that can be rendered to CSS. + * + * Includes the area, kind and weight of the color. + */ +case class Color(area: ColorArea, color: ColorDef): + def toCSS: String = s"${area.name}-${color.toCSS}" + +object Color: + import ColorDef.given + + given colorToCSS: Conversion[Color, HtmlMod] with + def apply(c: Color) = cls(c.toCSS) + + given colorToSVGCSS: Conversion[Color, SvgMod] with + def apply(c: Color) = svg.cls(c.toCSS) + + given colorSignalToCSS: Conversion[Signal[Color], HtmlMod] with + def apply(c: Signal[Color]) = cls <-- c.map(_.toCSS) + + given colorSignalToSVGCSS: Conversion[Signal[Color], SvgMod] with + def apply(c: Signal[Color]) = svg.cls <-- c.map(_.toCSS) + + def current = ColorDef(ColorKind.current) + def inherit = ColorDef(ColorKind.inherit) + def transp = ColorDef(ColorKind.transp) + def auto = ColorDef(ColorKind.auto) + def black = ColorDef(ColorKind.black) + def white = ColorDef(ColorKind.white) + def slate(weight: ColorWeight) = ColorDef(ColorKind.slate, weight) + def gray(weight: ColorWeight) = ColorDef(ColorKind.gray, weight) + def zinc(weight: ColorWeight) = ColorDef(ColorKind.zinc, weight) + def neutral(weight: ColorWeight) = ColorDef(ColorKind.neutral, weight) + def stone(weight: ColorWeight) = ColorDef(ColorKind.stone, weight) + def red(weight: ColorWeight) = ColorDef(ColorKind.red, weight) + def orange(weight: ColorWeight) = ColorDef(ColorKind.orange, weight) + def amber(weight: ColorWeight) = ColorDef(ColorKind.amber, weight) + def yellow(weight: ColorWeight) = ColorDef(ColorKind.yellow, weight) + def lime(weight: ColorWeight) = ColorDef(ColorKind.lime, weight) + def green(weight: ColorWeight) = ColorDef(ColorKind.green, weight) + def emerald(weight: ColorWeight) = ColorDef(ColorKind.emerald, weight) + def teal(weight: ColorWeight) = ColorDef(ColorKind.teal, weight) + def cyan(weight: ColorWeight) = ColorDef(ColorKind.cyan, weight) + def sky(weight: ColorWeight) = ColorDef(ColorKind.sky, weight) + def blue(weight: ColorWeight) = ColorDef(ColorKind.blue, weight) + def indigo(weight: ColorWeight) = ColorDef(ColorKind.indigo, weight) + def violet(weight: ColorWeight) = ColorDef(ColorKind.violet, weight) + def purple(weight: ColorWeight) = ColorDef(ColorKind.purple, weight) + def fuchsia(weight: ColorWeight) = ColorDef(ColorKind.fuchsia, weight) + def pink(weight: ColorWeight) = ColorDef(ColorKind.pink, weight) + def rose(weight: ColorWeight) = ColorDef(ColorKind.rose, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala new file mode 100644 index 0000000..4130840 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala @@ -0,0 +1,16 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines the area the color should apply to, eg. background, text, border, + * etc. + */ +enum ColorArea(val name: String): + case bg extends ColorArea("bg") + case text extends ColorArea("text") + case decoration extends ColorArea("decoration") + case border extends ColorArea("border") + case outline extends ColorArea("outline") + case divide extends ColorArea("divide") + case ring extends ColorArea("ring") + case ringOffset extends ColorArea("ring-offset") + case shadow extends ColorArea("shadow") + case accent extends ColorArea("accent") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala new file mode 100644 index 0000000..fab9404 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala @@ -0,0 +1,43 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** A combination of ColorKind and ColorWeight, if applicable. + * + * By applying area we get the full Color definition. + */ +sealed trait ColorDef: + def toCSS: String + + def bg = Color(ColorArea.bg, this) + def text = Color(ColorArea.text, this) + def decoration = Color(ColorArea.decoration, this) + def border = Color(ColorArea.border, this) + def outline = Color(ColorArea.outline, this) + def divide = Color(ColorArea.divide, this) + def ring = Color(ColorArea.ring, this) + def ringOffset = Color(ColorArea.ringOffset, this) + def shadow = Color(ColorArea.shadow, this) + def accent = Color(ColorArea.accent, this) + +// TODO: create a macro that will output the tailwind class as string directly during compilation, where possible. +object ColorDef: + case class WeightedColorDef( + kind: ColorKind, + weight: ColorWeight + ) extends ColorDef: + override def toCSS: String = s"${kind.name}-${weight.value}" + + case class UnweightedColorDef( + kind: ColorKind + ) extends ColorDef: + override def toCSS: String = kind.name + + // TODO: check that the kind is valid unweighted kind + // that means current, inherit, auto, transparent, black, white + // tried using implicit evidence, but the type inference for enumerations + // tends to generalize to the enum, instead of the real type + def apply[T <: ColorKind](kind: T)(using + ev: T <:< ColorKind.Unweighted + ): ColorDef = + UnweightedColorDef(kind) + def apply(kind: ColorKind, weight: ColorWeight): ColorDef = + WeightedColorDef(kind, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala new file mode 100644 index 0000000..865166b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala @@ -0,0 +1,47 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines what color should be used, without specifying the area or weight. + */ +sealed abstract class ColorKind private (val name: String): + def apply(weight: ColorWeight): ColorDef = + ColorDef.WeightedColorDef(this, weight) + +object ColorKind: + trait Unweighted: + self: ColorKind => + override def apply(weight: ColorWeight): ColorDef = + ColorDef.UnweightedColorDef(self) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case object current extends ColorKind("current") with Unweighted + case object inherit extends ColorKind("inherit") with Unweighted + // Not present in for all methods + case object transp extends ColorKind("transparent") with Unweighted + // Seen in accent, not preset otherwise + case object auto extends ColorKind("auto") with Unweighted + // Black and white do not have weight + case object black extends ColorKind("black") with Unweighted + case object white extends ColorKind("white") with Unweighted + case object slate extends ColorKind("slate") + case object gray extends ColorKind("gray") + case object zinc extends ColorKind("zinc") + case object neutral extends ColorKind("neutral") + case object stone extends ColorKind("stone") + case object red extends ColorKind("red") + case object orange extends ColorKind("orange") + case object amber extends ColorKind("amber") + case object yellow extends ColorKind("yellow") + case object lime extends ColorKind("lime") + case object green extends ColorKind("green") + case object emerald extends ColorKind("emerald") + case object teal extends ColorKind("teal") + case object cyan extends ColorKind("cyan") + case object sky extends ColorKind("sky") + case object blue extends ColorKind("blue") + case object indigo extends ColorKind("indigo") + case object violet extends ColorKind("violet") + case object purple extends ColorKind("purple") + case object fuchsia extends ColorKind("fuchsia") + case object pink extends ColorKind("pink") + case object rose extends ColorKind("rose") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala new file mode 100644 index 0000000..6f1a4cc --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala @@ -0,0 +1,33 @@ +package works.iterative.ui.components.laminar.tailwind.color + +opaque type ColorWeight = String + +extension (c: ColorWeight) def value: String = c + +/** Defines weight of a color, eg. 50, 100, 200, etc. + * + * Tailwind-like. + */ +object ColorWeight: + inline given int50: Conversion[50, ColorWeight] with + inline def apply(i: 50) = "50" + inline given int100: Conversion[100, ColorWeight] with + inline def apply(i: 100) = "100" + inline given int200: Conversion[200, ColorWeight] with + inline def apply(i: 200) = "200" + inline given int300: Conversion[300, ColorWeight] with + inline def apply(i: 300) = "300" + inline given int400: Conversion[400, ColorWeight] with + inline def apply(i: 400) = "400" + inline given int500: Conversion[500, ColorWeight] with + inline def apply(i: 500) = "500" + inline given int600: Conversion[600, ColorWeight] with + inline def apply(i: 600) = "600" + inline given int700: Conversion[700, ColorWeight] with + inline def apply(i: 700) = "700" + inline given int800: Conversion[800, ColorWeight] with + inline def apply(i: 800) = "800" + inline given int900: Conversion[900, ColorWeight] with + inline def apply(i: 900) = "900" + inline given int950: Conversion[950, ColorWeight] with + inline def apply(i: 950) = "950" diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala new file mode 100644 index 0000000..21e21f0 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala @@ -0,0 +1,12 @@ +package works.iterative.ui.components.laminar.tailwind + +/** We need a generic Color model that can be used both on server and on client. + * + * We have adopted the Tailwind model for now. There is nothing inherently + * Tailwind-specific in the implementation, but all the values are taken from + * their palette and the area model is very HTML biased. + * + * Still, I think that it is a good starting point for exploration and will + * satisfy our current needs. + */ +package object color {} diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" new file mode 100644 index 0000000..5c4e218 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" @@ -0,0 +1,21 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.model.color.ColorKind +import works.iterative.ui.components.tailwind.ComponentContext +import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given +import works.iterative.ui.model.Tag + +trait TailwindUIBadgesComponentsModule(using ComponentContext): + + object badges: + override def tag(name: String, color: ColorKind): HtmlElement = + p( + cls( + "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" + ), + color(800).text, + color(100).bg, + name + ) diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" new file mode 100644 index 0000000..0c9ca25 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" @@ -0,0 +1,24 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} + +trait TailwindUIHeadingsComponentsModule: + val headings: HeadingsComponents = new HeadingsComponents + + class HeadingsComponents: + def section(title: Node, actions: HtmlElement*): HtmlElement = + div( + cls("border-b border-gray-200 pb-5"), + div( + cls("sm:flex sm:items-center sm:justify-between"), + h3(cls("text-base font-semibold leading-6 text-gray-900"), title), + div( + cls("mt-3 flex sm:ml-4 sm:mt-0"), + actions match + case Nil => emptyMod + case first :: rest => + first :: rest.map(_.amend(cls("ml-3"))) + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala new file mode 100644 index 0000000..16ca01b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala @@ -0,0 +1,19 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.laminar.tailwind.color.ColorKind + +trait BadgeComponentsModule: + + object badges: + def pill(name: String, color: ColorKind): HtmlElement = + p( + cls( + "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" + ), + color(800).text, + color(100).bg, + name + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala new file mode 100644 index 0000000..ebb2968 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} + +trait ButtonComponentsModule: + + object buttons: + + private inline def srHelp(text: String): Modifier[HtmlElement] = + span(cls := "sr-only", text) + + def primaryButton( + id: String, + text: Modifier[HtmlElement], + icon: Option[SvgElement] = None, + buttonType: String = "submit" + )(mods: Modifier[HtmlElement]*): HtmlElement = + button( + tpe(buttonType), + cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + icon, + text, + mods + ) + + def secondaryButton( + id: String, + text: Modifier[HtmlElement], + icon: Option[SvgElement] = None, + buttonType: String = "button" + )(mods: Modifier[HtmlElement]*): HtmlElement = + button( + tpe(buttonType), + cls( + "ml-2 bg-white inline-flex justify-center 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" + ), + icon, + text, + mods + ) + + def inlineButton(id: String, icon: SvgElement)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + button( + tpe := "button", + cls := "ml-1 inline-flex h-4 w-4 flex-shrink-0 rounded-full p-1 text-gray-400 hover:bg-gray-200 hover:text-gray-500", + srHelp(id), + icon + ) + + def iconButton(id: String, icon: SvgElement, srText: Option[String] = None)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + button( + tpe := "button", + cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", + icon, + srText.map(srHelp(_)) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala new file mode 100644 index 0000000..eb6dd5f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala @@ -0,0 +1,101 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import org.scalajs.dom.html +import com.raquo.laminar.modifiers.KeyUpdater + +trait FormComponentsModule: + self: IconsModule => + + object forms: + + def section( + title: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] + )(content: Modifier[HtmlElement]*): HtmlElement = + div( + cls("space-y-6 sm:space-y-5"), + div( + h3(cls("text-lg leading-6 font-medium text-gray-900"), title), + subtitle.map(st => p(cls("mt-1 max-w-2xl text-sm text-gray-500"), st)) + ), + div(cls("mt-6 sm:mt-5 space-y-6 sm:space-y-5"), content) + ) + + def label(labelText: String, forId: Option[String] = None)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + L.label( + cls("block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"), + forId.map(id => L.forId(id)), + labelText, + mods + ) + + def field( + label: Modifier[HtmlElement] + )(content: Modifier[HtmlElement]*): 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, + div(cls("mt-1 sm:mt-0 sm:col-span-2"), content) + ) + + def field( + id: String, + labelText: String, + input: HtmlElement, + help: Option[String] + ): HtmlElement = + field( + label(labelText, Some(id))() + )( + input.amend(idAttr(id)), + help.map(h => p(cls("mt-2 text-sm text-gray-500"), h)) + ) + + def form(mods: Modifier[HtmlElement]*)( + sections: Modifier[HtmlElement]* + )(actions: Modifier[HtmlElement]*): HtmlElement = + L.form( + cls("space-y-8 divide-y divide-gray-200"), + mods, + sections, + div( + cls("pt-5"), + div(cls("flex justify-end"), actions) + ) + ) + + def inlineForm( + mods: Modifier[HtmlElement]* + ): HtmlElement = + L.form(cls("flex space-x-4"), mods) + + def inputField( + id: String, + labelText: String, + placeholderText: Option[String] = None, + inputType: String = "text", + helpText: Option[String] = None + ): HtmlElement = + field( + id, + labelText, + input( + tpe(inputType), + cls( + "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:max-w-xs sm:text-sm sm:leading-6" + ), + placeholderText.map(placeholder(_)) + ), + helpText + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala new file mode 100644 index 0000000..52da07e --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala @@ -0,0 +1,22 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} + +trait HeadingsComponentsModule: + object headings: + def section(title: Node, actions: HtmlElement*): HtmlElement = + div( + cls("border-b border-gray-200 pb-5"), + div( + cls("sm:flex sm:items-center sm:justify-between"), + h3(cls("text-base font-semibold leading-6 text-gray-900"), title), + div( + cls("mt-3 flex sm:ml-4 sm:mt-0"), + actions match + case Nil => emptyMod + case first :: rest => + first :: rest.map(_.amend(cls("ml-3"))) + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala new file mode 100644 index 0000000..c543150 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala @@ -0,0 +1,112 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext +import works.iterative.ui.components.tailwind.CustomAttrs + +trait IconsModule: + object icons: + import svg.* + + object aria: + val hidden = CustomAttrs.svg.ariaHidden + + private def withDefault( + mods: Seq[Modifier[SvgElement]], + default: Modifier[SvgElement] + ): Modifier[SvgElement] = + if mods.isEmpty then default else mods + + def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-8 w-8"), + 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" + ) + ) + ) + + def close(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-2 w-2"), + stroke := "currentColor", + fill := "none", + viewBox := "0 0 8 8", + path( + strokeLineCap := "round", + strokeWidth := "1.5", + d := "M1 1l6 6m0-6L1 7" + ) + ) + + def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls("h-5 w-5 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 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") + ) + ) + + def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls("h-5 w-5 text-gray-400")), + 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") + ) + ) + + def `document-chart-bar-outline`( + mods: Modifier[SvgElement]* + ): SvgElement = + svg( + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + strokeWidth := "1.5", + stroke := "currentColor", + withDefault(mods, cls := "h-6 w-6"), + path( + strokeLineCap := "round", + strokeLineJoin := "round", + d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" + ) + ) + + def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-5 w-5"), + cls := "flex-shrink-0 text-gray-400", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", + clipRule := "evenodd" + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala new file mode 100644 index 0000000..02cbdb6 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala @@ -0,0 +1,11 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait LayoutModule: + object layout: + def card(content: Modifier[HtmlElement]*): HtmlElement = + div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala new file mode 100644 index 0000000..b652da9 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala @@ -0,0 +1,88 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.laminar.nodes.ReactiveHtmlElement +import org.scalajs.dom.html.Paragraph +import com.raquo.laminar.nodes.TextNode +import works.iterative.ui.components.laminar.tailwind.color.ColorKind + +trait ListComponentsModule: + self: BadgeComponentsModule => + + object list: + def label( + text: String, + color: ColorKind + ): HtmlElement = badges.pill(text, color) + + def item( + title: String, + subtitle: Option[String], + right: Modifier[HtmlElement] = emptyMod, + avatar: Option[Modifier[HtmlElement]] = None + ): LI = + li( + cls("group"), + div( + cls( + "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" + ), + avatar.map(a => + div( + cls("flex-shrink-0"), + div( + cls( + "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" + ), + a + ) + ) + ), + div( + cls("flex-1 min-w-0"), + p( + cls("text-sm font-medium text-gray-900"), + title, + span(cls("float-right"), right) + ), + subtitle.map(st => + p( + cls("text-sm text-gray-500 truncate"), + st + ) + ) + ) + ) + ) + + def unordered( + children: Modifier[HtmlElement] + ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = + ul( + cls("relative z-0 divide-y divide-gray-200"), + role("list"), + children + ) + + def listSection( + header: String, + list: HtmlElement + ): Div = + div( + cls("relative"), + div( + cls( + "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" + ), + header + ), + list + ) + + def navigation(sections: Modifier[HtmlElement]): HtmlElement = + navTag( + cls("flex-1 min-h-0 overflow-y-auto"), + sections + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ModalComponentsModule.scala new file mode 100644 index 0000000..6027404 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ModalComponentsModule.scala @@ -0,0 +1,54 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait ModalComponentsModule: + object modal: + def modalDialog( + content: HtmlElement, + closeMod: Modifier[HtmlElement] + ): 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( + cls("fixed inset-0 transition-opacity"), + div(cls("absolute inset-0 bg-gray-500 opacity-75")), + closeMod + ) + ) + + div( + cls("fixed inset-0 z-20 overflow-y-auto"), + div( + cls("text-center sm:block sm:p-0"), + overlay, + browserCenteringModalTrick, + 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-7xl sm:align-middle" + ), + content + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/PageComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/PageComponentsModule.scala new file mode 100644 index 0000000..d5a422e --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/PageComponentsModule.scala @@ -0,0 +1,49 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait PageComponentsModule: + + object page: + def container( + children: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), + children + ) + + def singleColumn( + header: Modifier[HtmlElement] + )(children: Modifier[HtmlElement]*): HtmlElement = + div( + cls("p-8 bg-gray-100 h-full"), + header, + children + ) + + def pageHeader( + title: Modifier[HtmlElement], + right: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] = None + ): HtmlElement = + div( + cls("pb-5 border-b border-gray-200"), + div(cls("float-right"), right), + h1( + cls("text-2xl leading-6 font-medium text-gray-900"), + title + ), + subtitle.map( + p( + cls("text-sm font-medium text-gray-500"), + _ + ) + ) + ) + + def clickable: Modifier[HtmlElement] = + cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TableComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TableComponentsModule.scala new file mode 100644 index 0000000..417ea56 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TableComponentsModule.scala @@ -0,0 +1,86 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L +import com.raquo.laminar.api.L.{*, given} + +trait TableComponentsModule: + + object tables: + + def tableSection( + title: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] = None, + actions: Modifier[HtmlElement]* + )( + table: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls("px-4 sm:px-6 lg:px-8"), + div( + cls("sm:flex sm:items-center"), + div( + cls("sm:flex-auto"), + h1(cls("text-base font-semibold leading-6 text-gray-900"), title), + subtitle.map(st => p(cls("mt-2 text-sm text-gray-700"), st)) + ), + div(cls("mt-4 sm:ml-16 sm:mt-0 sm:flex-none"), actions) + ), + div( + cls("mt-8 flow-root"), + div( + cls("-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"), + div( + cls("inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"), + table + ) + ) + ) + ) + + def simpleTable(header: Modifier[HtmlElement]*)( + body: Modifier[HtmlElement]* + ): HtmlElement = + table( + cls("min-w-full divide-y divide-gray-300"), + thead(header), + tbody(cls("divide-y divide-gray-200"), body) + ) + + def headerRow( + mods: Modifier[HtmlElement]* + )(cells: HtmlElement*): HtmlElement = + tr( + cells.zipWithIndex.map((c, i) => + if i == 0 then c.amend(cls("py-3.5 pl-4 pr-3 sm:pl-0")) + else if i == cells.length - 1 then + c.amend(cls("py-3.5 pr-4 pl-3 sm:pr-0")) + else c.amend(cls("py-3.5 px-3")) + ) + ) + + def dataRow( + mods: Modifier[HtmlElement]* + )(cells: HtmlElement*): HtmlElement = + tr( + mods, + cells.zipWithIndex.map((c, i) => + if i == 0 then c.amend(cls("py-4 pl-4 pr-3 sm:pl-0")) + else if i == cells.length - 1 then + c.amend(cls("py-4 pr-4 pl-3 sm:pr-0")) + else c.amend(cls("py-4 px-3")) + ) + ) + + def headerCell(content: Modifier[HtmlElement]): HtmlElement = + th( + cls("text-left text-sm font-semibold text-gray-900"), + content + ) + + def dataCell(content: Modifier[HtmlElement]): HtmlElement = + td( + cls("whitespace-nowrap text-sm text-gray-500"), + content + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TailwindUICatalogue.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TailwindUICatalogue.scala new file mode 100644 index 0000000..5027b10 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TailwindUICatalogue.scala @@ -0,0 +1,17 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} + +trait TailwindUICatalogueModule + extends BadgeComponentsModule + with ButtonComponentsModule + with HeadingsComponentsModule + with IconsModule + with FormComponentsModule + with LayoutModule + with ListComponentsModule + with ModalComponentsModule + with PageComponentsModule + with TableComponentsModule diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/LabelsOnLeftFormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/LabelsOnLeftFormComponentsModule.scala deleted file mode 100644 index 210bbf8..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/LabelsOnLeftFormComponentsModule.scala +++ /dev/null @@ -1,155 +0,0 @@ -package works.iterative.ui.components.laminar -package tailwindui - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -trait LabelsOnLeftFormComponentsModule(using ctx: ComponentContext) - extends FormComponentsModule: - self: IconsModule => - override val forms = new FormComponents: - - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )(content: Modifier[HtmlElement]*): HtmlElement = - div( - cls("space-y-6 sm:space-y-5"), - div( - h3(cls("text-lg leading-6 font-medium text-gray-900"), title), - subtitle.map(st => p(cls("mt-1 max-w-2xl text-sm text-gray-500"), st)) - ), - div(cls("mt-6 sm:mt-5 space-y-6 sm:space-y-5"), content) - ) - - override def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement = - L.label( - cls("block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"), - forId.map(id => L.forId(id)), - labelText, - mods - ) - - override def field( - label: Modifier[HtmlElement] - )(content: Modifier[HtmlElement]*): 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, - div(cls("mt-1 sm:mt-0 sm:col-span-2"), content) - ) - - override def field( - id: String, - labelText: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement = - field( - label(labelText, Some(id))() - )( - input.amend(idAttr(id)), - help.map(h => p(cls("mt-2 text-sm text-gray-500"), h)) - ) - - override def form(mods: Modifier[HtmlElement]*)( - sections: Modifier[HtmlElement]* - )(actions: Modifier[HtmlElement]*): HtmlElement = - L.form( - cls("space-y-8 divide-y divide-gray-200"), - mods, - sections, - div( - cls("pt-5"), - div(cls("flex justify-end"), actions) - ) - ) - - override def inlineForm( - mods: Modifier[HtmlElement]* - ): HtmlElement = - L.form(cls("flex space-x-4"), mods) - - override def searchField( - id: String, - placeholderText: Option[String] = None - )( - mods: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "flex-1 min-w-0", - label(ctx.messages("forms.search.label"), Some(id))(cls("sr-only")), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - icons.`search-solid`() - ), - input( - tpe := "search", - nameAttr := "search", - idAttr := id, - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholderText - .orElse( - ctx.messages - .opt( - s"forms.search.${id}.placeholder", - s"form.search.placeholder" - ) - ) - .map(placeholder(_)), - mods - ) - ) - ) - - override def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement = - field( - id, - labelText, - input( - tpe(inputType), - cls( - "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:max-w-xs sm:text-sm sm:leading-6" - ), - placeholderText.map(placeholder(_)) - ), - helpText - ) - - override def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement = - div( - labelText, - input( - idAttr(id), - nameAttr(id), - autoComplete("date"), - tpe("date"), - cls( - "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ), - placeholderText.map(placeholder(_)), - mods(localDateSelect) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/TailwindUIButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/TailwindUIButtonComponentsModule.scala deleted file mode 100644 index 4c44157..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/TailwindUIButtonComponentsModule.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar -package tailwindui - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait TailwindUIButtonComponentsModule(using ctx: ComponentContext) - extends ButtonComponentsModule: - - override val buttons = new ButtonComponents: - - private inline def srHelp(id: String): Modifier[HtmlElement] = - ctx.messages - .opt(s"form.button.${id}.screenReaderHelp") - .map(sr => span(cls := "sr-only", sr)) - - override def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )(mods: Modifier[HtmlElement]*): HtmlElement = - button( - tpe(buttonType), - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - icon, - text, - mods - ) - - override def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )(mods: Modifier[HtmlElement]*): HtmlElement = - button( - tpe(buttonType), - cls( - "ml-2 bg-white inline-flex justify-center 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" - ), - icon, - text, - mods - ) - - override def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement = - button( - tpe := "button", - cls := "ml-1 inline-flex h-4 w-4 flex-shrink-0 rounded-full p-1 text-gray-400 hover:bg-gray-200 hover:text-gray-500", - srHelp(id), - icon - ) - - override def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement = - button( - tpe := "button", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - icon, - srHelp(id) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/tailwind/laminar/LaminarExtensions.scala b/ui/js/src/main/scala/works/iterative/ui/components/tailwind/laminar/LaminarExtensions.scala index 4c5e68b..4c87800 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/tailwind/laminar/LaminarExtensions.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/tailwind/laminar/LaminarExtensions.scala @@ -2,23 +2,9 @@ package laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.Color import works.iterative.core.UserMessage object LaminarExtensions: - given colorToCSS: Conversion[Color, Modifier[HtmlElement]] with - def apply(c: Color) = cls(c.toCSS) - - given colorToSVGCSS: Conversion[Color, Modifier[SvgElement]] with - def apply(c: Color) = svg.cls(c.toCSS) - - given colorSignalToCSS: Conversion[Signal[Color], Modifier[HtmlElement]] with - def apply(c: Signal[Color]) = cls <-- c.map(_.toCSS) - - given colorSignalToSVGCSS: Conversion[Signal[Color], Modifier[SvgElement]] - with - def apply(c: Signal[Color]) = svg.cls <-- c.map(_.toCSS) - inline given userMessageToModifier(using ctx: ComponentContext ): Conversion[UserMessage, Modifier[HtmlElement]] with diff --git a/ui/js/src/main/scala/works/iterative/ui/scenarios/Scenario.scala b/ui/js/src/main/scala/works/iterative/ui/scenarios/Scenario.scala index 99a212a..60de9f0 100644 --- a/ui/js/src/main/scala/works/iterative/ui/scenarios/Scenario.scala +++ b/ui/js/src/main/scala/works/iterative/ui/scenarios/Scenario.scala @@ -13,8 +13,6 @@ import works.iterative.core.MessageCatalogue import works.iterative.ui.components.tailwind.ComponentContext import works.iterative.ui.components.tailwind.StyleGuide -import works.iterative.ui.model.color.Color -import works.iterative.ui.model.color.ColorWeight object Scenario: type Id = String diff --git a/ui/shared/src/main/scala/works/iterative/ui/model/Tag.scala b/ui/shared/src/main/scala/works/iterative/ui/model/Tag.scala index 7352579..958aa75 100644 --- a/ui/shared/src/main/scala/works/iterative/ui/model/Tag.scala +++ b/ui/shared/src/main/scala/works/iterative/ui/model/Tag.scala @@ -1,10 +1,16 @@ package works.iterative.ui.model -import works.iterative.ui.model.color.ColorKind - -/** Representation of colored string value. +/** Representation of a label or tag. * - * Used generally to represent tags or "labels", eg. some kind of status or - * categorization. + * The UI renderer will recognize this value as tag and render accordingly. The + * value is used to determine both the color of the tag and the text displayed. + * + * @param value + * the value of the tag */ -final case class Tag(value: String, color: ColorKind = ColorKind.gray) +opaque type Tag = String + +object Tag: + def apply(value: String): Tag = value + + extension (tag: Tag) def value: String = tag diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala index a2c637c..bfa063a 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala @@ -1,13 +1,11 @@ package works.iterative.ui.components.laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorDef import com.raquo.laminar.tags.HtmlTag case class Bin[Source, +Value]( label: String, description: Option[String | HtmlElement], - color: ColorDef, valueOf: Source => Value ): def map[NewValue]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala deleted file mode 100644 index 3aac0bf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala +++ /dev/null @@ -1,34 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ButtonComponentsModule: - def buttons: ButtonComponents - - trait ButtonComponents: - def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala deleted file mode 100644 index 646e73d..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala +++ /dev/null @@ -1,191 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.color.ColorDef -import com.raquo.laminar.tags.HtmlTag -import org.scalajs.dom.html.UList -import com.raquo.laminar.nodes.ReactiveHtmlElement -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind - -trait DashboardComponentsModule: - def dashboard: DashboardComponents - - // Only methods that create HtmlElements - trait DashboardComponents(using ComponentContext): - def section(label: String, children: Modifier[HtmlElement]): Div - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI - def cardInitials( - initials: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div - def count(label: String, value: Int, color: ColorDef): Span - def counts(children: HtmlElement*): Span - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - -trait DefaultDashboardComponentsModule(using ComponentContext) - extends DashboardComponentsModule: - override val dashboard: DashboardComponents = new DashboardComponents: - def section(label: String, children: Modifier[HtmlElement]): Div = - div( - cls("mt-3"), - h2( - cls("text-gray-500 text-xs font-medium uppercase tracking-wide"), - label - ), - children - ) - - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] = - ul( - cls( - "mt-3 grid gap-5 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" - ), - children - ) - - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI = - li( - div( - cls("col-span-1 flex shadow-sm rounded-md"), - initials, - div( - cls( - "flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate" - ), - div( - cls("flex-1 px-4 py-2 text-sm truncate"), - div( - cls("text-gray-900 font-medium hover:text-gray-600 truncate"), - label - ), - p(cls("text-gray-500"), children) - ) - ) - ) - ) - - def cardInitials( - id: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div = - div( - cls( - "flex-shrink-0 flex flex-col items-center justify-center w-16 text-white text-sm font-medium rounded-l-md" - ), - color.map(_(600).bg), - div(id), - children - ) - - def count(label: String, value: Int, color: ColorDef): Span = - span(color.text, title(label), s"${value}") - - def counts(children: HtmlElement*): Span = - span(interleave(children, span(" / "))) - - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div = - div( - cls("flex flex-col mt-2"), - div( - cls( - "align-middle min-w-full shadow sm:rounded-lg" - ), - L.table( - cls("min-w-full border-separate border-spacing-0"), - styleAttr("border-spacing: 0"), // Tailwind somehow doesn't work - thead( - tr( - cls("border-t border-gray-200"), - headers.map(h => - th( - cls( - "sticky top-0 z-10 px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - h - ) - ) - ) - ), - tbody(cls("bg-white divide-y divide-gray-100"), sections) - ) - ) - ) - - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - td( - cls("bg-gray-50 border-gray-200 border-t border-b"), - cls( - "sticky top-14 z-10 bg-gray-50 px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - colSpan(cs), - label - ), - mods - ) - - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - mods, - td( - cls( - "px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900 truncate" - ), - label - ), - children.map(e => - td( - cls( - "whitespace-nowrap px-2 py-2 text-sm text-gray-500" - ), - e - ) - ) - ) - - private def interleave( - a: Seq[HtmlElement], - separator: => HtmlElement - ): Seq[HtmlElement] = - if a.size < 2 then a - else a.init.flatMap(n => Seq(n, separator)) :+ a.last diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala deleted file mode 100644 index a45beaf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.FileRef - -trait DetailComponentsModule: - - def details: DetailComponents - - trait DetailComponents: - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement - - def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement - - def files(fs: List[FileRef]): HtmlElement - - def file(f: FileRef): HtmlElement - -trait DefaultDetailComponentsModule(using ctx: ComponentContext) - extends DetailComponentsModule: - self: IconsModule => - - override val details: DetailComponents = new DetailComponents: - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement = - div( - div( - cls := "px-4 sm:px-0", - h3( - cls := "text-base font-semibold leading-7 text-gray-900", - title - ), - subtitle.map(st => - p( - cls := "mt-1 max-w-2xl text-sm leading-6 text-gray-500", - st - ) - ) - // TODO: actions - ), - div( - cls := "mt-6 border-t border-gray-100", - dl( - cls := "divide-y divide-gray-100", - fields - ) - ) - ) - - override def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0", - dt( - cls := "text-sm font-medium leading-6 text-gray-900", - title - ), - dd( - cls := "mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0", - content - ) - ) - - override def files(fs: List[FileRef]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-100 rounded-md border border-gray-200", - fs.map(file) - ) - - override def file(f: FileRef): HtmlElement = - li( - cls := "flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6", - div( - cls := "flex w-0 flex-1 items-center", - icons.`paper-clip-solid`(), - div( - cls := "ml-4 flex min-w-0 flex-1 gap-2", - span( - cls := "truncate font-medium", - f.name - ), - f.size.map(size => span(cls := "flex-shrink-0 text-gray-400", size)) - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - "Uložit" - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala new file mode 100644 index 0000000..f6c583f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait FormComponents(using ctx: ComponentContext) extends LocalDateSelectModule: + def searchIcon: SvgElement + + def renderLocalDateSelect( + id: String, + labelText: Option[String], + placeholderText: Option[String], + mods: LocalDateSelect => Modifier[HtmlElement] + ): HtmlElement = + div( + labelText, + input( + idAttr(id), + nameAttr(id), + autoComplete("date"), + tpe("date"), + cls( + "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ), + placeholderText.map(placeholder(_)), + mods(localDateSelect) + ) + ) + + def searchField( + id: String, + placeholderText: Option[String] = None + )( + mods: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls := "flex-1 min-w-0", + label(ctx.messages("forms.search.label"), forId(id), cls("sr-only")), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + searchIcon + ), + input( + tpe := "search", + nameAttr := "search", + idAttr := id, + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholderText + .orElse( + ctx.messages + .opt( + s"forms.search.${id}.placeholder", + s"form.search.placeholder" + ) + ) + .map(placeholder(_)), + mods + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala deleted file mode 100644 index 7714490..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala +++ /dev/null @@ -1,82 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -/** Form components - * - * A form is a collection of sections, each of which contains a collection of - * fields. - * - * Form -> * Section -> * Field - * -> * Action - * - * Each Field has an `id`, `label` and `input` element. The `id` is used to - * link the label to the input. - * - * The `input` element can be a simple text input, or a more complex component - * such as a date picker. The caller is expected to make the input work. - */ -trait FormComponentsModule extends LocalDateSelectModule: - def forms: FormComponents - - trait FormComponents: - - /** Layout the sections and actions of a form */ - def form(mods: Modifier[HtmlElement]*)(sections: Modifier[HtmlElement]*)( - actions: Modifier[HtmlElement]* - ): HtmlElement - - /** Section layout, with a title, optional subtitle and any content. */ - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label and any content */ - def field(label: Modifier[HtmlElement])( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label, input and optional help text */ - def field( - id: String, - label: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement - - def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout an inline form element */ - def inlineForm(mods: Modifier[HtmlElement]*): HtmlElement - - /** An input field with a search icon */ - def searchField(id: String, placeholderText: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement - - /** LocalDate input */ - def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala deleted file mode 100644 index 8acbe7f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala +++ /dev/null @@ -1,28 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.Tag - -trait GenericComponentsModule: - - def generic: GenericComponents - - trait GenericComponents: - def tag(name: String, color: ColorKind): HtmlElement - def tag(t: Tag): HtmlElement = tag(t.value, t.color) - -trait DefaultGenericComponentsModule(using ComponentContext) - extends GenericComponentsModule: - override val generic: GenericComponents = new GenericComponents: - override def tag(name: String, color: ColorKind): HtmlElement = - p( - cls( - "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" - ), - color(800).text, - color(100).bg, - name - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala deleted file mode 100644 index 8cfc1d9..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala +++ /dev/null @@ -1,123 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.CustomAttrs - -trait IconsModule: - def icons: Icons - - trait Icons: - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement - def close(mods: Modifier[SvgElement]*): SvgElement - def `search-solid`(mods: Modifier[SvgElement]*): SvgElement - def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement - def `document-chart-bar-outline`(mods: Modifier[SvgElement]*): SvgElement - def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement - -trait DefaultIconsModule(using ComponentContext) extends IconsModule: - override val icons: Icons = new Icons: - import svg.* - - object aria: - val hidden = CustomAttrs.svg.ariaHidden - - private def withDefault( - mods: Seq[Modifier[SvgElement]], - default: Modifier[SvgElement] - ): Modifier[SvgElement] = - if mods.isEmpty then default else mods - - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-8 w-8"), - 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" - ) - ) - ) - - override def close(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-2 w-2"), - stroke := "currentColor", - fill := "none", - viewBox := "0 0 8 8", - path( - strokeLineCap := "round", - strokeWidth := "1.5", - d := "M1 1l6 6m0-6L1 7" - ) - ) - - override def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 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 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") - ) - ) - - override def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 text-gray-400")), - 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") - ) - ) - - override def `document-chart-bar-outline`( - mods: Modifier[SvgElement]* - ): SvgElement = - svg( - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - strokeWidth := "1.5", - stroke := "currentColor", - withDefault(mods, cls := "h-6 w-6"), - path( - strokeLineCap := "round", - strokeLineJoin := "round", - d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" - ) - ) - - override def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-5 w-5"), - cls := "flex-shrink-0 text-gray-400", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", - clipRule := "evenodd" - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala deleted file mode 100644 index 95ab654..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait LayoutModule: - def layout: LayoutComponents - - trait LayoutComponents: - def card(content: Modifier[HtmlElement]*): HtmlElement - -trait DefaultLayoutModule(using ctx: ComponentContext) extends LayoutModule: - override val layout: LayoutComponents = new LayoutComponents: - override def card(content: Modifier[HtmlElement]*): HtmlElement = - div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala deleted file mode 100644 index 5946974..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom.html.Paragraph -import com.raquo.laminar.nodes.TextNode - -trait ListComponentsModule: - - def list: ListComponents - - trait ListComponents(using ComponentContext): - def label(text: String, color: ColorKind): HtmlElement - def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI - def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] - def listSection(header: String, list: HtmlElement): Div - def navigation(sections: Modifier[HtmlElement]): HtmlElement - -trait DefaultListComponentsModule(using ComponentContext) - extends ListComponentsModule: - self: GenericComponentsModule => - - override val list: ListComponents = new ListComponents: - override def label( - text: String, - color: ColorKind - ): HtmlElement = generic.tag(text, color) - - override def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI = - li( - cls("group"), - div( - cls( - "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" - ), - avatar.map(a => - div( - cls("flex-shrink-0"), - div( - cls( - "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" - ), - a - ) - ) - ), - div( - cls("flex-1 min-w-0"), - p( - cls("text-sm font-medium text-gray-900"), - title, - span(cls("float-right"), right) - ), - subtitle.map(st => - p( - cls("text-sm text-gray-500 truncate"), - st - ) - ) - ) - ) - ) - - override def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = - ul( - cls("relative z-0 divide-y divide-gray-200"), - role("list"), - children - ) - - override def listSection( - header: String, - list: HtmlElement - ): Div = - div( - cls("relative"), - div( - cls( - "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" - ), - header - ), - list - ) - - override def navigation(sections: Modifier[HtmlElement]): HtmlElement = - navTag( - cls("flex-1 min-h-0 overflow-y-auto"), - sections - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala deleted file mode 100644 index 5b5490f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ModalComponentsModule: - - def modal: ModalComponents - - trait ModalComponents: - def modalDialog( - content: HtmlElement, - onClose: Modifier[HtmlElement] - ): HtmlElement - -trait DefaultModalComponentsModule(using ctx: ComponentContext) - extends ModalComponentsModule: - - override val modal: ModalComponents = new ModalComponents: - - override def modalDialog( - content: HtmlElement, - closeMod: Modifier[HtmlElement] - ): 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( - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")), - closeMod - ) - ) - - div( - cls("fixed inset-0 z-20 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - 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-7xl sm:align-middle" - ), - content - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala deleted file mode 100644 index b713d8a..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala +++ /dev/null @@ -1,71 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait PageComponentsModule: - - val page: PageComponents - - trait PageComponents: - def container( - children: Modifier[HtmlElement]* - ): HtmlElement - - def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement - - def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement] = emptyMod, - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement - - /** Visage for clickable text, like links - */ - def clickable: Modifier[HtmlElement] - -trait DefaultPageComponentsModule(using ComponentContext) - extends PageComponentsModule: - - override val page: PageComponents = new PageComponents: - override def container( - children: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), - children - ) - - override def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement = - div( - cls("p-8 bg-gray-100 h-full"), - header, - children - ) - - override def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement = - div( - cls("pb-5 border-b border-gray-200"), - div(cls("float-right"), right), - h1( - cls("text-2xl leading-6 font-medium text-gray-900"), - title - ), - subtitle.map( - p( - cls("text-sm font-medium text-gray-500"), - _ - ) - ) - ) - - override def clickable: Modifier[HtmlElement] = - cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala deleted file mode 100644 index f70ee87..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala +++ /dev/null @@ -1,108 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} - -trait TableComponentsModule: - def tables: TableComponents - - trait TableComponents: - def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )(table: Modifier[HtmlElement]*): HtmlElement - - def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement - - def headerRow(mods: Modifier[HtmlElement]*)( - cells: HtmlElement* - ): HtmlElement - - def dataRow(mods: Modifier[HtmlElement]*)(cells: HtmlElement*): HtmlElement - - def headerCell(content: Modifier[HtmlElement]): HtmlElement - - def dataCell(content: Modifier[HtmlElement]): HtmlElement - -trait DefaultTableComponentsModule extends TableComponentsModule: - - override lazy val tables: TableComponents = new TableComponents: - - override def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )( - table: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("px-4 sm:px-6 lg:px-8"), - div( - cls("sm:flex sm:items-center"), - div( - cls("sm:flex-auto"), - h1(cls("text-base font-semibold leading-6 text-gray-900"), title), - subtitle.map(st => p(cls("mt-2 text-sm text-gray-700"), st)) - ), - div(cls("mt-4 sm:ml-16 sm:mt-0 sm:flex-none"), actions) - ), - div( - cls("mt-8 flow-root"), - div( - cls("-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"), - div( - cls("inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"), - table - ) - ) - ) - ) - - override def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement = - table( - cls("min-w-full divide-y divide-gray-300"), - thead(header), - tbody(cls("divide-y divide-gray-200"), body) - ) - - override def headerRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-3.5 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-3.5 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-3.5 px-3")) - ) - ) - - override def dataRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - mods, - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-4 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-4 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-4 px-3")) - ) - ) - - override def headerCell(content: Modifier[HtmlElement]): HtmlElement = - th( - cls("text-left text-sm font-semibold text-gray-900"), - content - ) - - override def dataCell(content: Modifier[HtmlElement]): HtmlElement = - td( - cls("whitespace-nowrap text-sm text-gray-500"), - content - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala index b937d45..12d01ae 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala @@ -3,7 +3,7 @@ import com.raquo.laminar.api.L.{*, given} import works.iterative.ui.components.laminar.HtmlTabular import works.iterative.ui.model.tables.Tabular -import works.iterative.ui.components.laminar.TableComponentsModule +import works.iterative.ui.components.laminar.tailwind.ui.TableComponentsModule import works.iterative.core.UserMessage import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given import works.iterative.ui.components.tailwind.ComponentContext diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala new file mode 100644 index 0000000..87f3e8d --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala @@ -0,0 +1,54 @@ +package works.iterative.ui.components.laminar.tailwind.color + +import com.raquo.laminar.api.L.{*, given} + +/** Complete color definition that can be rendered to CSS. + * + * Includes the area, kind and weight of the color. + */ +case class Color(area: ColorArea, color: ColorDef): + def toCSS: String = s"${area.name}-${color.toCSS}" + +object Color: + import ColorDef.given + + given colorToCSS: Conversion[Color, HtmlMod] with + def apply(c: Color) = cls(c.toCSS) + + given colorToSVGCSS: Conversion[Color, SvgMod] with + def apply(c: Color) = svg.cls(c.toCSS) + + given colorSignalToCSS: Conversion[Signal[Color], HtmlMod] with + def apply(c: Signal[Color]) = cls <-- c.map(_.toCSS) + + given colorSignalToSVGCSS: Conversion[Signal[Color], SvgMod] with + def apply(c: Signal[Color]) = svg.cls <-- c.map(_.toCSS) + + def current = ColorDef(ColorKind.current) + def inherit = ColorDef(ColorKind.inherit) + def transp = ColorDef(ColorKind.transp) + def auto = ColorDef(ColorKind.auto) + def black = ColorDef(ColorKind.black) + def white = ColorDef(ColorKind.white) + def slate(weight: ColorWeight) = ColorDef(ColorKind.slate, weight) + def gray(weight: ColorWeight) = ColorDef(ColorKind.gray, weight) + def zinc(weight: ColorWeight) = ColorDef(ColorKind.zinc, weight) + def neutral(weight: ColorWeight) = ColorDef(ColorKind.neutral, weight) + def stone(weight: ColorWeight) = ColorDef(ColorKind.stone, weight) + def red(weight: ColorWeight) = ColorDef(ColorKind.red, weight) + def orange(weight: ColorWeight) = ColorDef(ColorKind.orange, weight) + def amber(weight: ColorWeight) = ColorDef(ColorKind.amber, weight) + def yellow(weight: ColorWeight) = ColorDef(ColorKind.yellow, weight) + def lime(weight: ColorWeight) = ColorDef(ColorKind.lime, weight) + def green(weight: ColorWeight) = ColorDef(ColorKind.green, weight) + def emerald(weight: ColorWeight) = ColorDef(ColorKind.emerald, weight) + def teal(weight: ColorWeight) = ColorDef(ColorKind.teal, weight) + def cyan(weight: ColorWeight) = ColorDef(ColorKind.cyan, weight) + def sky(weight: ColorWeight) = ColorDef(ColorKind.sky, weight) + def blue(weight: ColorWeight) = ColorDef(ColorKind.blue, weight) + def indigo(weight: ColorWeight) = ColorDef(ColorKind.indigo, weight) + def violet(weight: ColorWeight) = ColorDef(ColorKind.violet, weight) + def purple(weight: ColorWeight) = ColorDef(ColorKind.purple, weight) + def fuchsia(weight: ColorWeight) = ColorDef(ColorKind.fuchsia, weight) + def pink(weight: ColorWeight) = ColorDef(ColorKind.pink, weight) + def rose(weight: ColorWeight) = ColorDef(ColorKind.rose, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala new file mode 100644 index 0000000..4130840 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala @@ -0,0 +1,16 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines the area the color should apply to, eg. background, text, border, + * etc. + */ +enum ColorArea(val name: String): + case bg extends ColorArea("bg") + case text extends ColorArea("text") + case decoration extends ColorArea("decoration") + case border extends ColorArea("border") + case outline extends ColorArea("outline") + case divide extends ColorArea("divide") + case ring extends ColorArea("ring") + case ringOffset extends ColorArea("ring-offset") + case shadow extends ColorArea("shadow") + case accent extends ColorArea("accent") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala new file mode 100644 index 0000000..fab9404 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala @@ -0,0 +1,43 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** A combination of ColorKind and ColorWeight, if applicable. + * + * By applying area we get the full Color definition. + */ +sealed trait ColorDef: + def toCSS: String + + def bg = Color(ColorArea.bg, this) + def text = Color(ColorArea.text, this) + def decoration = Color(ColorArea.decoration, this) + def border = Color(ColorArea.border, this) + def outline = Color(ColorArea.outline, this) + def divide = Color(ColorArea.divide, this) + def ring = Color(ColorArea.ring, this) + def ringOffset = Color(ColorArea.ringOffset, this) + def shadow = Color(ColorArea.shadow, this) + def accent = Color(ColorArea.accent, this) + +// TODO: create a macro that will output the tailwind class as string directly during compilation, where possible. +object ColorDef: + case class WeightedColorDef( + kind: ColorKind, + weight: ColorWeight + ) extends ColorDef: + override def toCSS: String = s"${kind.name}-${weight.value}" + + case class UnweightedColorDef( + kind: ColorKind + ) extends ColorDef: + override def toCSS: String = kind.name + + // TODO: check that the kind is valid unweighted kind + // that means current, inherit, auto, transparent, black, white + // tried using implicit evidence, but the type inference for enumerations + // tends to generalize to the enum, instead of the real type + def apply[T <: ColorKind](kind: T)(using + ev: T <:< ColorKind.Unweighted + ): ColorDef = + UnweightedColorDef(kind) + def apply(kind: ColorKind, weight: ColorWeight): ColorDef = + WeightedColorDef(kind, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala new file mode 100644 index 0000000..865166b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala @@ -0,0 +1,47 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines what color should be used, without specifying the area or weight. + */ +sealed abstract class ColorKind private (val name: String): + def apply(weight: ColorWeight): ColorDef = + ColorDef.WeightedColorDef(this, weight) + +object ColorKind: + trait Unweighted: + self: ColorKind => + override def apply(weight: ColorWeight): ColorDef = + ColorDef.UnweightedColorDef(self) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case object current extends ColorKind("current") with Unweighted + case object inherit extends ColorKind("inherit") with Unweighted + // Not present in for all methods + case object transp extends ColorKind("transparent") with Unweighted + // Seen in accent, not preset otherwise + case object auto extends ColorKind("auto") with Unweighted + // Black and white do not have weight + case object black extends ColorKind("black") with Unweighted + case object white extends ColorKind("white") with Unweighted + case object slate extends ColorKind("slate") + case object gray extends ColorKind("gray") + case object zinc extends ColorKind("zinc") + case object neutral extends ColorKind("neutral") + case object stone extends ColorKind("stone") + case object red extends ColorKind("red") + case object orange extends ColorKind("orange") + case object amber extends ColorKind("amber") + case object yellow extends ColorKind("yellow") + case object lime extends ColorKind("lime") + case object green extends ColorKind("green") + case object emerald extends ColorKind("emerald") + case object teal extends ColorKind("teal") + case object cyan extends ColorKind("cyan") + case object sky extends ColorKind("sky") + case object blue extends ColorKind("blue") + case object indigo extends ColorKind("indigo") + case object violet extends ColorKind("violet") + case object purple extends ColorKind("purple") + case object fuchsia extends ColorKind("fuchsia") + case object pink extends ColorKind("pink") + case object rose extends ColorKind("rose") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala new file mode 100644 index 0000000..6f1a4cc --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala @@ -0,0 +1,33 @@ +package works.iterative.ui.components.laminar.tailwind.color + +opaque type ColorWeight = String + +extension (c: ColorWeight) def value: String = c + +/** Defines weight of a color, eg. 50, 100, 200, etc. + * + * Tailwind-like. + */ +object ColorWeight: + inline given int50: Conversion[50, ColorWeight] with + inline def apply(i: 50) = "50" + inline given int100: Conversion[100, ColorWeight] with + inline def apply(i: 100) = "100" + inline given int200: Conversion[200, ColorWeight] with + inline def apply(i: 200) = "200" + inline given int300: Conversion[300, ColorWeight] with + inline def apply(i: 300) = "300" + inline given int400: Conversion[400, ColorWeight] with + inline def apply(i: 400) = "400" + inline given int500: Conversion[500, ColorWeight] with + inline def apply(i: 500) = "500" + inline given int600: Conversion[600, ColorWeight] with + inline def apply(i: 600) = "600" + inline given int700: Conversion[700, ColorWeight] with + inline def apply(i: 700) = "700" + inline given int800: Conversion[800, ColorWeight] with + inline def apply(i: 800) = "800" + inline given int900: Conversion[900, ColorWeight] with + inline def apply(i: 900) = "900" + inline given int950: Conversion[950, ColorWeight] with + inline def apply(i: 950) = "950" diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala new file mode 100644 index 0000000..21e21f0 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala @@ -0,0 +1,12 @@ +package works.iterative.ui.components.laminar.tailwind + +/** We need a generic Color model that can be used both on server and on client. + * + * We have adopted the Tailwind model for now. There is nothing inherently + * Tailwind-specific in the implementation, but all the values are taken from + * their palette and the area model is very HTML biased. + * + * Still, I think that it is a good starting point for exploration and will + * satisfy our current needs. + */ +package object color {} diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" new file mode 100644 index 0000000..5c4e218 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" @@ -0,0 +1,21 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.model.color.ColorKind +import works.iterative.ui.components.tailwind.ComponentContext +import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given +import works.iterative.ui.model.Tag + +trait TailwindUIBadgesComponentsModule(using ComponentContext): + + object badges: + override def tag(name: String, color: ColorKind): HtmlElement = + p( + cls( + "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" + ), + color(800).text, + color(100).bg, + name + ) diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" new file mode 100644 index 0000000..0c9ca25 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" @@ -0,0 +1,24 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} + +trait TailwindUIHeadingsComponentsModule: + val headings: HeadingsComponents = new HeadingsComponents + + class HeadingsComponents: + def section(title: Node, actions: HtmlElement*): HtmlElement = + div( + cls("border-b border-gray-200 pb-5"), + div( + cls("sm:flex sm:items-center sm:justify-between"), + h3(cls("text-base font-semibold leading-6 text-gray-900"), title), + div( + cls("mt-3 flex sm:ml-4 sm:mt-0"), + actions match + case Nil => emptyMod + case first :: rest => + first :: rest.map(_.amend(cls("ml-3"))) + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala new file mode 100644 index 0000000..16ca01b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala @@ -0,0 +1,19 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.laminar.tailwind.color.ColorKind + +trait BadgeComponentsModule: + + object badges: + def pill(name: String, color: ColorKind): HtmlElement = + p( + cls( + "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" + ), + color(800).text, + color(100).bg, + name + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala new file mode 100644 index 0000000..ebb2968 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} + +trait ButtonComponentsModule: + + object buttons: + + private inline def srHelp(text: String): Modifier[HtmlElement] = + span(cls := "sr-only", text) + + def primaryButton( + id: String, + text: Modifier[HtmlElement], + icon: Option[SvgElement] = None, + buttonType: String = "submit" + )(mods: Modifier[HtmlElement]*): HtmlElement = + button( + tpe(buttonType), + cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + icon, + text, + mods + ) + + def secondaryButton( + id: String, + text: Modifier[HtmlElement], + icon: Option[SvgElement] = None, + buttonType: String = "button" + )(mods: Modifier[HtmlElement]*): HtmlElement = + button( + tpe(buttonType), + cls( + "ml-2 bg-white inline-flex justify-center 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" + ), + icon, + text, + mods + ) + + def inlineButton(id: String, icon: SvgElement)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + button( + tpe := "button", + cls := "ml-1 inline-flex h-4 w-4 flex-shrink-0 rounded-full p-1 text-gray-400 hover:bg-gray-200 hover:text-gray-500", + srHelp(id), + icon + ) + + def iconButton(id: String, icon: SvgElement, srText: Option[String] = None)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + button( + tpe := "button", + cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", + icon, + srText.map(srHelp(_)) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala new file mode 100644 index 0000000..eb6dd5f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala @@ -0,0 +1,101 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import org.scalajs.dom.html +import com.raquo.laminar.modifiers.KeyUpdater + +trait FormComponentsModule: + self: IconsModule => + + object forms: + + def section( + title: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] + )(content: Modifier[HtmlElement]*): HtmlElement = + div( + cls("space-y-6 sm:space-y-5"), + div( + h3(cls("text-lg leading-6 font-medium text-gray-900"), title), + subtitle.map(st => p(cls("mt-1 max-w-2xl text-sm text-gray-500"), st)) + ), + div(cls("mt-6 sm:mt-5 space-y-6 sm:space-y-5"), content) + ) + + def label(labelText: String, forId: Option[String] = None)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + L.label( + cls("block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"), + forId.map(id => L.forId(id)), + labelText, + mods + ) + + def field( + label: Modifier[HtmlElement] + )(content: Modifier[HtmlElement]*): 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, + div(cls("mt-1 sm:mt-0 sm:col-span-2"), content) + ) + + def field( + id: String, + labelText: String, + input: HtmlElement, + help: Option[String] + ): HtmlElement = + field( + label(labelText, Some(id))() + )( + input.amend(idAttr(id)), + help.map(h => p(cls("mt-2 text-sm text-gray-500"), h)) + ) + + def form(mods: Modifier[HtmlElement]*)( + sections: Modifier[HtmlElement]* + )(actions: Modifier[HtmlElement]*): HtmlElement = + L.form( + cls("space-y-8 divide-y divide-gray-200"), + mods, + sections, + div( + cls("pt-5"), + div(cls("flex justify-end"), actions) + ) + ) + + def inlineForm( + mods: Modifier[HtmlElement]* + ): HtmlElement = + L.form(cls("flex space-x-4"), mods) + + def inputField( + id: String, + labelText: String, + placeholderText: Option[String] = None, + inputType: String = "text", + helpText: Option[String] = None + ): HtmlElement = + field( + id, + labelText, + input( + tpe(inputType), + cls( + "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:max-w-xs sm:text-sm sm:leading-6" + ), + placeholderText.map(placeholder(_)) + ), + helpText + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala new file mode 100644 index 0000000..52da07e --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala @@ -0,0 +1,22 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} + +trait HeadingsComponentsModule: + object headings: + def section(title: Node, actions: HtmlElement*): HtmlElement = + div( + cls("border-b border-gray-200 pb-5"), + div( + cls("sm:flex sm:items-center sm:justify-between"), + h3(cls("text-base font-semibold leading-6 text-gray-900"), title), + div( + cls("mt-3 flex sm:ml-4 sm:mt-0"), + actions match + case Nil => emptyMod + case first :: rest => + first :: rest.map(_.amend(cls("ml-3"))) + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala new file mode 100644 index 0000000..c543150 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala @@ -0,0 +1,112 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext +import works.iterative.ui.components.tailwind.CustomAttrs + +trait IconsModule: + object icons: + import svg.* + + object aria: + val hidden = CustomAttrs.svg.ariaHidden + + private def withDefault( + mods: Seq[Modifier[SvgElement]], + default: Modifier[SvgElement] + ): Modifier[SvgElement] = + if mods.isEmpty then default else mods + + def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-8 w-8"), + 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" + ) + ) + ) + + def close(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-2 w-2"), + stroke := "currentColor", + fill := "none", + viewBox := "0 0 8 8", + path( + strokeLineCap := "round", + strokeWidth := "1.5", + d := "M1 1l6 6m0-6L1 7" + ) + ) + + def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls("h-5 w-5 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 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") + ) + ) + + def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls("h-5 w-5 text-gray-400")), + 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") + ) + ) + + def `document-chart-bar-outline`( + mods: Modifier[SvgElement]* + ): SvgElement = + svg( + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + strokeWidth := "1.5", + stroke := "currentColor", + withDefault(mods, cls := "h-6 w-6"), + path( + strokeLineCap := "round", + strokeLineJoin := "round", + d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" + ) + ) + + def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-5 w-5"), + cls := "flex-shrink-0 text-gray-400", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", + clipRule := "evenodd" + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala new file mode 100644 index 0000000..02cbdb6 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala @@ -0,0 +1,11 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait LayoutModule: + object layout: + def card(content: Modifier[HtmlElement]*): HtmlElement = + div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala new file mode 100644 index 0000000..b652da9 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala @@ -0,0 +1,88 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.laminar.nodes.ReactiveHtmlElement +import org.scalajs.dom.html.Paragraph +import com.raquo.laminar.nodes.TextNode +import works.iterative.ui.components.laminar.tailwind.color.ColorKind + +trait ListComponentsModule: + self: BadgeComponentsModule => + + object list: + def label( + text: String, + color: ColorKind + ): HtmlElement = badges.pill(text, color) + + def item( + title: String, + subtitle: Option[String], + right: Modifier[HtmlElement] = emptyMod, + avatar: Option[Modifier[HtmlElement]] = None + ): LI = + li( + cls("group"), + div( + cls( + "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" + ), + avatar.map(a => + div( + cls("flex-shrink-0"), + div( + cls( + "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" + ), + a + ) + ) + ), + div( + cls("flex-1 min-w-0"), + p( + cls("text-sm font-medium text-gray-900"), + title, + span(cls("float-right"), right) + ), + subtitle.map(st => + p( + cls("text-sm text-gray-500 truncate"), + st + ) + ) + ) + ) + ) + + def unordered( + children: Modifier[HtmlElement] + ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = + ul( + cls("relative z-0 divide-y divide-gray-200"), + role("list"), + children + ) + + def listSection( + header: String, + list: HtmlElement + ): Div = + div( + cls("relative"), + div( + cls( + "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" + ), + header + ), + list + ) + + def navigation(sections: Modifier[HtmlElement]): HtmlElement = + navTag( + cls("flex-1 min-h-0 overflow-y-auto"), + sections + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ModalComponentsModule.scala new file mode 100644 index 0000000..6027404 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ModalComponentsModule.scala @@ -0,0 +1,54 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait ModalComponentsModule: + object modal: + def modalDialog( + content: HtmlElement, + closeMod: Modifier[HtmlElement] + ): 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( + cls("fixed inset-0 transition-opacity"), + div(cls("absolute inset-0 bg-gray-500 opacity-75")), + closeMod + ) + ) + + div( + cls("fixed inset-0 z-20 overflow-y-auto"), + div( + cls("text-center sm:block sm:p-0"), + overlay, + browserCenteringModalTrick, + 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-7xl sm:align-middle" + ), + content + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/PageComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/PageComponentsModule.scala new file mode 100644 index 0000000..d5a422e --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/PageComponentsModule.scala @@ -0,0 +1,49 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait PageComponentsModule: + + object page: + def container( + children: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), + children + ) + + def singleColumn( + header: Modifier[HtmlElement] + )(children: Modifier[HtmlElement]*): HtmlElement = + div( + cls("p-8 bg-gray-100 h-full"), + header, + children + ) + + def pageHeader( + title: Modifier[HtmlElement], + right: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] = None + ): HtmlElement = + div( + cls("pb-5 border-b border-gray-200"), + div(cls("float-right"), right), + h1( + cls("text-2xl leading-6 font-medium text-gray-900"), + title + ), + subtitle.map( + p( + cls("text-sm font-medium text-gray-500"), + _ + ) + ) + ) + + def clickable: Modifier[HtmlElement] = + cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TableComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TableComponentsModule.scala new file mode 100644 index 0000000..417ea56 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TableComponentsModule.scala @@ -0,0 +1,86 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L +import com.raquo.laminar.api.L.{*, given} + +trait TableComponentsModule: + + object tables: + + def tableSection( + title: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] = None, + actions: Modifier[HtmlElement]* + )( + table: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls("px-4 sm:px-6 lg:px-8"), + div( + cls("sm:flex sm:items-center"), + div( + cls("sm:flex-auto"), + h1(cls("text-base font-semibold leading-6 text-gray-900"), title), + subtitle.map(st => p(cls("mt-2 text-sm text-gray-700"), st)) + ), + div(cls("mt-4 sm:ml-16 sm:mt-0 sm:flex-none"), actions) + ), + div( + cls("mt-8 flow-root"), + div( + cls("-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"), + div( + cls("inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"), + table + ) + ) + ) + ) + + def simpleTable(header: Modifier[HtmlElement]*)( + body: Modifier[HtmlElement]* + ): HtmlElement = + table( + cls("min-w-full divide-y divide-gray-300"), + thead(header), + tbody(cls("divide-y divide-gray-200"), body) + ) + + def headerRow( + mods: Modifier[HtmlElement]* + )(cells: HtmlElement*): HtmlElement = + tr( + cells.zipWithIndex.map((c, i) => + if i == 0 then c.amend(cls("py-3.5 pl-4 pr-3 sm:pl-0")) + else if i == cells.length - 1 then + c.amend(cls("py-3.5 pr-4 pl-3 sm:pr-0")) + else c.amend(cls("py-3.5 px-3")) + ) + ) + + def dataRow( + mods: Modifier[HtmlElement]* + )(cells: HtmlElement*): HtmlElement = + tr( + mods, + cells.zipWithIndex.map((c, i) => + if i == 0 then c.amend(cls("py-4 pl-4 pr-3 sm:pl-0")) + else if i == cells.length - 1 then + c.amend(cls("py-4 pr-4 pl-3 sm:pr-0")) + else c.amend(cls("py-4 px-3")) + ) + ) + + def headerCell(content: Modifier[HtmlElement]): HtmlElement = + th( + cls("text-left text-sm font-semibold text-gray-900"), + content + ) + + def dataCell(content: Modifier[HtmlElement]): HtmlElement = + td( + cls("whitespace-nowrap text-sm text-gray-500"), + content + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TailwindUICatalogue.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TailwindUICatalogue.scala new file mode 100644 index 0000000..5027b10 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TailwindUICatalogue.scala @@ -0,0 +1,17 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} + +trait TailwindUICatalogueModule + extends BadgeComponentsModule + with ButtonComponentsModule + with HeadingsComponentsModule + with IconsModule + with FormComponentsModule + with LayoutModule + with ListComponentsModule + with ModalComponentsModule + with PageComponentsModule + with TableComponentsModule diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/LabelsOnLeftFormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/LabelsOnLeftFormComponentsModule.scala deleted file mode 100644 index 210bbf8..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/LabelsOnLeftFormComponentsModule.scala +++ /dev/null @@ -1,155 +0,0 @@ -package works.iterative.ui.components.laminar -package tailwindui - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -trait LabelsOnLeftFormComponentsModule(using ctx: ComponentContext) - extends FormComponentsModule: - self: IconsModule => - override val forms = new FormComponents: - - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )(content: Modifier[HtmlElement]*): HtmlElement = - div( - cls("space-y-6 sm:space-y-5"), - div( - h3(cls("text-lg leading-6 font-medium text-gray-900"), title), - subtitle.map(st => p(cls("mt-1 max-w-2xl text-sm text-gray-500"), st)) - ), - div(cls("mt-6 sm:mt-5 space-y-6 sm:space-y-5"), content) - ) - - override def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement = - L.label( - cls("block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"), - forId.map(id => L.forId(id)), - labelText, - mods - ) - - override def field( - label: Modifier[HtmlElement] - )(content: Modifier[HtmlElement]*): 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, - div(cls("mt-1 sm:mt-0 sm:col-span-2"), content) - ) - - override def field( - id: String, - labelText: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement = - field( - label(labelText, Some(id))() - )( - input.amend(idAttr(id)), - help.map(h => p(cls("mt-2 text-sm text-gray-500"), h)) - ) - - override def form(mods: Modifier[HtmlElement]*)( - sections: Modifier[HtmlElement]* - )(actions: Modifier[HtmlElement]*): HtmlElement = - L.form( - cls("space-y-8 divide-y divide-gray-200"), - mods, - sections, - div( - cls("pt-5"), - div(cls("flex justify-end"), actions) - ) - ) - - override def inlineForm( - mods: Modifier[HtmlElement]* - ): HtmlElement = - L.form(cls("flex space-x-4"), mods) - - override def searchField( - id: String, - placeholderText: Option[String] = None - )( - mods: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "flex-1 min-w-0", - label(ctx.messages("forms.search.label"), Some(id))(cls("sr-only")), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - icons.`search-solid`() - ), - input( - tpe := "search", - nameAttr := "search", - idAttr := id, - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholderText - .orElse( - ctx.messages - .opt( - s"forms.search.${id}.placeholder", - s"form.search.placeholder" - ) - ) - .map(placeholder(_)), - mods - ) - ) - ) - - override def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement = - field( - id, - labelText, - input( - tpe(inputType), - cls( - "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:max-w-xs sm:text-sm sm:leading-6" - ), - placeholderText.map(placeholder(_)) - ), - helpText - ) - - override def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement = - div( - labelText, - input( - idAttr(id), - nameAttr(id), - autoComplete("date"), - tpe("date"), - cls( - "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ), - placeholderText.map(placeholder(_)), - mods(localDateSelect) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/TailwindUIButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/TailwindUIButtonComponentsModule.scala deleted file mode 100644 index 4c44157..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/TailwindUIButtonComponentsModule.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar -package tailwindui - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait TailwindUIButtonComponentsModule(using ctx: ComponentContext) - extends ButtonComponentsModule: - - override val buttons = new ButtonComponents: - - private inline def srHelp(id: String): Modifier[HtmlElement] = - ctx.messages - .opt(s"form.button.${id}.screenReaderHelp") - .map(sr => span(cls := "sr-only", sr)) - - override def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )(mods: Modifier[HtmlElement]*): HtmlElement = - button( - tpe(buttonType), - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - icon, - text, - mods - ) - - override def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )(mods: Modifier[HtmlElement]*): HtmlElement = - button( - tpe(buttonType), - cls( - "ml-2 bg-white inline-flex justify-center 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" - ), - icon, - text, - mods - ) - - override def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement = - button( - tpe := "button", - cls := "ml-1 inline-flex h-4 w-4 flex-shrink-0 rounded-full p-1 text-gray-400 hover:bg-gray-200 hover:text-gray-500", - srHelp(id), - icon - ) - - override def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement = - button( - tpe := "button", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - icon, - srHelp(id) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/tailwind/laminar/LaminarExtensions.scala b/ui/js/src/main/scala/works/iterative/ui/components/tailwind/laminar/LaminarExtensions.scala index 4c5e68b..4c87800 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/tailwind/laminar/LaminarExtensions.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/tailwind/laminar/LaminarExtensions.scala @@ -2,23 +2,9 @@ package laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.Color import works.iterative.core.UserMessage object LaminarExtensions: - given colorToCSS: Conversion[Color, Modifier[HtmlElement]] with - def apply(c: Color) = cls(c.toCSS) - - given colorToSVGCSS: Conversion[Color, Modifier[SvgElement]] with - def apply(c: Color) = svg.cls(c.toCSS) - - given colorSignalToCSS: Conversion[Signal[Color], Modifier[HtmlElement]] with - def apply(c: Signal[Color]) = cls <-- c.map(_.toCSS) - - given colorSignalToSVGCSS: Conversion[Signal[Color], Modifier[SvgElement]] - with - def apply(c: Signal[Color]) = svg.cls <-- c.map(_.toCSS) - inline given userMessageToModifier(using ctx: ComponentContext ): Conversion[UserMessage, Modifier[HtmlElement]] with diff --git a/ui/js/src/main/scala/works/iterative/ui/scenarios/Scenario.scala b/ui/js/src/main/scala/works/iterative/ui/scenarios/Scenario.scala index 99a212a..60de9f0 100644 --- a/ui/js/src/main/scala/works/iterative/ui/scenarios/Scenario.scala +++ b/ui/js/src/main/scala/works/iterative/ui/scenarios/Scenario.scala @@ -13,8 +13,6 @@ import works.iterative.core.MessageCatalogue import works.iterative.ui.components.tailwind.ComponentContext import works.iterative.ui.components.tailwind.StyleGuide -import works.iterative.ui.model.color.Color -import works.iterative.ui.model.color.ColorWeight object Scenario: type Id = String diff --git a/ui/shared/src/main/scala/works/iterative/ui/model/Tag.scala b/ui/shared/src/main/scala/works/iterative/ui/model/Tag.scala index 7352579..958aa75 100644 --- a/ui/shared/src/main/scala/works/iterative/ui/model/Tag.scala +++ b/ui/shared/src/main/scala/works/iterative/ui/model/Tag.scala @@ -1,10 +1,16 @@ package works.iterative.ui.model -import works.iterative.ui.model.color.ColorKind - -/** Representation of colored string value. +/** Representation of a label or tag. * - * Used generally to represent tags or "labels", eg. some kind of status or - * categorization. + * The UI renderer will recognize this value as tag and render accordingly. The + * value is used to determine both the color of the tag and the text displayed. + * + * @param value + * the value of the tag */ -final case class Tag(value: String, color: ColorKind = ColorKind.gray) +opaque type Tag = String + +object Tag: + def apply(value: String): Tag = value + + extension (tag: Tag) def value: String = tag diff --git a/ui/shared/src/main/scala/works/iterative/ui/model/color/Color.scala b/ui/shared/src/main/scala/works/iterative/ui/model/color/Color.scala deleted file mode 100644 index 09d6e4d..0000000 --- a/ui/shared/src/main/scala/works/iterative/ui/model/color/Color.scala +++ /dev/null @@ -1,40 +0,0 @@ -package works.iterative.ui.model.color - -/** Complete color definition that can be rendered to CSS. - * - * Includes the area, kind and weight of the color. - */ -case class Color(area: ColorArea, color: ColorDef): - def toCSS: String = s"${area.name}-${color.toCSS}" - -object Color: - import ColorDef.given - - def current = ColorDef(ColorKind.current) - def inherit = ColorDef(ColorKind.inherit) - def transp = ColorDef(ColorKind.transp) - def auto = ColorDef(ColorKind.auto) - def black = ColorDef(ColorKind.black) - def white = ColorDef(ColorKind.white) - def slate(weight: ColorWeight) = ColorDef(ColorKind.slate, weight) - def gray(weight: ColorWeight) = ColorDef(ColorKind.gray, weight) - def zinc(weight: ColorWeight) = ColorDef(ColorKind.zinc, weight) - def neutral(weight: ColorWeight) = ColorDef(ColorKind.neutral, weight) - def stone(weight: ColorWeight) = ColorDef(ColorKind.stone, weight) - def red(weight: ColorWeight) = ColorDef(ColorKind.red, weight) - def orange(weight: ColorWeight) = ColorDef(ColorKind.orange, weight) - def amber(weight: ColorWeight) = ColorDef(ColorKind.amber, weight) - def yellow(weight: ColorWeight) = ColorDef(ColorKind.yellow, weight) - def lime(weight: ColorWeight) = ColorDef(ColorKind.lime, weight) - def green(weight: ColorWeight) = ColorDef(ColorKind.green, weight) - def emerald(weight: ColorWeight) = ColorDef(ColorKind.emerald, weight) - def teal(weight: ColorWeight) = ColorDef(ColorKind.teal, weight) - def cyan(weight: ColorWeight) = ColorDef(ColorKind.cyan, weight) - def sky(weight: ColorWeight) = ColorDef(ColorKind.sky, weight) - def blue(weight: ColorWeight) = ColorDef(ColorKind.blue, weight) - def indigo(weight: ColorWeight) = ColorDef(ColorKind.indigo, weight) - def violet(weight: ColorWeight) = ColorDef(ColorKind.violet, weight) - def purple(weight: ColorWeight) = ColorDef(ColorKind.purple, weight) - def fuchsia(weight: ColorWeight) = ColorDef(ColorKind.fuchsia, weight) - def pink(weight: ColorWeight) = ColorDef(ColorKind.pink, weight) - def rose(weight: ColorWeight) = ColorDef(ColorKind.rose, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala index a2c637c..bfa063a 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala @@ -1,13 +1,11 @@ package works.iterative.ui.components.laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorDef import com.raquo.laminar.tags.HtmlTag case class Bin[Source, +Value]( label: String, description: Option[String | HtmlElement], - color: ColorDef, valueOf: Source => Value ): def map[NewValue]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala deleted file mode 100644 index 3aac0bf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala +++ /dev/null @@ -1,34 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ButtonComponentsModule: - def buttons: ButtonComponents - - trait ButtonComponents: - def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala deleted file mode 100644 index 646e73d..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala +++ /dev/null @@ -1,191 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.color.ColorDef -import com.raquo.laminar.tags.HtmlTag -import org.scalajs.dom.html.UList -import com.raquo.laminar.nodes.ReactiveHtmlElement -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind - -trait DashboardComponentsModule: - def dashboard: DashboardComponents - - // Only methods that create HtmlElements - trait DashboardComponents(using ComponentContext): - def section(label: String, children: Modifier[HtmlElement]): Div - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI - def cardInitials( - initials: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div - def count(label: String, value: Int, color: ColorDef): Span - def counts(children: HtmlElement*): Span - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - -trait DefaultDashboardComponentsModule(using ComponentContext) - extends DashboardComponentsModule: - override val dashboard: DashboardComponents = new DashboardComponents: - def section(label: String, children: Modifier[HtmlElement]): Div = - div( - cls("mt-3"), - h2( - cls("text-gray-500 text-xs font-medium uppercase tracking-wide"), - label - ), - children - ) - - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] = - ul( - cls( - "mt-3 grid gap-5 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" - ), - children - ) - - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI = - li( - div( - cls("col-span-1 flex shadow-sm rounded-md"), - initials, - div( - cls( - "flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate" - ), - div( - cls("flex-1 px-4 py-2 text-sm truncate"), - div( - cls("text-gray-900 font-medium hover:text-gray-600 truncate"), - label - ), - p(cls("text-gray-500"), children) - ) - ) - ) - ) - - def cardInitials( - id: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div = - div( - cls( - "flex-shrink-0 flex flex-col items-center justify-center w-16 text-white text-sm font-medium rounded-l-md" - ), - color.map(_(600).bg), - div(id), - children - ) - - def count(label: String, value: Int, color: ColorDef): Span = - span(color.text, title(label), s"${value}") - - def counts(children: HtmlElement*): Span = - span(interleave(children, span(" / "))) - - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div = - div( - cls("flex flex-col mt-2"), - div( - cls( - "align-middle min-w-full shadow sm:rounded-lg" - ), - L.table( - cls("min-w-full border-separate border-spacing-0"), - styleAttr("border-spacing: 0"), // Tailwind somehow doesn't work - thead( - tr( - cls("border-t border-gray-200"), - headers.map(h => - th( - cls( - "sticky top-0 z-10 px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - h - ) - ) - ) - ), - tbody(cls("bg-white divide-y divide-gray-100"), sections) - ) - ) - ) - - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - td( - cls("bg-gray-50 border-gray-200 border-t border-b"), - cls( - "sticky top-14 z-10 bg-gray-50 px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - colSpan(cs), - label - ), - mods - ) - - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - mods, - td( - cls( - "px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900 truncate" - ), - label - ), - children.map(e => - td( - cls( - "whitespace-nowrap px-2 py-2 text-sm text-gray-500" - ), - e - ) - ) - ) - - private def interleave( - a: Seq[HtmlElement], - separator: => HtmlElement - ): Seq[HtmlElement] = - if a.size < 2 then a - else a.init.flatMap(n => Seq(n, separator)) :+ a.last diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala deleted file mode 100644 index a45beaf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.FileRef - -trait DetailComponentsModule: - - def details: DetailComponents - - trait DetailComponents: - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement - - def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement - - def files(fs: List[FileRef]): HtmlElement - - def file(f: FileRef): HtmlElement - -trait DefaultDetailComponentsModule(using ctx: ComponentContext) - extends DetailComponentsModule: - self: IconsModule => - - override val details: DetailComponents = new DetailComponents: - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement = - div( - div( - cls := "px-4 sm:px-0", - h3( - cls := "text-base font-semibold leading-7 text-gray-900", - title - ), - subtitle.map(st => - p( - cls := "mt-1 max-w-2xl text-sm leading-6 text-gray-500", - st - ) - ) - // TODO: actions - ), - div( - cls := "mt-6 border-t border-gray-100", - dl( - cls := "divide-y divide-gray-100", - fields - ) - ) - ) - - override def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0", - dt( - cls := "text-sm font-medium leading-6 text-gray-900", - title - ), - dd( - cls := "mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0", - content - ) - ) - - override def files(fs: List[FileRef]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-100 rounded-md border border-gray-200", - fs.map(file) - ) - - override def file(f: FileRef): HtmlElement = - li( - cls := "flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6", - div( - cls := "flex w-0 flex-1 items-center", - icons.`paper-clip-solid`(), - div( - cls := "ml-4 flex min-w-0 flex-1 gap-2", - span( - cls := "truncate font-medium", - f.name - ), - f.size.map(size => span(cls := "flex-shrink-0 text-gray-400", size)) - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - "Uložit" - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala new file mode 100644 index 0000000..f6c583f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait FormComponents(using ctx: ComponentContext) extends LocalDateSelectModule: + def searchIcon: SvgElement + + def renderLocalDateSelect( + id: String, + labelText: Option[String], + placeholderText: Option[String], + mods: LocalDateSelect => Modifier[HtmlElement] + ): HtmlElement = + div( + labelText, + input( + idAttr(id), + nameAttr(id), + autoComplete("date"), + tpe("date"), + cls( + "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ), + placeholderText.map(placeholder(_)), + mods(localDateSelect) + ) + ) + + def searchField( + id: String, + placeholderText: Option[String] = None + )( + mods: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls := "flex-1 min-w-0", + label(ctx.messages("forms.search.label"), forId(id), cls("sr-only")), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + searchIcon + ), + input( + tpe := "search", + nameAttr := "search", + idAttr := id, + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholderText + .orElse( + ctx.messages + .opt( + s"forms.search.${id}.placeholder", + s"form.search.placeholder" + ) + ) + .map(placeholder(_)), + mods + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala deleted file mode 100644 index 7714490..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala +++ /dev/null @@ -1,82 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -/** Form components - * - * A form is a collection of sections, each of which contains a collection of - * fields. - * - * Form -> * Section -> * Field - * -> * Action - * - * Each Field has an `id`, `label` and `input` element. The `id` is used to - * link the label to the input. - * - * The `input` element can be a simple text input, or a more complex component - * such as a date picker. The caller is expected to make the input work. - */ -trait FormComponentsModule extends LocalDateSelectModule: - def forms: FormComponents - - trait FormComponents: - - /** Layout the sections and actions of a form */ - def form(mods: Modifier[HtmlElement]*)(sections: Modifier[HtmlElement]*)( - actions: Modifier[HtmlElement]* - ): HtmlElement - - /** Section layout, with a title, optional subtitle and any content. */ - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label and any content */ - def field(label: Modifier[HtmlElement])( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label, input and optional help text */ - def field( - id: String, - label: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement - - def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout an inline form element */ - def inlineForm(mods: Modifier[HtmlElement]*): HtmlElement - - /** An input field with a search icon */ - def searchField(id: String, placeholderText: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement - - /** LocalDate input */ - def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala deleted file mode 100644 index 8acbe7f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala +++ /dev/null @@ -1,28 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.Tag - -trait GenericComponentsModule: - - def generic: GenericComponents - - trait GenericComponents: - def tag(name: String, color: ColorKind): HtmlElement - def tag(t: Tag): HtmlElement = tag(t.value, t.color) - -trait DefaultGenericComponentsModule(using ComponentContext) - extends GenericComponentsModule: - override val generic: GenericComponents = new GenericComponents: - override def tag(name: String, color: ColorKind): HtmlElement = - p( - cls( - "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" - ), - color(800).text, - color(100).bg, - name - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala deleted file mode 100644 index 8cfc1d9..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala +++ /dev/null @@ -1,123 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.CustomAttrs - -trait IconsModule: - def icons: Icons - - trait Icons: - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement - def close(mods: Modifier[SvgElement]*): SvgElement - def `search-solid`(mods: Modifier[SvgElement]*): SvgElement - def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement - def `document-chart-bar-outline`(mods: Modifier[SvgElement]*): SvgElement - def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement - -trait DefaultIconsModule(using ComponentContext) extends IconsModule: - override val icons: Icons = new Icons: - import svg.* - - object aria: - val hidden = CustomAttrs.svg.ariaHidden - - private def withDefault( - mods: Seq[Modifier[SvgElement]], - default: Modifier[SvgElement] - ): Modifier[SvgElement] = - if mods.isEmpty then default else mods - - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-8 w-8"), - 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" - ) - ) - ) - - override def close(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-2 w-2"), - stroke := "currentColor", - fill := "none", - viewBox := "0 0 8 8", - path( - strokeLineCap := "round", - strokeWidth := "1.5", - d := "M1 1l6 6m0-6L1 7" - ) - ) - - override def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 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 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") - ) - ) - - override def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 text-gray-400")), - 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") - ) - ) - - override def `document-chart-bar-outline`( - mods: Modifier[SvgElement]* - ): SvgElement = - svg( - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - strokeWidth := "1.5", - stroke := "currentColor", - withDefault(mods, cls := "h-6 w-6"), - path( - strokeLineCap := "round", - strokeLineJoin := "round", - d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" - ) - ) - - override def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-5 w-5"), - cls := "flex-shrink-0 text-gray-400", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", - clipRule := "evenodd" - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala deleted file mode 100644 index 95ab654..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait LayoutModule: - def layout: LayoutComponents - - trait LayoutComponents: - def card(content: Modifier[HtmlElement]*): HtmlElement - -trait DefaultLayoutModule(using ctx: ComponentContext) extends LayoutModule: - override val layout: LayoutComponents = new LayoutComponents: - override def card(content: Modifier[HtmlElement]*): HtmlElement = - div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala deleted file mode 100644 index 5946974..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom.html.Paragraph -import com.raquo.laminar.nodes.TextNode - -trait ListComponentsModule: - - def list: ListComponents - - trait ListComponents(using ComponentContext): - def label(text: String, color: ColorKind): HtmlElement - def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI - def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] - def listSection(header: String, list: HtmlElement): Div - def navigation(sections: Modifier[HtmlElement]): HtmlElement - -trait DefaultListComponentsModule(using ComponentContext) - extends ListComponentsModule: - self: GenericComponentsModule => - - override val list: ListComponents = new ListComponents: - override def label( - text: String, - color: ColorKind - ): HtmlElement = generic.tag(text, color) - - override def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI = - li( - cls("group"), - div( - cls( - "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" - ), - avatar.map(a => - div( - cls("flex-shrink-0"), - div( - cls( - "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" - ), - a - ) - ) - ), - div( - cls("flex-1 min-w-0"), - p( - cls("text-sm font-medium text-gray-900"), - title, - span(cls("float-right"), right) - ), - subtitle.map(st => - p( - cls("text-sm text-gray-500 truncate"), - st - ) - ) - ) - ) - ) - - override def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = - ul( - cls("relative z-0 divide-y divide-gray-200"), - role("list"), - children - ) - - override def listSection( - header: String, - list: HtmlElement - ): Div = - div( - cls("relative"), - div( - cls( - "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" - ), - header - ), - list - ) - - override def navigation(sections: Modifier[HtmlElement]): HtmlElement = - navTag( - cls("flex-1 min-h-0 overflow-y-auto"), - sections - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala deleted file mode 100644 index 5b5490f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ModalComponentsModule: - - def modal: ModalComponents - - trait ModalComponents: - def modalDialog( - content: HtmlElement, - onClose: Modifier[HtmlElement] - ): HtmlElement - -trait DefaultModalComponentsModule(using ctx: ComponentContext) - extends ModalComponentsModule: - - override val modal: ModalComponents = new ModalComponents: - - override def modalDialog( - content: HtmlElement, - closeMod: Modifier[HtmlElement] - ): 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( - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")), - closeMod - ) - ) - - div( - cls("fixed inset-0 z-20 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - 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-7xl sm:align-middle" - ), - content - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala deleted file mode 100644 index b713d8a..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala +++ /dev/null @@ -1,71 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait PageComponentsModule: - - val page: PageComponents - - trait PageComponents: - def container( - children: Modifier[HtmlElement]* - ): HtmlElement - - def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement - - def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement] = emptyMod, - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement - - /** Visage for clickable text, like links - */ - def clickable: Modifier[HtmlElement] - -trait DefaultPageComponentsModule(using ComponentContext) - extends PageComponentsModule: - - override val page: PageComponents = new PageComponents: - override def container( - children: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), - children - ) - - override def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement = - div( - cls("p-8 bg-gray-100 h-full"), - header, - children - ) - - override def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement = - div( - cls("pb-5 border-b border-gray-200"), - div(cls("float-right"), right), - h1( - cls("text-2xl leading-6 font-medium text-gray-900"), - title - ), - subtitle.map( - p( - cls("text-sm font-medium text-gray-500"), - _ - ) - ) - ) - - override def clickable: Modifier[HtmlElement] = - cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala deleted file mode 100644 index f70ee87..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala +++ /dev/null @@ -1,108 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} - -trait TableComponentsModule: - def tables: TableComponents - - trait TableComponents: - def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )(table: Modifier[HtmlElement]*): HtmlElement - - def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement - - def headerRow(mods: Modifier[HtmlElement]*)( - cells: HtmlElement* - ): HtmlElement - - def dataRow(mods: Modifier[HtmlElement]*)(cells: HtmlElement*): HtmlElement - - def headerCell(content: Modifier[HtmlElement]): HtmlElement - - def dataCell(content: Modifier[HtmlElement]): HtmlElement - -trait DefaultTableComponentsModule extends TableComponentsModule: - - override lazy val tables: TableComponents = new TableComponents: - - override def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )( - table: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("px-4 sm:px-6 lg:px-8"), - div( - cls("sm:flex sm:items-center"), - div( - cls("sm:flex-auto"), - h1(cls("text-base font-semibold leading-6 text-gray-900"), title), - subtitle.map(st => p(cls("mt-2 text-sm text-gray-700"), st)) - ), - div(cls("mt-4 sm:ml-16 sm:mt-0 sm:flex-none"), actions) - ), - div( - cls("mt-8 flow-root"), - div( - cls("-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"), - div( - cls("inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"), - table - ) - ) - ) - ) - - override def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement = - table( - cls("min-w-full divide-y divide-gray-300"), - thead(header), - tbody(cls("divide-y divide-gray-200"), body) - ) - - override def headerRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-3.5 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-3.5 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-3.5 px-3")) - ) - ) - - override def dataRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - mods, - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-4 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-4 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-4 px-3")) - ) - ) - - override def headerCell(content: Modifier[HtmlElement]): HtmlElement = - th( - cls("text-left text-sm font-semibold text-gray-900"), - content - ) - - override def dataCell(content: Modifier[HtmlElement]): HtmlElement = - td( - cls("whitespace-nowrap text-sm text-gray-500"), - content - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala index b937d45..12d01ae 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala @@ -3,7 +3,7 @@ import com.raquo.laminar.api.L.{*, given} import works.iterative.ui.components.laminar.HtmlTabular import works.iterative.ui.model.tables.Tabular -import works.iterative.ui.components.laminar.TableComponentsModule +import works.iterative.ui.components.laminar.tailwind.ui.TableComponentsModule import works.iterative.core.UserMessage import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given import works.iterative.ui.components.tailwind.ComponentContext diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala new file mode 100644 index 0000000..87f3e8d --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala @@ -0,0 +1,54 @@ +package works.iterative.ui.components.laminar.tailwind.color + +import com.raquo.laminar.api.L.{*, given} + +/** Complete color definition that can be rendered to CSS. + * + * Includes the area, kind and weight of the color. + */ +case class Color(area: ColorArea, color: ColorDef): + def toCSS: String = s"${area.name}-${color.toCSS}" + +object Color: + import ColorDef.given + + given colorToCSS: Conversion[Color, HtmlMod] with + def apply(c: Color) = cls(c.toCSS) + + given colorToSVGCSS: Conversion[Color, SvgMod] with + def apply(c: Color) = svg.cls(c.toCSS) + + given colorSignalToCSS: Conversion[Signal[Color], HtmlMod] with + def apply(c: Signal[Color]) = cls <-- c.map(_.toCSS) + + given colorSignalToSVGCSS: Conversion[Signal[Color], SvgMod] with + def apply(c: Signal[Color]) = svg.cls <-- c.map(_.toCSS) + + def current = ColorDef(ColorKind.current) + def inherit = ColorDef(ColorKind.inherit) + def transp = ColorDef(ColorKind.transp) + def auto = ColorDef(ColorKind.auto) + def black = ColorDef(ColorKind.black) + def white = ColorDef(ColorKind.white) + def slate(weight: ColorWeight) = ColorDef(ColorKind.slate, weight) + def gray(weight: ColorWeight) = ColorDef(ColorKind.gray, weight) + def zinc(weight: ColorWeight) = ColorDef(ColorKind.zinc, weight) + def neutral(weight: ColorWeight) = ColorDef(ColorKind.neutral, weight) + def stone(weight: ColorWeight) = ColorDef(ColorKind.stone, weight) + def red(weight: ColorWeight) = ColorDef(ColorKind.red, weight) + def orange(weight: ColorWeight) = ColorDef(ColorKind.orange, weight) + def amber(weight: ColorWeight) = ColorDef(ColorKind.amber, weight) + def yellow(weight: ColorWeight) = ColorDef(ColorKind.yellow, weight) + def lime(weight: ColorWeight) = ColorDef(ColorKind.lime, weight) + def green(weight: ColorWeight) = ColorDef(ColorKind.green, weight) + def emerald(weight: ColorWeight) = ColorDef(ColorKind.emerald, weight) + def teal(weight: ColorWeight) = ColorDef(ColorKind.teal, weight) + def cyan(weight: ColorWeight) = ColorDef(ColorKind.cyan, weight) + def sky(weight: ColorWeight) = ColorDef(ColorKind.sky, weight) + def blue(weight: ColorWeight) = ColorDef(ColorKind.blue, weight) + def indigo(weight: ColorWeight) = ColorDef(ColorKind.indigo, weight) + def violet(weight: ColorWeight) = ColorDef(ColorKind.violet, weight) + def purple(weight: ColorWeight) = ColorDef(ColorKind.purple, weight) + def fuchsia(weight: ColorWeight) = ColorDef(ColorKind.fuchsia, weight) + def pink(weight: ColorWeight) = ColorDef(ColorKind.pink, weight) + def rose(weight: ColorWeight) = ColorDef(ColorKind.rose, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala new file mode 100644 index 0000000..4130840 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala @@ -0,0 +1,16 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines the area the color should apply to, eg. background, text, border, + * etc. + */ +enum ColorArea(val name: String): + case bg extends ColorArea("bg") + case text extends ColorArea("text") + case decoration extends ColorArea("decoration") + case border extends ColorArea("border") + case outline extends ColorArea("outline") + case divide extends ColorArea("divide") + case ring extends ColorArea("ring") + case ringOffset extends ColorArea("ring-offset") + case shadow extends ColorArea("shadow") + case accent extends ColorArea("accent") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala new file mode 100644 index 0000000..fab9404 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala @@ -0,0 +1,43 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** A combination of ColorKind and ColorWeight, if applicable. + * + * By applying area we get the full Color definition. + */ +sealed trait ColorDef: + def toCSS: String + + def bg = Color(ColorArea.bg, this) + def text = Color(ColorArea.text, this) + def decoration = Color(ColorArea.decoration, this) + def border = Color(ColorArea.border, this) + def outline = Color(ColorArea.outline, this) + def divide = Color(ColorArea.divide, this) + def ring = Color(ColorArea.ring, this) + def ringOffset = Color(ColorArea.ringOffset, this) + def shadow = Color(ColorArea.shadow, this) + def accent = Color(ColorArea.accent, this) + +// TODO: create a macro that will output the tailwind class as string directly during compilation, where possible. +object ColorDef: + case class WeightedColorDef( + kind: ColorKind, + weight: ColorWeight + ) extends ColorDef: + override def toCSS: String = s"${kind.name}-${weight.value}" + + case class UnweightedColorDef( + kind: ColorKind + ) extends ColorDef: + override def toCSS: String = kind.name + + // TODO: check that the kind is valid unweighted kind + // that means current, inherit, auto, transparent, black, white + // tried using implicit evidence, but the type inference for enumerations + // tends to generalize to the enum, instead of the real type + def apply[T <: ColorKind](kind: T)(using + ev: T <:< ColorKind.Unweighted + ): ColorDef = + UnweightedColorDef(kind) + def apply(kind: ColorKind, weight: ColorWeight): ColorDef = + WeightedColorDef(kind, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala new file mode 100644 index 0000000..865166b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala @@ -0,0 +1,47 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines what color should be used, without specifying the area or weight. + */ +sealed abstract class ColorKind private (val name: String): + def apply(weight: ColorWeight): ColorDef = + ColorDef.WeightedColorDef(this, weight) + +object ColorKind: + trait Unweighted: + self: ColorKind => + override def apply(weight: ColorWeight): ColorDef = + ColorDef.UnweightedColorDef(self) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case object current extends ColorKind("current") with Unweighted + case object inherit extends ColorKind("inherit") with Unweighted + // Not present in for all methods + case object transp extends ColorKind("transparent") with Unweighted + // Seen in accent, not preset otherwise + case object auto extends ColorKind("auto") with Unweighted + // Black and white do not have weight + case object black extends ColorKind("black") with Unweighted + case object white extends ColorKind("white") with Unweighted + case object slate extends ColorKind("slate") + case object gray extends ColorKind("gray") + case object zinc extends ColorKind("zinc") + case object neutral extends ColorKind("neutral") + case object stone extends ColorKind("stone") + case object red extends ColorKind("red") + case object orange extends ColorKind("orange") + case object amber extends ColorKind("amber") + case object yellow extends ColorKind("yellow") + case object lime extends ColorKind("lime") + case object green extends ColorKind("green") + case object emerald extends ColorKind("emerald") + case object teal extends ColorKind("teal") + case object cyan extends ColorKind("cyan") + case object sky extends ColorKind("sky") + case object blue extends ColorKind("blue") + case object indigo extends ColorKind("indigo") + case object violet extends ColorKind("violet") + case object purple extends ColorKind("purple") + case object fuchsia extends ColorKind("fuchsia") + case object pink extends ColorKind("pink") + case object rose extends ColorKind("rose") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala new file mode 100644 index 0000000..6f1a4cc --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala @@ -0,0 +1,33 @@ +package works.iterative.ui.components.laminar.tailwind.color + +opaque type ColorWeight = String + +extension (c: ColorWeight) def value: String = c + +/** Defines weight of a color, eg. 50, 100, 200, etc. + * + * Tailwind-like. + */ +object ColorWeight: + inline given int50: Conversion[50, ColorWeight] with + inline def apply(i: 50) = "50" + inline given int100: Conversion[100, ColorWeight] with + inline def apply(i: 100) = "100" + inline given int200: Conversion[200, ColorWeight] with + inline def apply(i: 200) = "200" + inline given int300: Conversion[300, ColorWeight] with + inline def apply(i: 300) = "300" + inline given int400: Conversion[400, ColorWeight] with + inline def apply(i: 400) = "400" + inline given int500: Conversion[500, ColorWeight] with + inline def apply(i: 500) = "500" + inline given int600: Conversion[600, ColorWeight] with + inline def apply(i: 600) = "600" + inline given int700: Conversion[700, ColorWeight] with + inline def apply(i: 700) = "700" + inline given int800: Conversion[800, ColorWeight] with + inline def apply(i: 800) = "800" + inline given int900: Conversion[900, ColorWeight] with + inline def apply(i: 900) = "900" + inline given int950: Conversion[950, ColorWeight] with + inline def apply(i: 950) = "950" diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala new file mode 100644 index 0000000..21e21f0 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala @@ -0,0 +1,12 @@ +package works.iterative.ui.components.laminar.tailwind + +/** We need a generic Color model that can be used both on server and on client. + * + * We have adopted the Tailwind model for now. There is nothing inherently + * Tailwind-specific in the implementation, but all the values are taken from + * their palette and the area model is very HTML biased. + * + * Still, I think that it is a good starting point for exploration and will + * satisfy our current needs. + */ +package object color {} diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" new file mode 100644 index 0000000..5c4e218 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" @@ -0,0 +1,21 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.model.color.ColorKind +import works.iterative.ui.components.tailwind.ComponentContext +import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given +import works.iterative.ui.model.Tag + +trait TailwindUIBadgesComponentsModule(using ComponentContext): + + object badges: + override def tag(name: String, color: ColorKind): HtmlElement = + p( + cls( + "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" + ), + color(800).text, + color(100).bg, + name + ) diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" new file mode 100644 index 0000000..0c9ca25 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" @@ -0,0 +1,24 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} + +trait TailwindUIHeadingsComponentsModule: + val headings: HeadingsComponents = new HeadingsComponents + + class HeadingsComponents: + def section(title: Node, actions: HtmlElement*): HtmlElement = + div( + cls("border-b border-gray-200 pb-5"), + div( + cls("sm:flex sm:items-center sm:justify-between"), + h3(cls("text-base font-semibold leading-6 text-gray-900"), title), + div( + cls("mt-3 flex sm:ml-4 sm:mt-0"), + actions match + case Nil => emptyMod + case first :: rest => + first :: rest.map(_.amend(cls("ml-3"))) + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala new file mode 100644 index 0000000..16ca01b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala @@ -0,0 +1,19 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.laminar.tailwind.color.ColorKind + +trait BadgeComponentsModule: + + object badges: + def pill(name: String, color: ColorKind): HtmlElement = + p( + cls( + "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" + ), + color(800).text, + color(100).bg, + name + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala new file mode 100644 index 0000000..ebb2968 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} + +trait ButtonComponentsModule: + + object buttons: + + private inline def srHelp(text: String): Modifier[HtmlElement] = + span(cls := "sr-only", text) + + def primaryButton( + id: String, + text: Modifier[HtmlElement], + icon: Option[SvgElement] = None, + buttonType: String = "submit" + )(mods: Modifier[HtmlElement]*): HtmlElement = + button( + tpe(buttonType), + cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + icon, + text, + mods + ) + + def secondaryButton( + id: String, + text: Modifier[HtmlElement], + icon: Option[SvgElement] = None, + buttonType: String = "button" + )(mods: Modifier[HtmlElement]*): HtmlElement = + button( + tpe(buttonType), + cls( + "ml-2 bg-white inline-flex justify-center 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" + ), + icon, + text, + mods + ) + + def inlineButton(id: String, icon: SvgElement)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + button( + tpe := "button", + cls := "ml-1 inline-flex h-4 w-4 flex-shrink-0 rounded-full p-1 text-gray-400 hover:bg-gray-200 hover:text-gray-500", + srHelp(id), + icon + ) + + def iconButton(id: String, icon: SvgElement, srText: Option[String] = None)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + button( + tpe := "button", + cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", + icon, + srText.map(srHelp(_)) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala new file mode 100644 index 0000000..eb6dd5f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala @@ -0,0 +1,101 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import org.scalajs.dom.html +import com.raquo.laminar.modifiers.KeyUpdater + +trait FormComponentsModule: + self: IconsModule => + + object forms: + + def section( + title: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] + )(content: Modifier[HtmlElement]*): HtmlElement = + div( + cls("space-y-6 sm:space-y-5"), + div( + h3(cls("text-lg leading-6 font-medium text-gray-900"), title), + subtitle.map(st => p(cls("mt-1 max-w-2xl text-sm text-gray-500"), st)) + ), + div(cls("mt-6 sm:mt-5 space-y-6 sm:space-y-5"), content) + ) + + def label(labelText: String, forId: Option[String] = None)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + L.label( + cls("block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"), + forId.map(id => L.forId(id)), + labelText, + mods + ) + + def field( + label: Modifier[HtmlElement] + )(content: Modifier[HtmlElement]*): 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, + div(cls("mt-1 sm:mt-0 sm:col-span-2"), content) + ) + + def field( + id: String, + labelText: String, + input: HtmlElement, + help: Option[String] + ): HtmlElement = + field( + label(labelText, Some(id))() + )( + input.amend(idAttr(id)), + help.map(h => p(cls("mt-2 text-sm text-gray-500"), h)) + ) + + def form(mods: Modifier[HtmlElement]*)( + sections: Modifier[HtmlElement]* + )(actions: Modifier[HtmlElement]*): HtmlElement = + L.form( + cls("space-y-8 divide-y divide-gray-200"), + mods, + sections, + div( + cls("pt-5"), + div(cls("flex justify-end"), actions) + ) + ) + + def inlineForm( + mods: Modifier[HtmlElement]* + ): HtmlElement = + L.form(cls("flex space-x-4"), mods) + + def inputField( + id: String, + labelText: String, + placeholderText: Option[String] = None, + inputType: String = "text", + helpText: Option[String] = None + ): HtmlElement = + field( + id, + labelText, + input( + tpe(inputType), + cls( + "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:max-w-xs sm:text-sm sm:leading-6" + ), + placeholderText.map(placeholder(_)) + ), + helpText + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala new file mode 100644 index 0000000..52da07e --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala @@ -0,0 +1,22 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} + +trait HeadingsComponentsModule: + object headings: + def section(title: Node, actions: HtmlElement*): HtmlElement = + div( + cls("border-b border-gray-200 pb-5"), + div( + cls("sm:flex sm:items-center sm:justify-between"), + h3(cls("text-base font-semibold leading-6 text-gray-900"), title), + div( + cls("mt-3 flex sm:ml-4 sm:mt-0"), + actions match + case Nil => emptyMod + case first :: rest => + first :: rest.map(_.amend(cls("ml-3"))) + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala new file mode 100644 index 0000000..c543150 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala @@ -0,0 +1,112 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext +import works.iterative.ui.components.tailwind.CustomAttrs + +trait IconsModule: + object icons: + import svg.* + + object aria: + val hidden = CustomAttrs.svg.ariaHidden + + private def withDefault( + mods: Seq[Modifier[SvgElement]], + default: Modifier[SvgElement] + ): Modifier[SvgElement] = + if mods.isEmpty then default else mods + + def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-8 w-8"), + 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" + ) + ) + ) + + def close(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-2 w-2"), + stroke := "currentColor", + fill := "none", + viewBox := "0 0 8 8", + path( + strokeLineCap := "round", + strokeWidth := "1.5", + d := "M1 1l6 6m0-6L1 7" + ) + ) + + def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls("h-5 w-5 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 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") + ) + ) + + def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls("h-5 w-5 text-gray-400")), + 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") + ) + ) + + def `document-chart-bar-outline`( + mods: Modifier[SvgElement]* + ): SvgElement = + svg( + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + strokeWidth := "1.5", + stroke := "currentColor", + withDefault(mods, cls := "h-6 w-6"), + path( + strokeLineCap := "round", + strokeLineJoin := "round", + d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" + ) + ) + + def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-5 w-5"), + cls := "flex-shrink-0 text-gray-400", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", + clipRule := "evenodd" + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala new file mode 100644 index 0000000..02cbdb6 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala @@ -0,0 +1,11 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait LayoutModule: + object layout: + def card(content: Modifier[HtmlElement]*): HtmlElement = + div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala new file mode 100644 index 0000000..b652da9 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala @@ -0,0 +1,88 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.laminar.nodes.ReactiveHtmlElement +import org.scalajs.dom.html.Paragraph +import com.raquo.laminar.nodes.TextNode +import works.iterative.ui.components.laminar.tailwind.color.ColorKind + +trait ListComponentsModule: + self: BadgeComponentsModule => + + object list: + def label( + text: String, + color: ColorKind + ): HtmlElement = badges.pill(text, color) + + def item( + title: String, + subtitle: Option[String], + right: Modifier[HtmlElement] = emptyMod, + avatar: Option[Modifier[HtmlElement]] = None + ): LI = + li( + cls("group"), + div( + cls( + "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" + ), + avatar.map(a => + div( + cls("flex-shrink-0"), + div( + cls( + "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" + ), + a + ) + ) + ), + div( + cls("flex-1 min-w-0"), + p( + cls("text-sm font-medium text-gray-900"), + title, + span(cls("float-right"), right) + ), + subtitle.map(st => + p( + cls("text-sm text-gray-500 truncate"), + st + ) + ) + ) + ) + ) + + def unordered( + children: Modifier[HtmlElement] + ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = + ul( + cls("relative z-0 divide-y divide-gray-200"), + role("list"), + children + ) + + def listSection( + header: String, + list: HtmlElement + ): Div = + div( + cls("relative"), + div( + cls( + "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" + ), + header + ), + list + ) + + def navigation(sections: Modifier[HtmlElement]): HtmlElement = + navTag( + cls("flex-1 min-h-0 overflow-y-auto"), + sections + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ModalComponentsModule.scala new file mode 100644 index 0000000..6027404 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ModalComponentsModule.scala @@ -0,0 +1,54 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait ModalComponentsModule: + object modal: + def modalDialog( + content: HtmlElement, + closeMod: Modifier[HtmlElement] + ): 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( + cls("fixed inset-0 transition-opacity"), + div(cls("absolute inset-0 bg-gray-500 opacity-75")), + closeMod + ) + ) + + div( + cls("fixed inset-0 z-20 overflow-y-auto"), + div( + cls("text-center sm:block sm:p-0"), + overlay, + browserCenteringModalTrick, + 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-7xl sm:align-middle" + ), + content + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/PageComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/PageComponentsModule.scala new file mode 100644 index 0000000..d5a422e --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/PageComponentsModule.scala @@ -0,0 +1,49 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait PageComponentsModule: + + object page: + def container( + children: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), + children + ) + + def singleColumn( + header: Modifier[HtmlElement] + )(children: Modifier[HtmlElement]*): HtmlElement = + div( + cls("p-8 bg-gray-100 h-full"), + header, + children + ) + + def pageHeader( + title: Modifier[HtmlElement], + right: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] = None + ): HtmlElement = + div( + cls("pb-5 border-b border-gray-200"), + div(cls("float-right"), right), + h1( + cls("text-2xl leading-6 font-medium text-gray-900"), + title + ), + subtitle.map( + p( + cls("text-sm font-medium text-gray-500"), + _ + ) + ) + ) + + def clickable: Modifier[HtmlElement] = + cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TableComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TableComponentsModule.scala new file mode 100644 index 0000000..417ea56 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TableComponentsModule.scala @@ -0,0 +1,86 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L +import com.raquo.laminar.api.L.{*, given} + +trait TableComponentsModule: + + object tables: + + def tableSection( + title: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] = None, + actions: Modifier[HtmlElement]* + )( + table: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls("px-4 sm:px-6 lg:px-8"), + div( + cls("sm:flex sm:items-center"), + div( + cls("sm:flex-auto"), + h1(cls("text-base font-semibold leading-6 text-gray-900"), title), + subtitle.map(st => p(cls("mt-2 text-sm text-gray-700"), st)) + ), + div(cls("mt-4 sm:ml-16 sm:mt-0 sm:flex-none"), actions) + ), + div( + cls("mt-8 flow-root"), + div( + cls("-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"), + div( + cls("inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"), + table + ) + ) + ) + ) + + def simpleTable(header: Modifier[HtmlElement]*)( + body: Modifier[HtmlElement]* + ): HtmlElement = + table( + cls("min-w-full divide-y divide-gray-300"), + thead(header), + tbody(cls("divide-y divide-gray-200"), body) + ) + + def headerRow( + mods: Modifier[HtmlElement]* + )(cells: HtmlElement*): HtmlElement = + tr( + cells.zipWithIndex.map((c, i) => + if i == 0 then c.amend(cls("py-3.5 pl-4 pr-3 sm:pl-0")) + else if i == cells.length - 1 then + c.amend(cls("py-3.5 pr-4 pl-3 sm:pr-0")) + else c.amend(cls("py-3.5 px-3")) + ) + ) + + def dataRow( + mods: Modifier[HtmlElement]* + )(cells: HtmlElement*): HtmlElement = + tr( + mods, + cells.zipWithIndex.map((c, i) => + if i == 0 then c.amend(cls("py-4 pl-4 pr-3 sm:pl-0")) + else if i == cells.length - 1 then + c.amend(cls("py-4 pr-4 pl-3 sm:pr-0")) + else c.amend(cls("py-4 px-3")) + ) + ) + + def headerCell(content: Modifier[HtmlElement]): HtmlElement = + th( + cls("text-left text-sm font-semibold text-gray-900"), + content + ) + + def dataCell(content: Modifier[HtmlElement]): HtmlElement = + td( + cls("whitespace-nowrap text-sm text-gray-500"), + content + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TailwindUICatalogue.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TailwindUICatalogue.scala new file mode 100644 index 0000000..5027b10 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TailwindUICatalogue.scala @@ -0,0 +1,17 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} + +trait TailwindUICatalogueModule + extends BadgeComponentsModule + with ButtonComponentsModule + with HeadingsComponentsModule + with IconsModule + with FormComponentsModule + with LayoutModule + with ListComponentsModule + with ModalComponentsModule + with PageComponentsModule + with TableComponentsModule diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/LabelsOnLeftFormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/LabelsOnLeftFormComponentsModule.scala deleted file mode 100644 index 210bbf8..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/LabelsOnLeftFormComponentsModule.scala +++ /dev/null @@ -1,155 +0,0 @@ -package works.iterative.ui.components.laminar -package tailwindui - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -trait LabelsOnLeftFormComponentsModule(using ctx: ComponentContext) - extends FormComponentsModule: - self: IconsModule => - override val forms = new FormComponents: - - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )(content: Modifier[HtmlElement]*): HtmlElement = - div( - cls("space-y-6 sm:space-y-5"), - div( - h3(cls("text-lg leading-6 font-medium text-gray-900"), title), - subtitle.map(st => p(cls("mt-1 max-w-2xl text-sm text-gray-500"), st)) - ), - div(cls("mt-6 sm:mt-5 space-y-6 sm:space-y-5"), content) - ) - - override def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement = - L.label( - cls("block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"), - forId.map(id => L.forId(id)), - labelText, - mods - ) - - override def field( - label: Modifier[HtmlElement] - )(content: Modifier[HtmlElement]*): 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, - div(cls("mt-1 sm:mt-0 sm:col-span-2"), content) - ) - - override def field( - id: String, - labelText: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement = - field( - label(labelText, Some(id))() - )( - input.amend(idAttr(id)), - help.map(h => p(cls("mt-2 text-sm text-gray-500"), h)) - ) - - override def form(mods: Modifier[HtmlElement]*)( - sections: Modifier[HtmlElement]* - )(actions: Modifier[HtmlElement]*): HtmlElement = - L.form( - cls("space-y-8 divide-y divide-gray-200"), - mods, - sections, - div( - cls("pt-5"), - div(cls("flex justify-end"), actions) - ) - ) - - override def inlineForm( - mods: Modifier[HtmlElement]* - ): HtmlElement = - L.form(cls("flex space-x-4"), mods) - - override def searchField( - id: String, - placeholderText: Option[String] = None - )( - mods: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "flex-1 min-w-0", - label(ctx.messages("forms.search.label"), Some(id))(cls("sr-only")), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - icons.`search-solid`() - ), - input( - tpe := "search", - nameAttr := "search", - idAttr := id, - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholderText - .orElse( - ctx.messages - .opt( - s"forms.search.${id}.placeholder", - s"form.search.placeholder" - ) - ) - .map(placeholder(_)), - mods - ) - ) - ) - - override def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement = - field( - id, - labelText, - input( - tpe(inputType), - cls( - "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:max-w-xs sm:text-sm sm:leading-6" - ), - placeholderText.map(placeholder(_)) - ), - helpText - ) - - override def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement = - div( - labelText, - input( - idAttr(id), - nameAttr(id), - autoComplete("date"), - tpe("date"), - cls( - "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ), - placeholderText.map(placeholder(_)), - mods(localDateSelect) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/TailwindUIButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/TailwindUIButtonComponentsModule.scala deleted file mode 100644 index 4c44157..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/TailwindUIButtonComponentsModule.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar -package tailwindui - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait TailwindUIButtonComponentsModule(using ctx: ComponentContext) - extends ButtonComponentsModule: - - override val buttons = new ButtonComponents: - - private inline def srHelp(id: String): Modifier[HtmlElement] = - ctx.messages - .opt(s"form.button.${id}.screenReaderHelp") - .map(sr => span(cls := "sr-only", sr)) - - override def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )(mods: Modifier[HtmlElement]*): HtmlElement = - button( - tpe(buttonType), - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - icon, - text, - mods - ) - - override def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )(mods: Modifier[HtmlElement]*): HtmlElement = - button( - tpe(buttonType), - cls( - "ml-2 bg-white inline-flex justify-center 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" - ), - icon, - text, - mods - ) - - override def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement = - button( - tpe := "button", - cls := "ml-1 inline-flex h-4 w-4 flex-shrink-0 rounded-full p-1 text-gray-400 hover:bg-gray-200 hover:text-gray-500", - srHelp(id), - icon - ) - - override def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement = - button( - tpe := "button", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - icon, - srHelp(id) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/tailwind/laminar/LaminarExtensions.scala b/ui/js/src/main/scala/works/iterative/ui/components/tailwind/laminar/LaminarExtensions.scala index 4c5e68b..4c87800 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/tailwind/laminar/LaminarExtensions.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/tailwind/laminar/LaminarExtensions.scala @@ -2,23 +2,9 @@ package laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.Color import works.iterative.core.UserMessage object LaminarExtensions: - given colorToCSS: Conversion[Color, Modifier[HtmlElement]] with - def apply(c: Color) = cls(c.toCSS) - - given colorToSVGCSS: Conversion[Color, Modifier[SvgElement]] with - def apply(c: Color) = svg.cls(c.toCSS) - - given colorSignalToCSS: Conversion[Signal[Color], Modifier[HtmlElement]] with - def apply(c: Signal[Color]) = cls <-- c.map(_.toCSS) - - given colorSignalToSVGCSS: Conversion[Signal[Color], Modifier[SvgElement]] - with - def apply(c: Signal[Color]) = svg.cls <-- c.map(_.toCSS) - inline given userMessageToModifier(using ctx: ComponentContext ): Conversion[UserMessage, Modifier[HtmlElement]] with diff --git a/ui/js/src/main/scala/works/iterative/ui/scenarios/Scenario.scala b/ui/js/src/main/scala/works/iterative/ui/scenarios/Scenario.scala index 99a212a..60de9f0 100644 --- a/ui/js/src/main/scala/works/iterative/ui/scenarios/Scenario.scala +++ b/ui/js/src/main/scala/works/iterative/ui/scenarios/Scenario.scala @@ -13,8 +13,6 @@ import works.iterative.core.MessageCatalogue import works.iterative.ui.components.tailwind.ComponentContext import works.iterative.ui.components.tailwind.StyleGuide -import works.iterative.ui.model.color.Color -import works.iterative.ui.model.color.ColorWeight object Scenario: type Id = String diff --git a/ui/shared/src/main/scala/works/iterative/ui/model/Tag.scala b/ui/shared/src/main/scala/works/iterative/ui/model/Tag.scala index 7352579..958aa75 100644 --- a/ui/shared/src/main/scala/works/iterative/ui/model/Tag.scala +++ b/ui/shared/src/main/scala/works/iterative/ui/model/Tag.scala @@ -1,10 +1,16 @@ package works.iterative.ui.model -import works.iterative.ui.model.color.ColorKind - -/** Representation of colored string value. +/** Representation of a label or tag. * - * Used generally to represent tags or "labels", eg. some kind of status or - * categorization. + * The UI renderer will recognize this value as tag and render accordingly. The + * value is used to determine both the color of the tag and the text displayed. + * + * @param value + * the value of the tag */ -final case class Tag(value: String, color: ColorKind = ColorKind.gray) +opaque type Tag = String + +object Tag: + def apply(value: String): Tag = value + + extension (tag: Tag) def value: String = tag diff --git a/ui/shared/src/main/scala/works/iterative/ui/model/color/Color.scala b/ui/shared/src/main/scala/works/iterative/ui/model/color/Color.scala deleted file mode 100644 index 09d6e4d..0000000 --- a/ui/shared/src/main/scala/works/iterative/ui/model/color/Color.scala +++ /dev/null @@ -1,40 +0,0 @@ -package works.iterative.ui.model.color - -/** Complete color definition that can be rendered to CSS. - * - * Includes the area, kind and weight of the color. - */ -case class Color(area: ColorArea, color: ColorDef): - def toCSS: String = s"${area.name}-${color.toCSS}" - -object Color: - import ColorDef.given - - def current = ColorDef(ColorKind.current) - def inherit = ColorDef(ColorKind.inherit) - def transp = ColorDef(ColorKind.transp) - def auto = ColorDef(ColorKind.auto) - def black = ColorDef(ColorKind.black) - def white = ColorDef(ColorKind.white) - def slate(weight: ColorWeight) = ColorDef(ColorKind.slate, weight) - def gray(weight: ColorWeight) = ColorDef(ColorKind.gray, weight) - def zinc(weight: ColorWeight) = ColorDef(ColorKind.zinc, weight) - def neutral(weight: ColorWeight) = ColorDef(ColorKind.neutral, weight) - def stone(weight: ColorWeight) = ColorDef(ColorKind.stone, weight) - def red(weight: ColorWeight) = ColorDef(ColorKind.red, weight) - def orange(weight: ColorWeight) = ColorDef(ColorKind.orange, weight) - def amber(weight: ColorWeight) = ColorDef(ColorKind.amber, weight) - def yellow(weight: ColorWeight) = ColorDef(ColorKind.yellow, weight) - def lime(weight: ColorWeight) = ColorDef(ColorKind.lime, weight) - def green(weight: ColorWeight) = ColorDef(ColorKind.green, weight) - def emerald(weight: ColorWeight) = ColorDef(ColorKind.emerald, weight) - def teal(weight: ColorWeight) = ColorDef(ColorKind.teal, weight) - def cyan(weight: ColorWeight) = ColorDef(ColorKind.cyan, weight) - def sky(weight: ColorWeight) = ColorDef(ColorKind.sky, weight) - def blue(weight: ColorWeight) = ColorDef(ColorKind.blue, weight) - def indigo(weight: ColorWeight) = ColorDef(ColorKind.indigo, weight) - def violet(weight: ColorWeight) = ColorDef(ColorKind.violet, weight) - def purple(weight: ColorWeight) = ColorDef(ColorKind.purple, weight) - def fuchsia(weight: ColorWeight) = ColorDef(ColorKind.fuchsia, weight) - def pink(weight: ColorWeight) = ColorDef(ColorKind.pink, weight) - def rose(weight: ColorWeight) = ColorDef(ColorKind.rose, weight) diff --git a/ui/shared/src/main/scala/works/iterative/ui/model/color/ColorArea.scala b/ui/shared/src/main/scala/works/iterative/ui/model/color/ColorArea.scala deleted file mode 100644 index 1211287..0000000 --- a/ui/shared/src/main/scala/works/iterative/ui/model/color/ColorArea.scala +++ /dev/null @@ -1,16 +0,0 @@ -package works.iterative.ui.model.color - -/** Defines the area the color should apply to, eg. background, text, border, - * etc. - */ -enum ColorArea(val name: String): - case bg extends ColorArea("bg") - case text extends ColorArea("text") - case decoration extends ColorArea("decoration") - case border extends ColorArea("border") - case outline extends ColorArea("outline") - case divide extends ColorArea("divide") - case ring extends ColorArea("ring") - case ringOffset extends ColorArea("ring-offset") - case shadow extends ColorArea("shadow") - case accent extends ColorArea("accent") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala index a2c637c..bfa063a 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala @@ -1,13 +1,11 @@ package works.iterative.ui.components.laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorDef import com.raquo.laminar.tags.HtmlTag case class Bin[Source, +Value]( label: String, description: Option[String | HtmlElement], - color: ColorDef, valueOf: Source => Value ): def map[NewValue]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala deleted file mode 100644 index 3aac0bf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala +++ /dev/null @@ -1,34 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ButtonComponentsModule: - def buttons: ButtonComponents - - trait ButtonComponents: - def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala deleted file mode 100644 index 646e73d..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala +++ /dev/null @@ -1,191 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.color.ColorDef -import com.raquo.laminar.tags.HtmlTag -import org.scalajs.dom.html.UList -import com.raquo.laminar.nodes.ReactiveHtmlElement -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind - -trait DashboardComponentsModule: - def dashboard: DashboardComponents - - // Only methods that create HtmlElements - trait DashboardComponents(using ComponentContext): - def section(label: String, children: Modifier[HtmlElement]): Div - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI - def cardInitials( - initials: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div - def count(label: String, value: Int, color: ColorDef): Span - def counts(children: HtmlElement*): Span - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - -trait DefaultDashboardComponentsModule(using ComponentContext) - extends DashboardComponentsModule: - override val dashboard: DashboardComponents = new DashboardComponents: - def section(label: String, children: Modifier[HtmlElement]): Div = - div( - cls("mt-3"), - h2( - cls("text-gray-500 text-xs font-medium uppercase tracking-wide"), - label - ), - children - ) - - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] = - ul( - cls( - "mt-3 grid gap-5 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" - ), - children - ) - - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI = - li( - div( - cls("col-span-1 flex shadow-sm rounded-md"), - initials, - div( - cls( - "flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate" - ), - div( - cls("flex-1 px-4 py-2 text-sm truncate"), - div( - cls("text-gray-900 font-medium hover:text-gray-600 truncate"), - label - ), - p(cls("text-gray-500"), children) - ) - ) - ) - ) - - def cardInitials( - id: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div = - div( - cls( - "flex-shrink-0 flex flex-col items-center justify-center w-16 text-white text-sm font-medium rounded-l-md" - ), - color.map(_(600).bg), - div(id), - children - ) - - def count(label: String, value: Int, color: ColorDef): Span = - span(color.text, title(label), s"${value}") - - def counts(children: HtmlElement*): Span = - span(interleave(children, span(" / "))) - - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div = - div( - cls("flex flex-col mt-2"), - div( - cls( - "align-middle min-w-full shadow sm:rounded-lg" - ), - L.table( - cls("min-w-full border-separate border-spacing-0"), - styleAttr("border-spacing: 0"), // Tailwind somehow doesn't work - thead( - tr( - cls("border-t border-gray-200"), - headers.map(h => - th( - cls( - "sticky top-0 z-10 px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - h - ) - ) - ) - ), - tbody(cls("bg-white divide-y divide-gray-100"), sections) - ) - ) - ) - - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - td( - cls("bg-gray-50 border-gray-200 border-t border-b"), - cls( - "sticky top-14 z-10 bg-gray-50 px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - colSpan(cs), - label - ), - mods - ) - - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - mods, - td( - cls( - "px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900 truncate" - ), - label - ), - children.map(e => - td( - cls( - "whitespace-nowrap px-2 py-2 text-sm text-gray-500" - ), - e - ) - ) - ) - - private def interleave( - a: Seq[HtmlElement], - separator: => HtmlElement - ): Seq[HtmlElement] = - if a.size < 2 then a - else a.init.flatMap(n => Seq(n, separator)) :+ a.last diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala deleted file mode 100644 index a45beaf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.FileRef - -trait DetailComponentsModule: - - def details: DetailComponents - - trait DetailComponents: - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement - - def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement - - def files(fs: List[FileRef]): HtmlElement - - def file(f: FileRef): HtmlElement - -trait DefaultDetailComponentsModule(using ctx: ComponentContext) - extends DetailComponentsModule: - self: IconsModule => - - override val details: DetailComponents = new DetailComponents: - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement = - div( - div( - cls := "px-4 sm:px-0", - h3( - cls := "text-base font-semibold leading-7 text-gray-900", - title - ), - subtitle.map(st => - p( - cls := "mt-1 max-w-2xl text-sm leading-6 text-gray-500", - st - ) - ) - // TODO: actions - ), - div( - cls := "mt-6 border-t border-gray-100", - dl( - cls := "divide-y divide-gray-100", - fields - ) - ) - ) - - override def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0", - dt( - cls := "text-sm font-medium leading-6 text-gray-900", - title - ), - dd( - cls := "mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0", - content - ) - ) - - override def files(fs: List[FileRef]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-100 rounded-md border border-gray-200", - fs.map(file) - ) - - override def file(f: FileRef): HtmlElement = - li( - cls := "flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6", - div( - cls := "flex w-0 flex-1 items-center", - icons.`paper-clip-solid`(), - div( - cls := "ml-4 flex min-w-0 flex-1 gap-2", - span( - cls := "truncate font-medium", - f.name - ), - f.size.map(size => span(cls := "flex-shrink-0 text-gray-400", size)) - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - "Uložit" - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala new file mode 100644 index 0000000..f6c583f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait FormComponents(using ctx: ComponentContext) extends LocalDateSelectModule: + def searchIcon: SvgElement + + def renderLocalDateSelect( + id: String, + labelText: Option[String], + placeholderText: Option[String], + mods: LocalDateSelect => Modifier[HtmlElement] + ): HtmlElement = + div( + labelText, + input( + idAttr(id), + nameAttr(id), + autoComplete("date"), + tpe("date"), + cls( + "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ), + placeholderText.map(placeholder(_)), + mods(localDateSelect) + ) + ) + + def searchField( + id: String, + placeholderText: Option[String] = None + )( + mods: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls := "flex-1 min-w-0", + label(ctx.messages("forms.search.label"), forId(id), cls("sr-only")), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + searchIcon + ), + input( + tpe := "search", + nameAttr := "search", + idAttr := id, + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholderText + .orElse( + ctx.messages + .opt( + s"forms.search.${id}.placeholder", + s"form.search.placeholder" + ) + ) + .map(placeholder(_)), + mods + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala deleted file mode 100644 index 7714490..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala +++ /dev/null @@ -1,82 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -/** Form components - * - * A form is a collection of sections, each of which contains a collection of - * fields. - * - * Form -> * Section -> * Field - * -> * Action - * - * Each Field has an `id`, `label` and `input` element. The `id` is used to - * link the label to the input. - * - * The `input` element can be a simple text input, or a more complex component - * such as a date picker. The caller is expected to make the input work. - */ -trait FormComponentsModule extends LocalDateSelectModule: - def forms: FormComponents - - trait FormComponents: - - /** Layout the sections and actions of a form */ - def form(mods: Modifier[HtmlElement]*)(sections: Modifier[HtmlElement]*)( - actions: Modifier[HtmlElement]* - ): HtmlElement - - /** Section layout, with a title, optional subtitle and any content. */ - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label and any content */ - def field(label: Modifier[HtmlElement])( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label, input and optional help text */ - def field( - id: String, - label: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement - - def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout an inline form element */ - def inlineForm(mods: Modifier[HtmlElement]*): HtmlElement - - /** An input field with a search icon */ - def searchField(id: String, placeholderText: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement - - /** LocalDate input */ - def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala deleted file mode 100644 index 8acbe7f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala +++ /dev/null @@ -1,28 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.Tag - -trait GenericComponentsModule: - - def generic: GenericComponents - - trait GenericComponents: - def tag(name: String, color: ColorKind): HtmlElement - def tag(t: Tag): HtmlElement = tag(t.value, t.color) - -trait DefaultGenericComponentsModule(using ComponentContext) - extends GenericComponentsModule: - override val generic: GenericComponents = new GenericComponents: - override def tag(name: String, color: ColorKind): HtmlElement = - p( - cls( - "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" - ), - color(800).text, - color(100).bg, - name - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala deleted file mode 100644 index 8cfc1d9..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala +++ /dev/null @@ -1,123 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.CustomAttrs - -trait IconsModule: - def icons: Icons - - trait Icons: - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement - def close(mods: Modifier[SvgElement]*): SvgElement - def `search-solid`(mods: Modifier[SvgElement]*): SvgElement - def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement - def `document-chart-bar-outline`(mods: Modifier[SvgElement]*): SvgElement - def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement - -trait DefaultIconsModule(using ComponentContext) extends IconsModule: - override val icons: Icons = new Icons: - import svg.* - - object aria: - val hidden = CustomAttrs.svg.ariaHidden - - private def withDefault( - mods: Seq[Modifier[SvgElement]], - default: Modifier[SvgElement] - ): Modifier[SvgElement] = - if mods.isEmpty then default else mods - - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-8 w-8"), - 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" - ) - ) - ) - - override def close(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-2 w-2"), - stroke := "currentColor", - fill := "none", - viewBox := "0 0 8 8", - path( - strokeLineCap := "round", - strokeWidth := "1.5", - d := "M1 1l6 6m0-6L1 7" - ) - ) - - override def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 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 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") - ) - ) - - override def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 text-gray-400")), - 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") - ) - ) - - override def `document-chart-bar-outline`( - mods: Modifier[SvgElement]* - ): SvgElement = - svg( - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - strokeWidth := "1.5", - stroke := "currentColor", - withDefault(mods, cls := "h-6 w-6"), - path( - strokeLineCap := "round", - strokeLineJoin := "round", - d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" - ) - ) - - override def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-5 w-5"), - cls := "flex-shrink-0 text-gray-400", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", - clipRule := "evenodd" - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala deleted file mode 100644 index 95ab654..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait LayoutModule: - def layout: LayoutComponents - - trait LayoutComponents: - def card(content: Modifier[HtmlElement]*): HtmlElement - -trait DefaultLayoutModule(using ctx: ComponentContext) extends LayoutModule: - override val layout: LayoutComponents = new LayoutComponents: - override def card(content: Modifier[HtmlElement]*): HtmlElement = - div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala deleted file mode 100644 index 5946974..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom.html.Paragraph -import com.raquo.laminar.nodes.TextNode - -trait ListComponentsModule: - - def list: ListComponents - - trait ListComponents(using ComponentContext): - def label(text: String, color: ColorKind): HtmlElement - def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI - def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] - def listSection(header: String, list: HtmlElement): Div - def navigation(sections: Modifier[HtmlElement]): HtmlElement - -trait DefaultListComponentsModule(using ComponentContext) - extends ListComponentsModule: - self: GenericComponentsModule => - - override val list: ListComponents = new ListComponents: - override def label( - text: String, - color: ColorKind - ): HtmlElement = generic.tag(text, color) - - override def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI = - li( - cls("group"), - div( - cls( - "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" - ), - avatar.map(a => - div( - cls("flex-shrink-0"), - div( - cls( - "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" - ), - a - ) - ) - ), - div( - cls("flex-1 min-w-0"), - p( - cls("text-sm font-medium text-gray-900"), - title, - span(cls("float-right"), right) - ), - subtitle.map(st => - p( - cls("text-sm text-gray-500 truncate"), - st - ) - ) - ) - ) - ) - - override def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = - ul( - cls("relative z-0 divide-y divide-gray-200"), - role("list"), - children - ) - - override def listSection( - header: String, - list: HtmlElement - ): Div = - div( - cls("relative"), - div( - cls( - "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" - ), - header - ), - list - ) - - override def navigation(sections: Modifier[HtmlElement]): HtmlElement = - navTag( - cls("flex-1 min-h-0 overflow-y-auto"), - sections - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala deleted file mode 100644 index 5b5490f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ModalComponentsModule: - - def modal: ModalComponents - - trait ModalComponents: - def modalDialog( - content: HtmlElement, - onClose: Modifier[HtmlElement] - ): HtmlElement - -trait DefaultModalComponentsModule(using ctx: ComponentContext) - extends ModalComponentsModule: - - override val modal: ModalComponents = new ModalComponents: - - override def modalDialog( - content: HtmlElement, - closeMod: Modifier[HtmlElement] - ): 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( - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")), - closeMod - ) - ) - - div( - cls("fixed inset-0 z-20 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - 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-7xl sm:align-middle" - ), - content - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala deleted file mode 100644 index b713d8a..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala +++ /dev/null @@ -1,71 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait PageComponentsModule: - - val page: PageComponents - - trait PageComponents: - def container( - children: Modifier[HtmlElement]* - ): HtmlElement - - def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement - - def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement] = emptyMod, - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement - - /** Visage for clickable text, like links - */ - def clickable: Modifier[HtmlElement] - -trait DefaultPageComponentsModule(using ComponentContext) - extends PageComponentsModule: - - override val page: PageComponents = new PageComponents: - override def container( - children: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), - children - ) - - override def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement = - div( - cls("p-8 bg-gray-100 h-full"), - header, - children - ) - - override def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement = - div( - cls("pb-5 border-b border-gray-200"), - div(cls("float-right"), right), - h1( - cls("text-2xl leading-6 font-medium text-gray-900"), - title - ), - subtitle.map( - p( - cls("text-sm font-medium text-gray-500"), - _ - ) - ) - ) - - override def clickable: Modifier[HtmlElement] = - cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala deleted file mode 100644 index f70ee87..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala +++ /dev/null @@ -1,108 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} - -trait TableComponentsModule: - def tables: TableComponents - - trait TableComponents: - def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )(table: Modifier[HtmlElement]*): HtmlElement - - def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement - - def headerRow(mods: Modifier[HtmlElement]*)( - cells: HtmlElement* - ): HtmlElement - - def dataRow(mods: Modifier[HtmlElement]*)(cells: HtmlElement*): HtmlElement - - def headerCell(content: Modifier[HtmlElement]): HtmlElement - - def dataCell(content: Modifier[HtmlElement]): HtmlElement - -trait DefaultTableComponentsModule extends TableComponentsModule: - - override lazy val tables: TableComponents = new TableComponents: - - override def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )( - table: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("px-4 sm:px-6 lg:px-8"), - div( - cls("sm:flex sm:items-center"), - div( - cls("sm:flex-auto"), - h1(cls("text-base font-semibold leading-6 text-gray-900"), title), - subtitle.map(st => p(cls("mt-2 text-sm text-gray-700"), st)) - ), - div(cls("mt-4 sm:ml-16 sm:mt-0 sm:flex-none"), actions) - ), - div( - cls("mt-8 flow-root"), - div( - cls("-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"), - div( - cls("inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"), - table - ) - ) - ) - ) - - override def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement = - table( - cls("min-w-full divide-y divide-gray-300"), - thead(header), - tbody(cls("divide-y divide-gray-200"), body) - ) - - override def headerRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-3.5 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-3.5 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-3.5 px-3")) - ) - ) - - override def dataRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - mods, - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-4 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-4 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-4 px-3")) - ) - ) - - override def headerCell(content: Modifier[HtmlElement]): HtmlElement = - th( - cls("text-left text-sm font-semibold text-gray-900"), - content - ) - - override def dataCell(content: Modifier[HtmlElement]): HtmlElement = - td( - cls("whitespace-nowrap text-sm text-gray-500"), - content - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala index b937d45..12d01ae 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala @@ -3,7 +3,7 @@ import com.raquo.laminar.api.L.{*, given} import works.iterative.ui.components.laminar.HtmlTabular import works.iterative.ui.model.tables.Tabular -import works.iterative.ui.components.laminar.TableComponentsModule +import works.iterative.ui.components.laminar.tailwind.ui.TableComponentsModule import works.iterative.core.UserMessage import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given import works.iterative.ui.components.tailwind.ComponentContext diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala new file mode 100644 index 0000000..87f3e8d --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala @@ -0,0 +1,54 @@ +package works.iterative.ui.components.laminar.tailwind.color + +import com.raquo.laminar.api.L.{*, given} + +/** Complete color definition that can be rendered to CSS. + * + * Includes the area, kind and weight of the color. + */ +case class Color(area: ColorArea, color: ColorDef): + def toCSS: String = s"${area.name}-${color.toCSS}" + +object Color: + import ColorDef.given + + given colorToCSS: Conversion[Color, HtmlMod] with + def apply(c: Color) = cls(c.toCSS) + + given colorToSVGCSS: Conversion[Color, SvgMod] with + def apply(c: Color) = svg.cls(c.toCSS) + + given colorSignalToCSS: Conversion[Signal[Color], HtmlMod] with + def apply(c: Signal[Color]) = cls <-- c.map(_.toCSS) + + given colorSignalToSVGCSS: Conversion[Signal[Color], SvgMod] with + def apply(c: Signal[Color]) = svg.cls <-- c.map(_.toCSS) + + def current = ColorDef(ColorKind.current) + def inherit = ColorDef(ColorKind.inherit) + def transp = ColorDef(ColorKind.transp) + def auto = ColorDef(ColorKind.auto) + def black = ColorDef(ColorKind.black) + def white = ColorDef(ColorKind.white) + def slate(weight: ColorWeight) = ColorDef(ColorKind.slate, weight) + def gray(weight: ColorWeight) = ColorDef(ColorKind.gray, weight) + def zinc(weight: ColorWeight) = ColorDef(ColorKind.zinc, weight) + def neutral(weight: ColorWeight) = ColorDef(ColorKind.neutral, weight) + def stone(weight: ColorWeight) = ColorDef(ColorKind.stone, weight) + def red(weight: ColorWeight) = ColorDef(ColorKind.red, weight) + def orange(weight: ColorWeight) = ColorDef(ColorKind.orange, weight) + def amber(weight: ColorWeight) = ColorDef(ColorKind.amber, weight) + def yellow(weight: ColorWeight) = ColorDef(ColorKind.yellow, weight) + def lime(weight: ColorWeight) = ColorDef(ColorKind.lime, weight) + def green(weight: ColorWeight) = ColorDef(ColorKind.green, weight) + def emerald(weight: ColorWeight) = ColorDef(ColorKind.emerald, weight) + def teal(weight: ColorWeight) = ColorDef(ColorKind.teal, weight) + def cyan(weight: ColorWeight) = ColorDef(ColorKind.cyan, weight) + def sky(weight: ColorWeight) = ColorDef(ColorKind.sky, weight) + def blue(weight: ColorWeight) = ColorDef(ColorKind.blue, weight) + def indigo(weight: ColorWeight) = ColorDef(ColorKind.indigo, weight) + def violet(weight: ColorWeight) = ColorDef(ColorKind.violet, weight) + def purple(weight: ColorWeight) = ColorDef(ColorKind.purple, weight) + def fuchsia(weight: ColorWeight) = ColorDef(ColorKind.fuchsia, weight) + def pink(weight: ColorWeight) = ColorDef(ColorKind.pink, weight) + def rose(weight: ColorWeight) = ColorDef(ColorKind.rose, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala new file mode 100644 index 0000000..4130840 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala @@ -0,0 +1,16 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines the area the color should apply to, eg. background, text, border, + * etc. + */ +enum ColorArea(val name: String): + case bg extends ColorArea("bg") + case text extends ColorArea("text") + case decoration extends ColorArea("decoration") + case border extends ColorArea("border") + case outline extends ColorArea("outline") + case divide extends ColorArea("divide") + case ring extends ColorArea("ring") + case ringOffset extends ColorArea("ring-offset") + case shadow extends ColorArea("shadow") + case accent extends ColorArea("accent") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala new file mode 100644 index 0000000..fab9404 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala @@ -0,0 +1,43 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** A combination of ColorKind and ColorWeight, if applicable. + * + * By applying area we get the full Color definition. + */ +sealed trait ColorDef: + def toCSS: String + + def bg = Color(ColorArea.bg, this) + def text = Color(ColorArea.text, this) + def decoration = Color(ColorArea.decoration, this) + def border = Color(ColorArea.border, this) + def outline = Color(ColorArea.outline, this) + def divide = Color(ColorArea.divide, this) + def ring = Color(ColorArea.ring, this) + def ringOffset = Color(ColorArea.ringOffset, this) + def shadow = Color(ColorArea.shadow, this) + def accent = Color(ColorArea.accent, this) + +// TODO: create a macro that will output the tailwind class as string directly during compilation, where possible. +object ColorDef: + case class WeightedColorDef( + kind: ColorKind, + weight: ColorWeight + ) extends ColorDef: + override def toCSS: String = s"${kind.name}-${weight.value}" + + case class UnweightedColorDef( + kind: ColorKind + ) extends ColorDef: + override def toCSS: String = kind.name + + // TODO: check that the kind is valid unweighted kind + // that means current, inherit, auto, transparent, black, white + // tried using implicit evidence, but the type inference for enumerations + // tends to generalize to the enum, instead of the real type + def apply[T <: ColorKind](kind: T)(using + ev: T <:< ColorKind.Unweighted + ): ColorDef = + UnweightedColorDef(kind) + def apply(kind: ColorKind, weight: ColorWeight): ColorDef = + WeightedColorDef(kind, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala new file mode 100644 index 0000000..865166b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala @@ -0,0 +1,47 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines what color should be used, without specifying the area or weight. + */ +sealed abstract class ColorKind private (val name: String): + def apply(weight: ColorWeight): ColorDef = + ColorDef.WeightedColorDef(this, weight) + +object ColorKind: + trait Unweighted: + self: ColorKind => + override def apply(weight: ColorWeight): ColorDef = + ColorDef.UnweightedColorDef(self) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case object current extends ColorKind("current") with Unweighted + case object inherit extends ColorKind("inherit") with Unweighted + // Not present in for all methods + case object transp extends ColorKind("transparent") with Unweighted + // Seen in accent, not preset otherwise + case object auto extends ColorKind("auto") with Unweighted + // Black and white do not have weight + case object black extends ColorKind("black") with Unweighted + case object white extends ColorKind("white") with Unweighted + case object slate extends ColorKind("slate") + case object gray extends ColorKind("gray") + case object zinc extends ColorKind("zinc") + case object neutral extends ColorKind("neutral") + case object stone extends ColorKind("stone") + case object red extends ColorKind("red") + case object orange extends ColorKind("orange") + case object amber extends ColorKind("amber") + case object yellow extends ColorKind("yellow") + case object lime extends ColorKind("lime") + case object green extends ColorKind("green") + case object emerald extends ColorKind("emerald") + case object teal extends ColorKind("teal") + case object cyan extends ColorKind("cyan") + case object sky extends ColorKind("sky") + case object blue extends ColorKind("blue") + case object indigo extends ColorKind("indigo") + case object violet extends ColorKind("violet") + case object purple extends ColorKind("purple") + case object fuchsia extends ColorKind("fuchsia") + case object pink extends ColorKind("pink") + case object rose extends ColorKind("rose") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala new file mode 100644 index 0000000..6f1a4cc --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala @@ -0,0 +1,33 @@ +package works.iterative.ui.components.laminar.tailwind.color + +opaque type ColorWeight = String + +extension (c: ColorWeight) def value: String = c + +/** Defines weight of a color, eg. 50, 100, 200, etc. + * + * Tailwind-like. + */ +object ColorWeight: + inline given int50: Conversion[50, ColorWeight] with + inline def apply(i: 50) = "50" + inline given int100: Conversion[100, ColorWeight] with + inline def apply(i: 100) = "100" + inline given int200: Conversion[200, ColorWeight] with + inline def apply(i: 200) = "200" + inline given int300: Conversion[300, ColorWeight] with + inline def apply(i: 300) = "300" + inline given int400: Conversion[400, ColorWeight] with + inline def apply(i: 400) = "400" + inline given int500: Conversion[500, ColorWeight] with + inline def apply(i: 500) = "500" + inline given int600: Conversion[600, ColorWeight] with + inline def apply(i: 600) = "600" + inline given int700: Conversion[700, ColorWeight] with + inline def apply(i: 700) = "700" + inline given int800: Conversion[800, ColorWeight] with + inline def apply(i: 800) = "800" + inline given int900: Conversion[900, ColorWeight] with + inline def apply(i: 900) = "900" + inline given int950: Conversion[950, ColorWeight] with + inline def apply(i: 950) = "950" diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala new file mode 100644 index 0000000..21e21f0 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala @@ -0,0 +1,12 @@ +package works.iterative.ui.components.laminar.tailwind + +/** We need a generic Color model that can be used both on server and on client. + * + * We have adopted the Tailwind model for now. There is nothing inherently + * Tailwind-specific in the implementation, but all the values are taken from + * their palette and the area model is very HTML biased. + * + * Still, I think that it is a good starting point for exploration and will + * satisfy our current needs. + */ +package object color {} diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" new file mode 100644 index 0000000..5c4e218 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" @@ -0,0 +1,21 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.model.color.ColorKind +import works.iterative.ui.components.tailwind.ComponentContext +import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given +import works.iterative.ui.model.Tag + +trait TailwindUIBadgesComponentsModule(using ComponentContext): + + object badges: + override def tag(name: String, color: ColorKind): HtmlElement = + p( + cls( + "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" + ), + color(800).text, + color(100).bg, + name + ) diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" new file mode 100644 index 0000000..0c9ca25 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" @@ -0,0 +1,24 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} + +trait TailwindUIHeadingsComponentsModule: + val headings: HeadingsComponents = new HeadingsComponents + + class HeadingsComponents: + def section(title: Node, actions: HtmlElement*): HtmlElement = + div( + cls("border-b border-gray-200 pb-5"), + div( + cls("sm:flex sm:items-center sm:justify-between"), + h3(cls("text-base font-semibold leading-6 text-gray-900"), title), + div( + cls("mt-3 flex sm:ml-4 sm:mt-0"), + actions match + case Nil => emptyMod + case first :: rest => + first :: rest.map(_.amend(cls("ml-3"))) + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala new file mode 100644 index 0000000..16ca01b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala @@ -0,0 +1,19 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.laminar.tailwind.color.ColorKind + +trait BadgeComponentsModule: + + object badges: + def pill(name: String, color: ColorKind): HtmlElement = + p( + cls( + "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" + ), + color(800).text, + color(100).bg, + name + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala new file mode 100644 index 0000000..ebb2968 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} + +trait ButtonComponentsModule: + + object buttons: + + private inline def srHelp(text: String): Modifier[HtmlElement] = + span(cls := "sr-only", text) + + def primaryButton( + id: String, + text: Modifier[HtmlElement], + icon: Option[SvgElement] = None, + buttonType: String = "submit" + )(mods: Modifier[HtmlElement]*): HtmlElement = + button( + tpe(buttonType), + cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + icon, + text, + mods + ) + + def secondaryButton( + id: String, + text: Modifier[HtmlElement], + icon: Option[SvgElement] = None, + buttonType: String = "button" + )(mods: Modifier[HtmlElement]*): HtmlElement = + button( + tpe(buttonType), + cls( + "ml-2 bg-white inline-flex justify-center 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" + ), + icon, + text, + mods + ) + + def inlineButton(id: String, icon: SvgElement)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + button( + tpe := "button", + cls := "ml-1 inline-flex h-4 w-4 flex-shrink-0 rounded-full p-1 text-gray-400 hover:bg-gray-200 hover:text-gray-500", + srHelp(id), + icon + ) + + def iconButton(id: String, icon: SvgElement, srText: Option[String] = None)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + button( + tpe := "button", + cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", + icon, + srText.map(srHelp(_)) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala new file mode 100644 index 0000000..eb6dd5f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala @@ -0,0 +1,101 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import org.scalajs.dom.html +import com.raquo.laminar.modifiers.KeyUpdater + +trait FormComponentsModule: + self: IconsModule => + + object forms: + + def section( + title: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] + )(content: Modifier[HtmlElement]*): HtmlElement = + div( + cls("space-y-6 sm:space-y-5"), + div( + h3(cls("text-lg leading-6 font-medium text-gray-900"), title), + subtitle.map(st => p(cls("mt-1 max-w-2xl text-sm text-gray-500"), st)) + ), + div(cls("mt-6 sm:mt-5 space-y-6 sm:space-y-5"), content) + ) + + def label(labelText: String, forId: Option[String] = None)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + L.label( + cls("block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"), + forId.map(id => L.forId(id)), + labelText, + mods + ) + + def field( + label: Modifier[HtmlElement] + )(content: Modifier[HtmlElement]*): 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, + div(cls("mt-1 sm:mt-0 sm:col-span-2"), content) + ) + + def field( + id: String, + labelText: String, + input: HtmlElement, + help: Option[String] + ): HtmlElement = + field( + label(labelText, Some(id))() + )( + input.amend(idAttr(id)), + help.map(h => p(cls("mt-2 text-sm text-gray-500"), h)) + ) + + def form(mods: Modifier[HtmlElement]*)( + sections: Modifier[HtmlElement]* + )(actions: Modifier[HtmlElement]*): HtmlElement = + L.form( + cls("space-y-8 divide-y divide-gray-200"), + mods, + sections, + div( + cls("pt-5"), + div(cls("flex justify-end"), actions) + ) + ) + + def inlineForm( + mods: Modifier[HtmlElement]* + ): HtmlElement = + L.form(cls("flex space-x-4"), mods) + + def inputField( + id: String, + labelText: String, + placeholderText: Option[String] = None, + inputType: String = "text", + helpText: Option[String] = None + ): HtmlElement = + field( + id, + labelText, + input( + tpe(inputType), + cls( + "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:max-w-xs sm:text-sm sm:leading-6" + ), + placeholderText.map(placeholder(_)) + ), + helpText + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala new file mode 100644 index 0000000..52da07e --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala @@ -0,0 +1,22 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} + +trait HeadingsComponentsModule: + object headings: + def section(title: Node, actions: HtmlElement*): HtmlElement = + div( + cls("border-b border-gray-200 pb-5"), + div( + cls("sm:flex sm:items-center sm:justify-between"), + h3(cls("text-base font-semibold leading-6 text-gray-900"), title), + div( + cls("mt-3 flex sm:ml-4 sm:mt-0"), + actions match + case Nil => emptyMod + case first :: rest => + first :: rest.map(_.amend(cls("ml-3"))) + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala new file mode 100644 index 0000000..c543150 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala @@ -0,0 +1,112 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext +import works.iterative.ui.components.tailwind.CustomAttrs + +trait IconsModule: + object icons: + import svg.* + + object aria: + val hidden = CustomAttrs.svg.ariaHidden + + private def withDefault( + mods: Seq[Modifier[SvgElement]], + default: Modifier[SvgElement] + ): Modifier[SvgElement] = + if mods.isEmpty then default else mods + + def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-8 w-8"), + 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" + ) + ) + ) + + def close(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-2 w-2"), + stroke := "currentColor", + fill := "none", + viewBox := "0 0 8 8", + path( + strokeLineCap := "round", + strokeWidth := "1.5", + d := "M1 1l6 6m0-6L1 7" + ) + ) + + def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls("h-5 w-5 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 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") + ) + ) + + def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls("h-5 w-5 text-gray-400")), + 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") + ) + ) + + def `document-chart-bar-outline`( + mods: Modifier[SvgElement]* + ): SvgElement = + svg( + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + strokeWidth := "1.5", + stroke := "currentColor", + withDefault(mods, cls := "h-6 w-6"), + path( + strokeLineCap := "round", + strokeLineJoin := "round", + d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" + ) + ) + + def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-5 w-5"), + cls := "flex-shrink-0 text-gray-400", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", + clipRule := "evenodd" + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala new file mode 100644 index 0000000..02cbdb6 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala @@ -0,0 +1,11 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait LayoutModule: + object layout: + def card(content: Modifier[HtmlElement]*): HtmlElement = + div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala new file mode 100644 index 0000000..b652da9 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala @@ -0,0 +1,88 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.laminar.nodes.ReactiveHtmlElement +import org.scalajs.dom.html.Paragraph +import com.raquo.laminar.nodes.TextNode +import works.iterative.ui.components.laminar.tailwind.color.ColorKind + +trait ListComponentsModule: + self: BadgeComponentsModule => + + object list: + def label( + text: String, + color: ColorKind + ): HtmlElement = badges.pill(text, color) + + def item( + title: String, + subtitle: Option[String], + right: Modifier[HtmlElement] = emptyMod, + avatar: Option[Modifier[HtmlElement]] = None + ): LI = + li( + cls("group"), + div( + cls( + "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" + ), + avatar.map(a => + div( + cls("flex-shrink-0"), + div( + cls( + "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" + ), + a + ) + ) + ), + div( + cls("flex-1 min-w-0"), + p( + cls("text-sm font-medium text-gray-900"), + title, + span(cls("float-right"), right) + ), + subtitle.map(st => + p( + cls("text-sm text-gray-500 truncate"), + st + ) + ) + ) + ) + ) + + def unordered( + children: Modifier[HtmlElement] + ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = + ul( + cls("relative z-0 divide-y divide-gray-200"), + role("list"), + children + ) + + def listSection( + header: String, + list: HtmlElement + ): Div = + div( + cls("relative"), + div( + cls( + "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" + ), + header + ), + list + ) + + def navigation(sections: Modifier[HtmlElement]): HtmlElement = + navTag( + cls("flex-1 min-h-0 overflow-y-auto"), + sections + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ModalComponentsModule.scala new file mode 100644 index 0000000..6027404 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ModalComponentsModule.scala @@ -0,0 +1,54 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait ModalComponentsModule: + object modal: + def modalDialog( + content: HtmlElement, + closeMod: Modifier[HtmlElement] + ): 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( + cls("fixed inset-0 transition-opacity"), + div(cls("absolute inset-0 bg-gray-500 opacity-75")), + closeMod + ) + ) + + div( + cls("fixed inset-0 z-20 overflow-y-auto"), + div( + cls("text-center sm:block sm:p-0"), + overlay, + browserCenteringModalTrick, + 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-7xl sm:align-middle" + ), + content + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/PageComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/PageComponentsModule.scala new file mode 100644 index 0000000..d5a422e --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/PageComponentsModule.scala @@ -0,0 +1,49 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait PageComponentsModule: + + object page: + def container( + children: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), + children + ) + + def singleColumn( + header: Modifier[HtmlElement] + )(children: Modifier[HtmlElement]*): HtmlElement = + div( + cls("p-8 bg-gray-100 h-full"), + header, + children + ) + + def pageHeader( + title: Modifier[HtmlElement], + right: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] = None + ): HtmlElement = + div( + cls("pb-5 border-b border-gray-200"), + div(cls("float-right"), right), + h1( + cls("text-2xl leading-6 font-medium text-gray-900"), + title + ), + subtitle.map( + p( + cls("text-sm font-medium text-gray-500"), + _ + ) + ) + ) + + def clickable: Modifier[HtmlElement] = + cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TableComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TableComponentsModule.scala new file mode 100644 index 0000000..417ea56 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TableComponentsModule.scala @@ -0,0 +1,86 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L +import com.raquo.laminar.api.L.{*, given} + +trait TableComponentsModule: + + object tables: + + def tableSection( + title: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] = None, + actions: Modifier[HtmlElement]* + )( + table: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls("px-4 sm:px-6 lg:px-8"), + div( + cls("sm:flex sm:items-center"), + div( + cls("sm:flex-auto"), + h1(cls("text-base font-semibold leading-6 text-gray-900"), title), + subtitle.map(st => p(cls("mt-2 text-sm text-gray-700"), st)) + ), + div(cls("mt-4 sm:ml-16 sm:mt-0 sm:flex-none"), actions) + ), + div( + cls("mt-8 flow-root"), + div( + cls("-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"), + div( + cls("inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"), + table + ) + ) + ) + ) + + def simpleTable(header: Modifier[HtmlElement]*)( + body: Modifier[HtmlElement]* + ): HtmlElement = + table( + cls("min-w-full divide-y divide-gray-300"), + thead(header), + tbody(cls("divide-y divide-gray-200"), body) + ) + + def headerRow( + mods: Modifier[HtmlElement]* + )(cells: HtmlElement*): HtmlElement = + tr( + cells.zipWithIndex.map((c, i) => + if i == 0 then c.amend(cls("py-3.5 pl-4 pr-3 sm:pl-0")) + else if i == cells.length - 1 then + c.amend(cls("py-3.5 pr-4 pl-3 sm:pr-0")) + else c.amend(cls("py-3.5 px-3")) + ) + ) + + def dataRow( + mods: Modifier[HtmlElement]* + )(cells: HtmlElement*): HtmlElement = + tr( + mods, + cells.zipWithIndex.map((c, i) => + if i == 0 then c.amend(cls("py-4 pl-4 pr-3 sm:pl-0")) + else if i == cells.length - 1 then + c.amend(cls("py-4 pr-4 pl-3 sm:pr-0")) + else c.amend(cls("py-4 px-3")) + ) + ) + + def headerCell(content: Modifier[HtmlElement]): HtmlElement = + th( + cls("text-left text-sm font-semibold text-gray-900"), + content + ) + + def dataCell(content: Modifier[HtmlElement]): HtmlElement = + td( + cls("whitespace-nowrap text-sm text-gray-500"), + content + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TailwindUICatalogue.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TailwindUICatalogue.scala new file mode 100644 index 0000000..5027b10 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TailwindUICatalogue.scala @@ -0,0 +1,17 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} + +trait TailwindUICatalogueModule + extends BadgeComponentsModule + with ButtonComponentsModule + with HeadingsComponentsModule + with IconsModule + with FormComponentsModule + with LayoutModule + with ListComponentsModule + with ModalComponentsModule + with PageComponentsModule + with TableComponentsModule diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/LabelsOnLeftFormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/LabelsOnLeftFormComponentsModule.scala deleted file mode 100644 index 210bbf8..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/LabelsOnLeftFormComponentsModule.scala +++ /dev/null @@ -1,155 +0,0 @@ -package works.iterative.ui.components.laminar -package tailwindui - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -trait LabelsOnLeftFormComponentsModule(using ctx: ComponentContext) - extends FormComponentsModule: - self: IconsModule => - override val forms = new FormComponents: - - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )(content: Modifier[HtmlElement]*): HtmlElement = - div( - cls("space-y-6 sm:space-y-5"), - div( - h3(cls("text-lg leading-6 font-medium text-gray-900"), title), - subtitle.map(st => p(cls("mt-1 max-w-2xl text-sm text-gray-500"), st)) - ), - div(cls("mt-6 sm:mt-5 space-y-6 sm:space-y-5"), content) - ) - - override def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement = - L.label( - cls("block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"), - forId.map(id => L.forId(id)), - labelText, - mods - ) - - override def field( - label: Modifier[HtmlElement] - )(content: Modifier[HtmlElement]*): 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, - div(cls("mt-1 sm:mt-0 sm:col-span-2"), content) - ) - - override def field( - id: String, - labelText: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement = - field( - label(labelText, Some(id))() - )( - input.amend(idAttr(id)), - help.map(h => p(cls("mt-2 text-sm text-gray-500"), h)) - ) - - override def form(mods: Modifier[HtmlElement]*)( - sections: Modifier[HtmlElement]* - )(actions: Modifier[HtmlElement]*): HtmlElement = - L.form( - cls("space-y-8 divide-y divide-gray-200"), - mods, - sections, - div( - cls("pt-5"), - div(cls("flex justify-end"), actions) - ) - ) - - override def inlineForm( - mods: Modifier[HtmlElement]* - ): HtmlElement = - L.form(cls("flex space-x-4"), mods) - - override def searchField( - id: String, - placeholderText: Option[String] = None - )( - mods: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "flex-1 min-w-0", - label(ctx.messages("forms.search.label"), Some(id))(cls("sr-only")), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - icons.`search-solid`() - ), - input( - tpe := "search", - nameAttr := "search", - idAttr := id, - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholderText - .orElse( - ctx.messages - .opt( - s"forms.search.${id}.placeholder", - s"form.search.placeholder" - ) - ) - .map(placeholder(_)), - mods - ) - ) - ) - - override def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement = - field( - id, - labelText, - input( - tpe(inputType), - cls( - "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:max-w-xs sm:text-sm sm:leading-6" - ), - placeholderText.map(placeholder(_)) - ), - helpText - ) - - override def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement = - div( - labelText, - input( - idAttr(id), - nameAttr(id), - autoComplete("date"), - tpe("date"), - cls( - "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ), - placeholderText.map(placeholder(_)), - mods(localDateSelect) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/TailwindUIButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/TailwindUIButtonComponentsModule.scala deleted file mode 100644 index 4c44157..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/TailwindUIButtonComponentsModule.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar -package tailwindui - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait TailwindUIButtonComponentsModule(using ctx: ComponentContext) - extends ButtonComponentsModule: - - override val buttons = new ButtonComponents: - - private inline def srHelp(id: String): Modifier[HtmlElement] = - ctx.messages - .opt(s"form.button.${id}.screenReaderHelp") - .map(sr => span(cls := "sr-only", sr)) - - override def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )(mods: Modifier[HtmlElement]*): HtmlElement = - button( - tpe(buttonType), - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - icon, - text, - mods - ) - - override def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )(mods: Modifier[HtmlElement]*): HtmlElement = - button( - tpe(buttonType), - cls( - "ml-2 bg-white inline-flex justify-center 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" - ), - icon, - text, - mods - ) - - override def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement = - button( - tpe := "button", - cls := "ml-1 inline-flex h-4 w-4 flex-shrink-0 rounded-full p-1 text-gray-400 hover:bg-gray-200 hover:text-gray-500", - srHelp(id), - icon - ) - - override def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement = - button( - tpe := "button", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - icon, - srHelp(id) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/tailwind/laminar/LaminarExtensions.scala b/ui/js/src/main/scala/works/iterative/ui/components/tailwind/laminar/LaminarExtensions.scala index 4c5e68b..4c87800 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/tailwind/laminar/LaminarExtensions.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/tailwind/laminar/LaminarExtensions.scala @@ -2,23 +2,9 @@ package laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.Color import works.iterative.core.UserMessage object LaminarExtensions: - given colorToCSS: Conversion[Color, Modifier[HtmlElement]] with - def apply(c: Color) = cls(c.toCSS) - - given colorToSVGCSS: Conversion[Color, Modifier[SvgElement]] with - def apply(c: Color) = svg.cls(c.toCSS) - - given colorSignalToCSS: Conversion[Signal[Color], Modifier[HtmlElement]] with - def apply(c: Signal[Color]) = cls <-- c.map(_.toCSS) - - given colorSignalToSVGCSS: Conversion[Signal[Color], Modifier[SvgElement]] - with - def apply(c: Signal[Color]) = svg.cls <-- c.map(_.toCSS) - inline given userMessageToModifier(using ctx: ComponentContext ): Conversion[UserMessage, Modifier[HtmlElement]] with diff --git a/ui/js/src/main/scala/works/iterative/ui/scenarios/Scenario.scala b/ui/js/src/main/scala/works/iterative/ui/scenarios/Scenario.scala index 99a212a..60de9f0 100644 --- a/ui/js/src/main/scala/works/iterative/ui/scenarios/Scenario.scala +++ b/ui/js/src/main/scala/works/iterative/ui/scenarios/Scenario.scala @@ -13,8 +13,6 @@ import works.iterative.core.MessageCatalogue import works.iterative.ui.components.tailwind.ComponentContext import works.iterative.ui.components.tailwind.StyleGuide -import works.iterative.ui.model.color.Color -import works.iterative.ui.model.color.ColorWeight object Scenario: type Id = String diff --git a/ui/shared/src/main/scala/works/iterative/ui/model/Tag.scala b/ui/shared/src/main/scala/works/iterative/ui/model/Tag.scala index 7352579..958aa75 100644 --- a/ui/shared/src/main/scala/works/iterative/ui/model/Tag.scala +++ b/ui/shared/src/main/scala/works/iterative/ui/model/Tag.scala @@ -1,10 +1,16 @@ package works.iterative.ui.model -import works.iterative.ui.model.color.ColorKind - -/** Representation of colored string value. +/** Representation of a label or tag. * - * Used generally to represent tags or "labels", eg. some kind of status or - * categorization. + * The UI renderer will recognize this value as tag and render accordingly. The + * value is used to determine both the color of the tag and the text displayed. + * + * @param value + * the value of the tag */ -final case class Tag(value: String, color: ColorKind = ColorKind.gray) +opaque type Tag = String + +object Tag: + def apply(value: String): Tag = value + + extension (tag: Tag) def value: String = tag diff --git a/ui/shared/src/main/scala/works/iterative/ui/model/color/Color.scala b/ui/shared/src/main/scala/works/iterative/ui/model/color/Color.scala deleted file mode 100644 index 09d6e4d..0000000 --- a/ui/shared/src/main/scala/works/iterative/ui/model/color/Color.scala +++ /dev/null @@ -1,40 +0,0 @@ -package works.iterative.ui.model.color - -/** Complete color definition that can be rendered to CSS. - * - * Includes the area, kind and weight of the color. - */ -case class Color(area: ColorArea, color: ColorDef): - def toCSS: String = s"${area.name}-${color.toCSS}" - -object Color: - import ColorDef.given - - def current = ColorDef(ColorKind.current) - def inherit = ColorDef(ColorKind.inherit) - def transp = ColorDef(ColorKind.transp) - def auto = ColorDef(ColorKind.auto) - def black = ColorDef(ColorKind.black) - def white = ColorDef(ColorKind.white) - def slate(weight: ColorWeight) = ColorDef(ColorKind.slate, weight) - def gray(weight: ColorWeight) = ColorDef(ColorKind.gray, weight) - def zinc(weight: ColorWeight) = ColorDef(ColorKind.zinc, weight) - def neutral(weight: ColorWeight) = ColorDef(ColorKind.neutral, weight) - def stone(weight: ColorWeight) = ColorDef(ColorKind.stone, weight) - def red(weight: ColorWeight) = ColorDef(ColorKind.red, weight) - def orange(weight: ColorWeight) = ColorDef(ColorKind.orange, weight) - def amber(weight: ColorWeight) = ColorDef(ColorKind.amber, weight) - def yellow(weight: ColorWeight) = ColorDef(ColorKind.yellow, weight) - def lime(weight: ColorWeight) = ColorDef(ColorKind.lime, weight) - def green(weight: ColorWeight) = ColorDef(ColorKind.green, weight) - def emerald(weight: ColorWeight) = ColorDef(ColorKind.emerald, weight) - def teal(weight: ColorWeight) = ColorDef(ColorKind.teal, weight) - def cyan(weight: ColorWeight) = ColorDef(ColorKind.cyan, weight) - def sky(weight: ColorWeight) = ColorDef(ColorKind.sky, weight) - def blue(weight: ColorWeight) = ColorDef(ColorKind.blue, weight) - def indigo(weight: ColorWeight) = ColorDef(ColorKind.indigo, weight) - def violet(weight: ColorWeight) = ColorDef(ColorKind.violet, weight) - def purple(weight: ColorWeight) = ColorDef(ColorKind.purple, weight) - def fuchsia(weight: ColorWeight) = ColorDef(ColorKind.fuchsia, weight) - def pink(weight: ColorWeight) = ColorDef(ColorKind.pink, weight) - def rose(weight: ColorWeight) = ColorDef(ColorKind.rose, weight) diff --git a/ui/shared/src/main/scala/works/iterative/ui/model/color/ColorArea.scala b/ui/shared/src/main/scala/works/iterative/ui/model/color/ColorArea.scala deleted file mode 100644 index 1211287..0000000 --- a/ui/shared/src/main/scala/works/iterative/ui/model/color/ColorArea.scala +++ /dev/null @@ -1,16 +0,0 @@ -package works.iterative.ui.model.color - -/** Defines the area the color should apply to, eg. background, text, border, - * etc. - */ -enum ColorArea(val name: String): - case bg extends ColorArea("bg") - case text extends ColorArea("text") - case decoration extends ColorArea("decoration") - case border extends ColorArea("border") - case outline extends ColorArea("outline") - case divide extends ColorArea("divide") - case ring extends ColorArea("ring") - case ringOffset extends ColorArea("ring-offset") - case shadow extends ColorArea("shadow") - case accent extends ColorArea("accent") diff --git a/ui/shared/src/main/scala/works/iterative/ui/model/color/ColorDef.scala b/ui/shared/src/main/scala/works/iterative/ui/model/color/ColorDef.scala deleted file mode 100644 index 9c5ec61..0000000 --- a/ui/shared/src/main/scala/works/iterative/ui/model/color/ColorDef.scala +++ /dev/null @@ -1,43 +0,0 @@ -package works.iterative.ui.model.color - -/** A combination of ColorKind and ColorWeight, if applicable. - * - * By applying area we get the full Color definition. - */ -sealed trait ColorDef: - def toCSS: String - - def bg = Color(ColorArea.bg, this) - def text = Color(ColorArea.text, this) - def decoration = Color(ColorArea.decoration, this) - def border = Color(ColorArea.border, this) - def outline = Color(ColorArea.outline, this) - def divide = Color(ColorArea.divide, this) - def ring = Color(ColorArea.ring, this) - def ringOffset = Color(ColorArea.ringOffset, this) - def shadow = Color(ColorArea.shadow, this) - def accent = Color(ColorArea.accent, this) - -// TODO: create a macro that will output the tailwind class as string directly during compilation, where possible. -object ColorDef: - case class WeightedColorDef( - kind: ColorKind, - weight: ColorWeight - ) extends ColorDef: - override def toCSS: String = s"${kind.name}-${weight.value}" - - case class UnweightedColorDef( - kind: ColorKind - ) extends ColorDef: - override def toCSS: String = kind.name - - // TODO: check that the kind is valid unweighted kind - // that means current, inherit, auto, transparent, black, white - // tried using implicit evidence, but the type inference for enumerations - // tends to generalize to the enum, instead of the real type - def apply[T <: ColorKind](kind: T)(using - ev: T <:< ColorKind.Unweighted - ): ColorDef = - UnweightedColorDef(kind) - def apply(kind: ColorKind, weight: ColorWeight): ColorDef = - WeightedColorDef(kind, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala index a2c637c..bfa063a 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala @@ -1,13 +1,11 @@ package works.iterative.ui.components.laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorDef import com.raquo.laminar.tags.HtmlTag case class Bin[Source, +Value]( label: String, description: Option[String | HtmlElement], - color: ColorDef, valueOf: Source => Value ): def map[NewValue]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala deleted file mode 100644 index 3aac0bf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala +++ /dev/null @@ -1,34 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ButtonComponentsModule: - def buttons: ButtonComponents - - trait ButtonComponents: - def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala deleted file mode 100644 index 646e73d..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala +++ /dev/null @@ -1,191 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.color.ColorDef -import com.raquo.laminar.tags.HtmlTag -import org.scalajs.dom.html.UList -import com.raquo.laminar.nodes.ReactiveHtmlElement -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind - -trait DashboardComponentsModule: - def dashboard: DashboardComponents - - // Only methods that create HtmlElements - trait DashboardComponents(using ComponentContext): - def section(label: String, children: Modifier[HtmlElement]): Div - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI - def cardInitials( - initials: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div - def count(label: String, value: Int, color: ColorDef): Span - def counts(children: HtmlElement*): Span - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - -trait DefaultDashboardComponentsModule(using ComponentContext) - extends DashboardComponentsModule: - override val dashboard: DashboardComponents = new DashboardComponents: - def section(label: String, children: Modifier[HtmlElement]): Div = - div( - cls("mt-3"), - h2( - cls("text-gray-500 text-xs font-medium uppercase tracking-wide"), - label - ), - children - ) - - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] = - ul( - cls( - "mt-3 grid gap-5 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" - ), - children - ) - - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI = - li( - div( - cls("col-span-1 flex shadow-sm rounded-md"), - initials, - div( - cls( - "flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate" - ), - div( - cls("flex-1 px-4 py-2 text-sm truncate"), - div( - cls("text-gray-900 font-medium hover:text-gray-600 truncate"), - label - ), - p(cls("text-gray-500"), children) - ) - ) - ) - ) - - def cardInitials( - id: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div = - div( - cls( - "flex-shrink-0 flex flex-col items-center justify-center w-16 text-white text-sm font-medium rounded-l-md" - ), - color.map(_(600).bg), - div(id), - children - ) - - def count(label: String, value: Int, color: ColorDef): Span = - span(color.text, title(label), s"${value}") - - def counts(children: HtmlElement*): Span = - span(interleave(children, span(" / "))) - - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div = - div( - cls("flex flex-col mt-2"), - div( - cls( - "align-middle min-w-full shadow sm:rounded-lg" - ), - L.table( - cls("min-w-full border-separate border-spacing-0"), - styleAttr("border-spacing: 0"), // Tailwind somehow doesn't work - thead( - tr( - cls("border-t border-gray-200"), - headers.map(h => - th( - cls( - "sticky top-0 z-10 px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - h - ) - ) - ) - ), - tbody(cls("bg-white divide-y divide-gray-100"), sections) - ) - ) - ) - - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - td( - cls("bg-gray-50 border-gray-200 border-t border-b"), - cls( - "sticky top-14 z-10 bg-gray-50 px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - colSpan(cs), - label - ), - mods - ) - - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - mods, - td( - cls( - "px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900 truncate" - ), - label - ), - children.map(e => - td( - cls( - "whitespace-nowrap px-2 py-2 text-sm text-gray-500" - ), - e - ) - ) - ) - - private def interleave( - a: Seq[HtmlElement], - separator: => HtmlElement - ): Seq[HtmlElement] = - if a.size < 2 then a - else a.init.flatMap(n => Seq(n, separator)) :+ a.last diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala deleted file mode 100644 index a45beaf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.FileRef - -trait DetailComponentsModule: - - def details: DetailComponents - - trait DetailComponents: - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement - - def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement - - def files(fs: List[FileRef]): HtmlElement - - def file(f: FileRef): HtmlElement - -trait DefaultDetailComponentsModule(using ctx: ComponentContext) - extends DetailComponentsModule: - self: IconsModule => - - override val details: DetailComponents = new DetailComponents: - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement = - div( - div( - cls := "px-4 sm:px-0", - h3( - cls := "text-base font-semibold leading-7 text-gray-900", - title - ), - subtitle.map(st => - p( - cls := "mt-1 max-w-2xl text-sm leading-6 text-gray-500", - st - ) - ) - // TODO: actions - ), - div( - cls := "mt-6 border-t border-gray-100", - dl( - cls := "divide-y divide-gray-100", - fields - ) - ) - ) - - override def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0", - dt( - cls := "text-sm font-medium leading-6 text-gray-900", - title - ), - dd( - cls := "mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0", - content - ) - ) - - override def files(fs: List[FileRef]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-100 rounded-md border border-gray-200", - fs.map(file) - ) - - override def file(f: FileRef): HtmlElement = - li( - cls := "flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6", - div( - cls := "flex w-0 flex-1 items-center", - icons.`paper-clip-solid`(), - div( - cls := "ml-4 flex min-w-0 flex-1 gap-2", - span( - cls := "truncate font-medium", - f.name - ), - f.size.map(size => span(cls := "flex-shrink-0 text-gray-400", size)) - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - "Uložit" - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala new file mode 100644 index 0000000..f6c583f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait FormComponents(using ctx: ComponentContext) extends LocalDateSelectModule: + def searchIcon: SvgElement + + def renderLocalDateSelect( + id: String, + labelText: Option[String], + placeholderText: Option[String], + mods: LocalDateSelect => Modifier[HtmlElement] + ): HtmlElement = + div( + labelText, + input( + idAttr(id), + nameAttr(id), + autoComplete("date"), + tpe("date"), + cls( + "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ), + placeholderText.map(placeholder(_)), + mods(localDateSelect) + ) + ) + + def searchField( + id: String, + placeholderText: Option[String] = None + )( + mods: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls := "flex-1 min-w-0", + label(ctx.messages("forms.search.label"), forId(id), cls("sr-only")), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + searchIcon + ), + input( + tpe := "search", + nameAttr := "search", + idAttr := id, + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholderText + .orElse( + ctx.messages + .opt( + s"forms.search.${id}.placeholder", + s"form.search.placeholder" + ) + ) + .map(placeholder(_)), + mods + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala deleted file mode 100644 index 7714490..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala +++ /dev/null @@ -1,82 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -/** Form components - * - * A form is a collection of sections, each of which contains a collection of - * fields. - * - * Form -> * Section -> * Field - * -> * Action - * - * Each Field has an `id`, `label` and `input` element. The `id` is used to - * link the label to the input. - * - * The `input` element can be a simple text input, or a more complex component - * such as a date picker. The caller is expected to make the input work. - */ -trait FormComponentsModule extends LocalDateSelectModule: - def forms: FormComponents - - trait FormComponents: - - /** Layout the sections and actions of a form */ - def form(mods: Modifier[HtmlElement]*)(sections: Modifier[HtmlElement]*)( - actions: Modifier[HtmlElement]* - ): HtmlElement - - /** Section layout, with a title, optional subtitle and any content. */ - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label and any content */ - def field(label: Modifier[HtmlElement])( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label, input and optional help text */ - def field( - id: String, - label: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement - - def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout an inline form element */ - def inlineForm(mods: Modifier[HtmlElement]*): HtmlElement - - /** An input field with a search icon */ - def searchField(id: String, placeholderText: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement - - /** LocalDate input */ - def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala deleted file mode 100644 index 8acbe7f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala +++ /dev/null @@ -1,28 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.Tag - -trait GenericComponentsModule: - - def generic: GenericComponents - - trait GenericComponents: - def tag(name: String, color: ColorKind): HtmlElement - def tag(t: Tag): HtmlElement = tag(t.value, t.color) - -trait DefaultGenericComponentsModule(using ComponentContext) - extends GenericComponentsModule: - override val generic: GenericComponents = new GenericComponents: - override def tag(name: String, color: ColorKind): HtmlElement = - p( - cls( - "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" - ), - color(800).text, - color(100).bg, - name - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala deleted file mode 100644 index 8cfc1d9..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala +++ /dev/null @@ -1,123 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.CustomAttrs - -trait IconsModule: - def icons: Icons - - trait Icons: - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement - def close(mods: Modifier[SvgElement]*): SvgElement - def `search-solid`(mods: Modifier[SvgElement]*): SvgElement - def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement - def `document-chart-bar-outline`(mods: Modifier[SvgElement]*): SvgElement - def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement - -trait DefaultIconsModule(using ComponentContext) extends IconsModule: - override val icons: Icons = new Icons: - import svg.* - - object aria: - val hidden = CustomAttrs.svg.ariaHidden - - private def withDefault( - mods: Seq[Modifier[SvgElement]], - default: Modifier[SvgElement] - ): Modifier[SvgElement] = - if mods.isEmpty then default else mods - - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-8 w-8"), - 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" - ) - ) - ) - - override def close(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-2 w-2"), - stroke := "currentColor", - fill := "none", - viewBox := "0 0 8 8", - path( - strokeLineCap := "round", - strokeWidth := "1.5", - d := "M1 1l6 6m0-6L1 7" - ) - ) - - override def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 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 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") - ) - ) - - override def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 text-gray-400")), - 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") - ) - ) - - override def `document-chart-bar-outline`( - mods: Modifier[SvgElement]* - ): SvgElement = - svg( - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - strokeWidth := "1.5", - stroke := "currentColor", - withDefault(mods, cls := "h-6 w-6"), - path( - strokeLineCap := "round", - strokeLineJoin := "round", - d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" - ) - ) - - override def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-5 w-5"), - cls := "flex-shrink-0 text-gray-400", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", - clipRule := "evenodd" - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala deleted file mode 100644 index 95ab654..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait LayoutModule: - def layout: LayoutComponents - - trait LayoutComponents: - def card(content: Modifier[HtmlElement]*): HtmlElement - -trait DefaultLayoutModule(using ctx: ComponentContext) extends LayoutModule: - override val layout: LayoutComponents = new LayoutComponents: - override def card(content: Modifier[HtmlElement]*): HtmlElement = - div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala deleted file mode 100644 index 5946974..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom.html.Paragraph -import com.raquo.laminar.nodes.TextNode - -trait ListComponentsModule: - - def list: ListComponents - - trait ListComponents(using ComponentContext): - def label(text: String, color: ColorKind): HtmlElement - def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI - def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] - def listSection(header: String, list: HtmlElement): Div - def navigation(sections: Modifier[HtmlElement]): HtmlElement - -trait DefaultListComponentsModule(using ComponentContext) - extends ListComponentsModule: - self: GenericComponentsModule => - - override val list: ListComponents = new ListComponents: - override def label( - text: String, - color: ColorKind - ): HtmlElement = generic.tag(text, color) - - override def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI = - li( - cls("group"), - div( - cls( - "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" - ), - avatar.map(a => - div( - cls("flex-shrink-0"), - div( - cls( - "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" - ), - a - ) - ) - ), - div( - cls("flex-1 min-w-0"), - p( - cls("text-sm font-medium text-gray-900"), - title, - span(cls("float-right"), right) - ), - subtitle.map(st => - p( - cls("text-sm text-gray-500 truncate"), - st - ) - ) - ) - ) - ) - - override def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = - ul( - cls("relative z-0 divide-y divide-gray-200"), - role("list"), - children - ) - - override def listSection( - header: String, - list: HtmlElement - ): Div = - div( - cls("relative"), - div( - cls( - "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" - ), - header - ), - list - ) - - override def navigation(sections: Modifier[HtmlElement]): HtmlElement = - navTag( - cls("flex-1 min-h-0 overflow-y-auto"), - sections - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala deleted file mode 100644 index 5b5490f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ModalComponentsModule: - - def modal: ModalComponents - - trait ModalComponents: - def modalDialog( - content: HtmlElement, - onClose: Modifier[HtmlElement] - ): HtmlElement - -trait DefaultModalComponentsModule(using ctx: ComponentContext) - extends ModalComponentsModule: - - override val modal: ModalComponents = new ModalComponents: - - override def modalDialog( - content: HtmlElement, - closeMod: Modifier[HtmlElement] - ): 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( - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")), - closeMod - ) - ) - - div( - cls("fixed inset-0 z-20 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - 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-7xl sm:align-middle" - ), - content - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala deleted file mode 100644 index b713d8a..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala +++ /dev/null @@ -1,71 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait PageComponentsModule: - - val page: PageComponents - - trait PageComponents: - def container( - children: Modifier[HtmlElement]* - ): HtmlElement - - def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement - - def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement] = emptyMod, - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement - - /** Visage for clickable text, like links - */ - def clickable: Modifier[HtmlElement] - -trait DefaultPageComponentsModule(using ComponentContext) - extends PageComponentsModule: - - override val page: PageComponents = new PageComponents: - override def container( - children: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), - children - ) - - override def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement = - div( - cls("p-8 bg-gray-100 h-full"), - header, - children - ) - - override def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement = - div( - cls("pb-5 border-b border-gray-200"), - div(cls("float-right"), right), - h1( - cls("text-2xl leading-6 font-medium text-gray-900"), - title - ), - subtitle.map( - p( - cls("text-sm font-medium text-gray-500"), - _ - ) - ) - ) - - override def clickable: Modifier[HtmlElement] = - cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala deleted file mode 100644 index f70ee87..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala +++ /dev/null @@ -1,108 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} - -trait TableComponentsModule: - def tables: TableComponents - - trait TableComponents: - def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )(table: Modifier[HtmlElement]*): HtmlElement - - def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement - - def headerRow(mods: Modifier[HtmlElement]*)( - cells: HtmlElement* - ): HtmlElement - - def dataRow(mods: Modifier[HtmlElement]*)(cells: HtmlElement*): HtmlElement - - def headerCell(content: Modifier[HtmlElement]): HtmlElement - - def dataCell(content: Modifier[HtmlElement]): HtmlElement - -trait DefaultTableComponentsModule extends TableComponentsModule: - - override lazy val tables: TableComponents = new TableComponents: - - override def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )( - table: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("px-4 sm:px-6 lg:px-8"), - div( - cls("sm:flex sm:items-center"), - div( - cls("sm:flex-auto"), - h1(cls("text-base font-semibold leading-6 text-gray-900"), title), - subtitle.map(st => p(cls("mt-2 text-sm text-gray-700"), st)) - ), - div(cls("mt-4 sm:ml-16 sm:mt-0 sm:flex-none"), actions) - ), - div( - cls("mt-8 flow-root"), - div( - cls("-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"), - div( - cls("inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"), - table - ) - ) - ) - ) - - override def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement = - table( - cls("min-w-full divide-y divide-gray-300"), - thead(header), - tbody(cls("divide-y divide-gray-200"), body) - ) - - override def headerRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-3.5 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-3.5 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-3.5 px-3")) - ) - ) - - override def dataRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - mods, - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-4 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-4 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-4 px-3")) - ) - ) - - override def headerCell(content: Modifier[HtmlElement]): HtmlElement = - th( - cls("text-left text-sm font-semibold text-gray-900"), - content - ) - - override def dataCell(content: Modifier[HtmlElement]): HtmlElement = - td( - cls("whitespace-nowrap text-sm text-gray-500"), - content - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala index b937d45..12d01ae 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala @@ -3,7 +3,7 @@ import com.raquo.laminar.api.L.{*, given} import works.iterative.ui.components.laminar.HtmlTabular import works.iterative.ui.model.tables.Tabular -import works.iterative.ui.components.laminar.TableComponentsModule +import works.iterative.ui.components.laminar.tailwind.ui.TableComponentsModule import works.iterative.core.UserMessage import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given import works.iterative.ui.components.tailwind.ComponentContext diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala new file mode 100644 index 0000000..87f3e8d --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala @@ -0,0 +1,54 @@ +package works.iterative.ui.components.laminar.tailwind.color + +import com.raquo.laminar.api.L.{*, given} + +/** Complete color definition that can be rendered to CSS. + * + * Includes the area, kind and weight of the color. + */ +case class Color(area: ColorArea, color: ColorDef): + def toCSS: String = s"${area.name}-${color.toCSS}" + +object Color: + import ColorDef.given + + given colorToCSS: Conversion[Color, HtmlMod] with + def apply(c: Color) = cls(c.toCSS) + + given colorToSVGCSS: Conversion[Color, SvgMod] with + def apply(c: Color) = svg.cls(c.toCSS) + + given colorSignalToCSS: Conversion[Signal[Color], HtmlMod] with + def apply(c: Signal[Color]) = cls <-- c.map(_.toCSS) + + given colorSignalToSVGCSS: Conversion[Signal[Color], SvgMod] with + def apply(c: Signal[Color]) = svg.cls <-- c.map(_.toCSS) + + def current = ColorDef(ColorKind.current) + def inherit = ColorDef(ColorKind.inherit) + def transp = ColorDef(ColorKind.transp) + def auto = ColorDef(ColorKind.auto) + def black = ColorDef(ColorKind.black) + def white = ColorDef(ColorKind.white) + def slate(weight: ColorWeight) = ColorDef(ColorKind.slate, weight) + def gray(weight: ColorWeight) = ColorDef(ColorKind.gray, weight) + def zinc(weight: ColorWeight) = ColorDef(ColorKind.zinc, weight) + def neutral(weight: ColorWeight) = ColorDef(ColorKind.neutral, weight) + def stone(weight: ColorWeight) = ColorDef(ColorKind.stone, weight) + def red(weight: ColorWeight) = ColorDef(ColorKind.red, weight) + def orange(weight: ColorWeight) = ColorDef(ColorKind.orange, weight) + def amber(weight: ColorWeight) = ColorDef(ColorKind.amber, weight) + def yellow(weight: ColorWeight) = ColorDef(ColorKind.yellow, weight) + def lime(weight: ColorWeight) = ColorDef(ColorKind.lime, weight) + def green(weight: ColorWeight) = ColorDef(ColorKind.green, weight) + def emerald(weight: ColorWeight) = ColorDef(ColorKind.emerald, weight) + def teal(weight: ColorWeight) = ColorDef(ColorKind.teal, weight) + def cyan(weight: ColorWeight) = ColorDef(ColorKind.cyan, weight) + def sky(weight: ColorWeight) = ColorDef(ColorKind.sky, weight) + def blue(weight: ColorWeight) = ColorDef(ColorKind.blue, weight) + def indigo(weight: ColorWeight) = ColorDef(ColorKind.indigo, weight) + def violet(weight: ColorWeight) = ColorDef(ColorKind.violet, weight) + def purple(weight: ColorWeight) = ColorDef(ColorKind.purple, weight) + def fuchsia(weight: ColorWeight) = ColorDef(ColorKind.fuchsia, weight) + def pink(weight: ColorWeight) = ColorDef(ColorKind.pink, weight) + def rose(weight: ColorWeight) = ColorDef(ColorKind.rose, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala new file mode 100644 index 0000000..4130840 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala @@ -0,0 +1,16 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines the area the color should apply to, eg. background, text, border, + * etc. + */ +enum ColorArea(val name: String): + case bg extends ColorArea("bg") + case text extends ColorArea("text") + case decoration extends ColorArea("decoration") + case border extends ColorArea("border") + case outline extends ColorArea("outline") + case divide extends ColorArea("divide") + case ring extends ColorArea("ring") + case ringOffset extends ColorArea("ring-offset") + case shadow extends ColorArea("shadow") + case accent extends ColorArea("accent") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala new file mode 100644 index 0000000..fab9404 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala @@ -0,0 +1,43 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** A combination of ColorKind and ColorWeight, if applicable. + * + * By applying area we get the full Color definition. + */ +sealed trait ColorDef: + def toCSS: String + + def bg = Color(ColorArea.bg, this) + def text = Color(ColorArea.text, this) + def decoration = Color(ColorArea.decoration, this) + def border = Color(ColorArea.border, this) + def outline = Color(ColorArea.outline, this) + def divide = Color(ColorArea.divide, this) + def ring = Color(ColorArea.ring, this) + def ringOffset = Color(ColorArea.ringOffset, this) + def shadow = Color(ColorArea.shadow, this) + def accent = Color(ColorArea.accent, this) + +// TODO: create a macro that will output the tailwind class as string directly during compilation, where possible. +object ColorDef: + case class WeightedColorDef( + kind: ColorKind, + weight: ColorWeight + ) extends ColorDef: + override def toCSS: String = s"${kind.name}-${weight.value}" + + case class UnweightedColorDef( + kind: ColorKind + ) extends ColorDef: + override def toCSS: String = kind.name + + // TODO: check that the kind is valid unweighted kind + // that means current, inherit, auto, transparent, black, white + // tried using implicit evidence, but the type inference for enumerations + // tends to generalize to the enum, instead of the real type + def apply[T <: ColorKind](kind: T)(using + ev: T <:< ColorKind.Unweighted + ): ColorDef = + UnweightedColorDef(kind) + def apply(kind: ColorKind, weight: ColorWeight): ColorDef = + WeightedColorDef(kind, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala new file mode 100644 index 0000000..865166b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala @@ -0,0 +1,47 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines what color should be used, without specifying the area or weight. + */ +sealed abstract class ColorKind private (val name: String): + def apply(weight: ColorWeight): ColorDef = + ColorDef.WeightedColorDef(this, weight) + +object ColorKind: + trait Unweighted: + self: ColorKind => + override def apply(weight: ColorWeight): ColorDef = + ColorDef.UnweightedColorDef(self) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case object current extends ColorKind("current") with Unweighted + case object inherit extends ColorKind("inherit") with Unweighted + // Not present in for all methods + case object transp extends ColorKind("transparent") with Unweighted + // Seen in accent, not preset otherwise + case object auto extends ColorKind("auto") with Unweighted + // Black and white do not have weight + case object black extends ColorKind("black") with Unweighted + case object white extends ColorKind("white") with Unweighted + case object slate extends ColorKind("slate") + case object gray extends ColorKind("gray") + case object zinc extends ColorKind("zinc") + case object neutral extends ColorKind("neutral") + case object stone extends ColorKind("stone") + case object red extends ColorKind("red") + case object orange extends ColorKind("orange") + case object amber extends ColorKind("amber") + case object yellow extends ColorKind("yellow") + case object lime extends ColorKind("lime") + case object green extends ColorKind("green") + case object emerald extends ColorKind("emerald") + case object teal extends ColorKind("teal") + case object cyan extends ColorKind("cyan") + case object sky extends ColorKind("sky") + case object blue extends ColorKind("blue") + case object indigo extends ColorKind("indigo") + case object violet extends ColorKind("violet") + case object purple extends ColorKind("purple") + case object fuchsia extends ColorKind("fuchsia") + case object pink extends ColorKind("pink") + case object rose extends ColorKind("rose") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala new file mode 100644 index 0000000..6f1a4cc --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala @@ -0,0 +1,33 @@ +package works.iterative.ui.components.laminar.tailwind.color + +opaque type ColorWeight = String + +extension (c: ColorWeight) def value: String = c + +/** Defines weight of a color, eg. 50, 100, 200, etc. + * + * Tailwind-like. + */ +object ColorWeight: + inline given int50: Conversion[50, ColorWeight] with + inline def apply(i: 50) = "50" + inline given int100: Conversion[100, ColorWeight] with + inline def apply(i: 100) = "100" + inline given int200: Conversion[200, ColorWeight] with + inline def apply(i: 200) = "200" + inline given int300: Conversion[300, ColorWeight] with + inline def apply(i: 300) = "300" + inline given int400: Conversion[400, ColorWeight] with + inline def apply(i: 400) = "400" + inline given int500: Conversion[500, ColorWeight] with + inline def apply(i: 500) = "500" + inline given int600: Conversion[600, ColorWeight] with + inline def apply(i: 600) = "600" + inline given int700: Conversion[700, ColorWeight] with + inline def apply(i: 700) = "700" + inline given int800: Conversion[800, ColorWeight] with + inline def apply(i: 800) = "800" + inline given int900: Conversion[900, ColorWeight] with + inline def apply(i: 900) = "900" + inline given int950: Conversion[950, ColorWeight] with + inline def apply(i: 950) = "950" diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala new file mode 100644 index 0000000..21e21f0 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala @@ -0,0 +1,12 @@ +package works.iterative.ui.components.laminar.tailwind + +/** We need a generic Color model that can be used both on server and on client. + * + * We have adopted the Tailwind model for now. There is nothing inherently + * Tailwind-specific in the implementation, but all the values are taken from + * their palette and the area model is very HTML biased. + * + * Still, I think that it is a good starting point for exploration and will + * satisfy our current needs. + */ +package object color {} diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" new file mode 100644 index 0000000..5c4e218 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" @@ -0,0 +1,21 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.model.color.ColorKind +import works.iterative.ui.components.tailwind.ComponentContext +import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given +import works.iterative.ui.model.Tag + +trait TailwindUIBadgesComponentsModule(using ComponentContext): + + object badges: + override def tag(name: String, color: ColorKind): HtmlElement = + p( + cls( + "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" + ), + color(800).text, + color(100).bg, + name + ) diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" new file mode 100644 index 0000000..0c9ca25 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" @@ -0,0 +1,24 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} + +trait TailwindUIHeadingsComponentsModule: + val headings: HeadingsComponents = new HeadingsComponents + + class HeadingsComponents: + def section(title: Node, actions: HtmlElement*): HtmlElement = + div( + cls("border-b border-gray-200 pb-5"), + div( + cls("sm:flex sm:items-center sm:justify-between"), + h3(cls("text-base font-semibold leading-6 text-gray-900"), title), + div( + cls("mt-3 flex sm:ml-4 sm:mt-0"), + actions match + case Nil => emptyMod + case first :: rest => + first :: rest.map(_.amend(cls("ml-3"))) + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala new file mode 100644 index 0000000..16ca01b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala @@ -0,0 +1,19 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.laminar.tailwind.color.ColorKind + +trait BadgeComponentsModule: + + object badges: + def pill(name: String, color: ColorKind): HtmlElement = + p( + cls( + "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" + ), + color(800).text, + color(100).bg, + name + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala new file mode 100644 index 0000000..ebb2968 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} + +trait ButtonComponentsModule: + + object buttons: + + private inline def srHelp(text: String): Modifier[HtmlElement] = + span(cls := "sr-only", text) + + def primaryButton( + id: String, + text: Modifier[HtmlElement], + icon: Option[SvgElement] = None, + buttonType: String = "submit" + )(mods: Modifier[HtmlElement]*): HtmlElement = + button( + tpe(buttonType), + cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + icon, + text, + mods + ) + + def secondaryButton( + id: String, + text: Modifier[HtmlElement], + icon: Option[SvgElement] = None, + buttonType: String = "button" + )(mods: Modifier[HtmlElement]*): HtmlElement = + button( + tpe(buttonType), + cls( + "ml-2 bg-white inline-flex justify-center 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" + ), + icon, + text, + mods + ) + + def inlineButton(id: String, icon: SvgElement)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + button( + tpe := "button", + cls := "ml-1 inline-flex h-4 w-4 flex-shrink-0 rounded-full p-1 text-gray-400 hover:bg-gray-200 hover:text-gray-500", + srHelp(id), + icon + ) + + def iconButton(id: String, icon: SvgElement, srText: Option[String] = None)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + button( + tpe := "button", + cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", + icon, + srText.map(srHelp(_)) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala new file mode 100644 index 0000000..eb6dd5f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala @@ -0,0 +1,101 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import org.scalajs.dom.html +import com.raquo.laminar.modifiers.KeyUpdater + +trait FormComponentsModule: + self: IconsModule => + + object forms: + + def section( + title: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] + )(content: Modifier[HtmlElement]*): HtmlElement = + div( + cls("space-y-6 sm:space-y-5"), + div( + h3(cls("text-lg leading-6 font-medium text-gray-900"), title), + subtitle.map(st => p(cls("mt-1 max-w-2xl text-sm text-gray-500"), st)) + ), + div(cls("mt-6 sm:mt-5 space-y-6 sm:space-y-5"), content) + ) + + def label(labelText: String, forId: Option[String] = None)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + L.label( + cls("block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"), + forId.map(id => L.forId(id)), + labelText, + mods + ) + + def field( + label: Modifier[HtmlElement] + )(content: Modifier[HtmlElement]*): 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, + div(cls("mt-1 sm:mt-0 sm:col-span-2"), content) + ) + + def field( + id: String, + labelText: String, + input: HtmlElement, + help: Option[String] + ): HtmlElement = + field( + label(labelText, Some(id))() + )( + input.amend(idAttr(id)), + help.map(h => p(cls("mt-2 text-sm text-gray-500"), h)) + ) + + def form(mods: Modifier[HtmlElement]*)( + sections: Modifier[HtmlElement]* + )(actions: Modifier[HtmlElement]*): HtmlElement = + L.form( + cls("space-y-8 divide-y divide-gray-200"), + mods, + sections, + div( + cls("pt-5"), + div(cls("flex justify-end"), actions) + ) + ) + + def inlineForm( + mods: Modifier[HtmlElement]* + ): HtmlElement = + L.form(cls("flex space-x-4"), mods) + + def inputField( + id: String, + labelText: String, + placeholderText: Option[String] = None, + inputType: String = "text", + helpText: Option[String] = None + ): HtmlElement = + field( + id, + labelText, + input( + tpe(inputType), + cls( + "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:max-w-xs sm:text-sm sm:leading-6" + ), + placeholderText.map(placeholder(_)) + ), + helpText + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala new file mode 100644 index 0000000..52da07e --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala @@ -0,0 +1,22 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} + +trait HeadingsComponentsModule: + object headings: + def section(title: Node, actions: HtmlElement*): HtmlElement = + div( + cls("border-b border-gray-200 pb-5"), + div( + cls("sm:flex sm:items-center sm:justify-between"), + h3(cls("text-base font-semibold leading-6 text-gray-900"), title), + div( + cls("mt-3 flex sm:ml-4 sm:mt-0"), + actions match + case Nil => emptyMod + case first :: rest => + first :: rest.map(_.amend(cls("ml-3"))) + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala new file mode 100644 index 0000000..c543150 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala @@ -0,0 +1,112 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext +import works.iterative.ui.components.tailwind.CustomAttrs + +trait IconsModule: + object icons: + import svg.* + + object aria: + val hidden = CustomAttrs.svg.ariaHidden + + private def withDefault( + mods: Seq[Modifier[SvgElement]], + default: Modifier[SvgElement] + ): Modifier[SvgElement] = + if mods.isEmpty then default else mods + + def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-8 w-8"), + 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" + ) + ) + ) + + def close(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-2 w-2"), + stroke := "currentColor", + fill := "none", + viewBox := "0 0 8 8", + path( + strokeLineCap := "round", + strokeWidth := "1.5", + d := "M1 1l6 6m0-6L1 7" + ) + ) + + def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls("h-5 w-5 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 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") + ) + ) + + def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls("h-5 w-5 text-gray-400")), + 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") + ) + ) + + def `document-chart-bar-outline`( + mods: Modifier[SvgElement]* + ): SvgElement = + svg( + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + strokeWidth := "1.5", + stroke := "currentColor", + withDefault(mods, cls := "h-6 w-6"), + path( + strokeLineCap := "round", + strokeLineJoin := "round", + d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" + ) + ) + + def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-5 w-5"), + cls := "flex-shrink-0 text-gray-400", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", + clipRule := "evenodd" + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala new file mode 100644 index 0000000..02cbdb6 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala @@ -0,0 +1,11 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait LayoutModule: + object layout: + def card(content: Modifier[HtmlElement]*): HtmlElement = + div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala new file mode 100644 index 0000000..b652da9 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala @@ -0,0 +1,88 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.laminar.nodes.ReactiveHtmlElement +import org.scalajs.dom.html.Paragraph +import com.raquo.laminar.nodes.TextNode +import works.iterative.ui.components.laminar.tailwind.color.ColorKind + +trait ListComponentsModule: + self: BadgeComponentsModule => + + object list: + def label( + text: String, + color: ColorKind + ): HtmlElement = badges.pill(text, color) + + def item( + title: String, + subtitle: Option[String], + right: Modifier[HtmlElement] = emptyMod, + avatar: Option[Modifier[HtmlElement]] = None + ): LI = + li( + cls("group"), + div( + cls( + "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" + ), + avatar.map(a => + div( + cls("flex-shrink-0"), + div( + cls( + "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" + ), + a + ) + ) + ), + div( + cls("flex-1 min-w-0"), + p( + cls("text-sm font-medium text-gray-900"), + title, + span(cls("float-right"), right) + ), + subtitle.map(st => + p( + cls("text-sm text-gray-500 truncate"), + st + ) + ) + ) + ) + ) + + def unordered( + children: Modifier[HtmlElement] + ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = + ul( + cls("relative z-0 divide-y divide-gray-200"), + role("list"), + children + ) + + def listSection( + header: String, + list: HtmlElement + ): Div = + div( + cls("relative"), + div( + cls( + "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" + ), + header + ), + list + ) + + def navigation(sections: Modifier[HtmlElement]): HtmlElement = + navTag( + cls("flex-1 min-h-0 overflow-y-auto"), + sections + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ModalComponentsModule.scala new file mode 100644 index 0000000..6027404 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ModalComponentsModule.scala @@ -0,0 +1,54 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait ModalComponentsModule: + object modal: + def modalDialog( + content: HtmlElement, + closeMod: Modifier[HtmlElement] + ): 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( + cls("fixed inset-0 transition-opacity"), + div(cls("absolute inset-0 bg-gray-500 opacity-75")), + closeMod + ) + ) + + div( + cls("fixed inset-0 z-20 overflow-y-auto"), + div( + cls("text-center sm:block sm:p-0"), + overlay, + browserCenteringModalTrick, + 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-7xl sm:align-middle" + ), + content + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/PageComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/PageComponentsModule.scala new file mode 100644 index 0000000..d5a422e --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/PageComponentsModule.scala @@ -0,0 +1,49 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait PageComponentsModule: + + object page: + def container( + children: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), + children + ) + + def singleColumn( + header: Modifier[HtmlElement] + )(children: Modifier[HtmlElement]*): HtmlElement = + div( + cls("p-8 bg-gray-100 h-full"), + header, + children + ) + + def pageHeader( + title: Modifier[HtmlElement], + right: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] = None + ): HtmlElement = + div( + cls("pb-5 border-b border-gray-200"), + div(cls("float-right"), right), + h1( + cls("text-2xl leading-6 font-medium text-gray-900"), + title + ), + subtitle.map( + p( + cls("text-sm font-medium text-gray-500"), + _ + ) + ) + ) + + def clickable: Modifier[HtmlElement] = + cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TableComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TableComponentsModule.scala new file mode 100644 index 0000000..417ea56 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TableComponentsModule.scala @@ -0,0 +1,86 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L +import com.raquo.laminar.api.L.{*, given} + +trait TableComponentsModule: + + object tables: + + def tableSection( + title: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] = None, + actions: Modifier[HtmlElement]* + )( + table: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls("px-4 sm:px-6 lg:px-8"), + div( + cls("sm:flex sm:items-center"), + div( + cls("sm:flex-auto"), + h1(cls("text-base font-semibold leading-6 text-gray-900"), title), + subtitle.map(st => p(cls("mt-2 text-sm text-gray-700"), st)) + ), + div(cls("mt-4 sm:ml-16 sm:mt-0 sm:flex-none"), actions) + ), + div( + cls("mt-8 flow-root"), + div( + cls("-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"), + div( + cls("inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"), + table + ) + ) + ) + ) + + def simpleTable(header: Modifier[HtmlElement]*)( + body: Modifier[HtmlElement]* + ): HtmlElement = + table( + cls("min-w-full divide-y divide-gray-300"), + thead(header), + tbody(cls("divide-y divide-gray-200"), body) + ) + + def headerRow( + mods: Modifier[HtmlElement]* + )(cells: HtmlElement*): HtmlElement = + tr( + cells.zipWithIndex.map((c, i) => + if i == 0 then c.amend(cls("py-3.5 pl-4 pr-3 sm:pl-0")) + else if i == cells.length - 1 then + c.amend(cls("py-3.5 pr-4 pl-3 sm:pr-0")) + else c.amend(cls("py-3.5 px-3")) + ) + ) + + def dataRow( + mods: Modifier[HtmlElement]* + )(cells: HtmlElement*): HtmlElement = + tr( + mods, + cells.zipWithIndex.map((c, i) => + if i == 0 then c.amend(cls("py-4 pl-4 pr-3 sm:pl-0")) + else if i == cells.length - 1 then + c.amend(cls("py-4 pr-4 pl-3 sm:pr-0")) + else c.amend(cls("py-4 px-3")) + ) + ) + + def headerCell(content: Modifier[HtmlElement]): HtmlElement = + th( + cls("text-left text-sm font-semibold text-gray-900"), + content + ) + + def dataCell(content: Modifier[HtmlElement]): HtmlElement = + td( + cls("whitespace-nowrap text-sm text-gray-500"), + content + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TailwindUICatalogue.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TailwindUICatalogue.scala new file mode 100644 index 0000000..5027b10 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TailwindUICatalogue.scala @@ -0,0 +1,17 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} + +trait TailwindUICatalogueModule + extends BadgeComponentsModule + with ButtonComponentsModule + with HeadingsComponentsModule + with IconsModule + with FormComponentsModule + with LayoutModule + with ListComponentsModule + with ModalComponentsModule + with PageComponentsModule + with TableComponentsModule diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/LabelsOnLeftFormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/LabelsOnLeftFormComponentsModule.scala deleted file mode 100644 index 210bbf8..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/LabelsOnLeftFormComponentsModule.scala +++ /dev/null @@ -1,155 +0,0 @@ -package works.iterative.ui.components.laminar -package tailwindui - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -trait LabelsOnLeftFormComponentsModule(using ctx: ComponentContext) - extends FormComponentsModule: - self: IconsModule => - override val forms = new FormComponents: - - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )(content: Modifier[HtmlElement]*): HtmlElement = - div( - cls("space-y-6 sm:space-y-5"), - div( - h3(cls("text-lg leading-6 font-medium text-gray-900"), title), - subtitle.map(st => p(cls("mt-1 max-w-2xl text-sm text-gray-500"), st)) - ), - div(cls("mt-6 sm:mt-5 space-y-6 sm:space-y-5"), content) - ) - - override def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement = - L.label( - cls("block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"), - forId.map(id => L.forId(id)), - labelText, - mods - ) - - override def field( - label: Modifier[HtmlElement] - )(content: Modifier[HtmlElement]*): 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, - div(cls("mt-1 sm:mt-0 sm:col-span-2"), content) - ) - - override def field( - id: String, - labelText: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement = - field( - label(labelText, Some(id))() - )( - input.amend(idAttr(id)), - help.map(h => p(cls("mt-2 text-sm text-gray-500"), h)) - ) - - override def form(mods: Modifier[HtmlElement]*)( - sections: Modifier[HtmlElement]* - )(actions: Modifier[HtmlElement]*): HtmlElement = - L.form( - cls("space-y-8 divide-y divide-gray-200"), - mods, - sections, - div( - cls("pt-5"), - div(cls("flex justify-end"), actions) - ) - ) - - override def inlineForm( - mods: Modifier[HtmlElement]* - ): HtmlElement = - L.form(cls("flex space-x-4"), mods) - - override def searchField( - id: String, - placeholderText: Option[String] = None - )( - mods: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "flex-1 min-w-0", - label(ctx.messages("forms.search.label"), Some(id))(cls("sr-only")), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - icons.`search-solid`() - ), - input( - tpe := "search", - nameAttr := "search", - idAttr := id, - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholderText - .orElse( - ctx.messages - .opt( - s"forms.search.${id}.placeholder", - s"form.search.placeholder" - ) - ) - .map(placeholder(_)), - mods - ) - ) - ) - - override def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement = - field( - id, - labelText, - input( - tpe(inputType), - cls( - "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:max-w-xs sm:text-sm sm:leading-6" - ), - placeholderText.map(placeholder(_)) - ), - helpText - ) - - override def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement = - div( - labelText, - input( - idAttr(id), - nameAttr(id), - autoComplete("date"), - tpe("date"), - cls( - "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ), - placeholderText.map(placeholder(_)), - mods(localDateSelect) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/TailwindUIButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/TailwindUIButtonComponentsModule.scala deleted file mode 100644 index 4c44157..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/TailwindUIButtonComponentsModule.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar -package tailwindui - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait TailwindUIButtonComponentsModule(using ctx: ComponentContext) - extends ButtonComponentsModule: - - override val buttons = new ButtonComponents: - - private inline def srHelp(id: String): Modifier[HtmlElement] = - ctx.messages - .opt(s"form.button.${id}.screenReaderHelp") - .map(sr => span(cls := "sr-only", sr)) - - override def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )(mods: Modifier[HtmlElement]*): HtmlElement = - button( - tpe(buttonType), - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - icon, - text, - mods - ) - - override def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )(mods: Modifier[HtmlElement]*): HtmlElement = - button( - tpe(buttonType), - cls( - "ml-2 bg-white inline-flex justify-center 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" - ), - icon, - text, - mods - ) - - override def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement = - button( - tpe := "button", - cls := "ml-1 inline-flex h-4 w-4 flex-shrink-0 rounded-full p-1 text-gray-400 hover:bg-gray-200 hover:text-gray-500", - srHelp(id), - icon - ) - - override def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement = - button( - tpe := "button", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - icon, - srHelp(id) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/tailwind/laminar/LaminarExtensions.scala b/ui/js/src/main/scala/works/iterative/ui/components/tailwind/laminar/LaminarExtensions.scala index 4c5e68b..4c87800 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/tailwind/laminar/LaminarExtensions.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/tailwind/laminar/LaminarExtensions.scala @@ -2,23 +2,9 @@ package laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.Color import works.iterative.core.UserMessage object LaminarExtensions: - given colorToCSS: Conversion[Color, Modifier[HtmlElement]] with - def apply(c: Color) = cls(c.toCSS) - - given colorToSVGCSS: Conversion[Color, Modifier[SvgElement]] with - def apply(c: Color) = svg.cls(c.toCSS) - - given colorSignalToCSS: Conversion[Signal[Color], Modifier[HtmlElement]] with - def apply(c: Signal[Color]) = cls <-- c.map(_.toCSS) - - given colorSignalToSVGCSS: Conversion[Signal[Color], Modifier[SvgElement]] - with - def apply(c: Signal[Color]) = svg.cls <-- c.map(_.toCSS) - inline given userMessageToModifier(using ctx: ComponentContext ): Conversion[UserMessage, Modifier[HtmlElement]] with diff --git a/ui/js/src/main/scala/works/iterative/ui/scenarios/Scenario.scala b/ui/js/src/main/scala/works/iterative/ui/scenarios/Scenario.scala index 99a212a..60de9f0 100644 --- a/ui/js/src/main/scala/works/iterative/ui/scenarios/Scenario.scala +++ b/ui/js/src/main/scala/works/iterative/ui/scenarios/Scenario.scala @@ -13,8 +13,6 @@ import works.iterative.core.MessageCatalogue import works.iterative.ui.components.tailwind.ComponentContext import works.iterative.ui.components.tailwind.StyleGuide -import works.iterative.ui.model.color.Color -import works.iterative.ui.model.color.ColorWeight object Scenario: type Id = String diff --git a/ui/shared/src/main/scala/works/iterative/ui/model/Tag.scala b/ui/shared/src/main/scala/works/iterative/ui/model/Tag.scala index 7352579..958aa75 100644 --- a/ui/shared/src/main/scala/works/iterative/ui/model/Tag.scala +++ b/ui/shared/src/main/scala/works/iterative/ui/model/Tag.scala @@ -1,10 +1,16 @@ package works.iterative.ui.model -import works.iterative.ui.model.color.ColorKind - -/** Representation of colored string value. +/** Representation of a label or tag. * - * Used generally to represent tags or "labels", eg. some kind of status or - * categorization. + * The UI renderer will recognize this value as tag and render accordingly. The + * value is used to determine both the color of the tag and the text displayed. + * + * @param value + * the value of the tag */ -final case class Tag(value: String, color: ColorKind = ColorKind.gray) +opaque type Tag = String + +object Tag: + def apply(value: String): Tag = value + + extension (tag: Tag) def value: String = tag diff --git a/ui/shared/src/main/scala/works/iterative/ui/model/color/Color.scala b/ui/shared/src/main/scala/works/iterative/ui/model/color/Color.scala deleted file mode 100644 index 09d6e4d..0000000 --- a/ui/shared/src/main/scala/works/iterative/ui/model/color/Color.scala +++ /dev/null @@ -1,40 +0,0 @@ -package works.iterative.ui.model.color - -/** Complete color definition that can be rendered to CSS. - * - * Includes the area, kind and weight of the color. - */ -case class Color(area: ColorArea, color: ColorDef): - def toCSS: String = s"${area.name}-${color.toCSS}" - -object Color: - import ColorDef.given - - def current = ColorDef(ColorKind.current) - def inherit = ColorDef(ColorKind.inherit) - def transp = ColorDef(ColorKind.transp) - def auto = ColorDef(ColorKind.auto) - def black = ColorDef(ColorKind.black) - def white = ColorDef(ColorKind.white) - def slate(weight: ColorWeight) = ColorDef(ColorKind.slate, weight) - def gray(weight: ColorWeight) = ColorDef(ColorKind.gray, weight) - def zinc(weight: ColorWeight) = ColorDef(ColorKind.zinc, weight) - def neutral(weight: ColorWeight) = ColorDef(ColorKind.neutral, weight) - def stone(weight: ColorWeight) = ColorDef(ColorKind.stone, weight) - def red(weight: ColorWeight) = ColorDef(ColorKind.red, weight) - def orange(weight: ColorWeight) = ColorDef(ColorKind.orange, weight) - def amber(weight: ColorWeight) = ColorDef(ColorKind.amber, weight) - def yellow(weight: ColorWeight) = ColorDef(ColorKind.yellow, weight) - def lime(weight: ColorWeight) = ColorDef(ColorKind.lime, weight) - def green(weight: ColorWeight) = ColorDef(ColorKind.green, weight) - def emerald(weight: ColorWeight) = ColorDef(ColorKind.emerald, weight) - def teal(weight: ColorWeight) = ColorDef(ColorKind.teal, weight) - def cyan(weight: ColorWeight) = ColorDef(ColorKind.cyan, weight) - def sky(weight: ColorWeight) = ColorDef(ColorKind.sky, weight) - def blue(weight: ColorWeight) = ColorDef(ColorKind.blue, weight) - def indigo(weight: ColorWeight) = ColorDef(ColorKind.indigo, weight) - def violet(weight: ColorWeight) = ColorDef(ColorKind.violet, weight) - def purple(weight: ColorWeight) = ColorDef(ColorKind.purple, weight) - def fuchsia(weight: ColorWeight) = ColorDef(ColorKind.fuchsia, weight) - def pink(weight: ColorWeight) = ColorDef(ColorKind.pink, weight) - def rose(weight: ColorWeight) = ColorDef(ColorKind.rose, weight) diff --git a/ui/shared/src/main/scala/works/iterative/ui/model/color/ColorArea.scala b/ui/shared/src/main/scala/works/iterative/ui/model/color/ColorArea.scala deleted file mode 100644 index 1211287..0000000 --- a/ui/shared/src/main/scala/works/iterative/ui/model/color/ColorArea.scala +++ /dev/null @@ -1,16 +0,0 @@ -package works.iterative.ui.model.color - -/** Defines the area the color should apply to, eg. background, text, border, - * etc. - */ -enum ColorArea(val name: String): - case bg extends ColorArea("bg") - case text extends ColorArea("text") - case decoration extends ColorArea("decoration") - case border extends ColorArea("border") - case outline extends ColorArea("outline") - case divide extends ColorArea("divide") - case ring extends ColorArea("ring") - case ringOffset extends ColorArea("ring-offset") - case shadow extends ColorArea("shadow") - case accent extends ColorArea("accent") diff --git a/ui/shared/src/main/scala/works/iterative/ui/model/color/ColorDef.scala b/ui/shared/src/main/scala/works/iterative/ui/model/color/ColorDef.scala deleted file mode 100644 index 9c5ec61..0000000 --- a/ui/shared/src/main/scala/works/iterative/ui/model/color/ColorDef.scala +++ /dev/null @@ -1,43 +0,0 @@ -package works.iterative.ui.model.color - -/** A combination of ColorKind and ColorWeight, if applicable. - * - * By applying area we get the full Color definition. - */ -sealed trait ColorDef: - def toCSS: String - - def bg = Color(ColorArea.bg, this) - def text = Color(ColorArea.text, this) - def decoration = Color(ColorArea.decoration, this) - def border = Color(ColorArea.border, this) - def outline = Color(ColorArea.outline, this) - def divide = Color(ColorArea.divide, this) - def ring = Color(ColorArea.ring, this) - def ringOffset = Color(ColorArea.ringOffset, this) - def shadow = Color(ColorArea.shadow, this) - def accent = Color(ColorArea.accent, this) - -// TODO: create a macro that will output the tailwind class as string directly during compilation, where possible. -object ColorDef: - case class WeightedColorDef( - kind: ColorKind, - weight: ColorWeight - ) extends ColorDef: - override def toCSS: String = s"${kind.name}-${weight.value}" - - case class UnweightedColorDef( - kind: ColorKind - ) extends ColorDef: - override def toCSS: String = kind.name - - // TODO: check that the kind is valid unweighted kind - // that means current, inherit, auto, transparent, black, white - // tried using implicit evidence, but the type inference for enumerations - // tends to generalize to the enum, instead of the real type - def apply[T <: ColorKind](kind: T)(using - ev: T <:< ColorKind.Unweighted - ): ColorDef = - UnweightedColorDef(kind) - def apply(kind: ColorKind, weight: ColorWeight): ColorDef = - WeightedColorDef(kind, weight) diff --git a/ui/shared/src/main/scala/works/iterative/ui/model/color/ColorKind.scala b/ui/shared/src/main/scala/works/iterative/ui/model/color/ColorKind.scala deleted file mode 100644 index ea24372..0000000 --- a/ui/shared/src/main/scala/works/iterative/ui/model/color/ColorKind.scala +++ /dev/null @@ -1,47 +0,0 @@ -package works.iterative.ui.model.color - -/** Defines what color should be used, without specifying the area or weight. - */ -sealed abstract class ColorKind private (val name: String): - def apply(weight: ColorWeight): ColorDef = - ColorDef.WeightedColorDef(this, weight) - -object ColorKind: - trait Unweighted: - self: ColorKind => - override def apply(weight: ColorWeight): ColorDef = - ColorDef.UnweightedColorDef(self) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case object current extends ColorKind("current") with Unweighted - case object inherit extends ColorKind("inherit") with Unweighted - // Not present in for all methods - case object transp extends ColorKind("transparent") with Unweighted - // Seen in accent, not preset otherwise - case object auto extends ColorKind("auto") with Unweighted - // Black and white do not have weight - case object black extends ColorKind("black") with Unweighted - case object white extends ColorKind("white") with Unweighted - case object slate extends ColorKind("slate") - case object gray extends ColorKind("gray") - case object zinc extends ColorKind("zinc") - case object neutral extends ColorKind("neutral") - case object stone extends ColorKind("stone") - case object red extends ColorKind("red") - case object orange extends ColorKind("orange") - case object amber extends ColorKind("amber") - case object yellow extends ColorKind("yellow") - case object lime extends ColorKind("lime") - case object green extends ColorKind("green") - case object emerald extends ColorKind("emerald") - case object teal extends ColorKind("teal") - case object cyan extends ColorKind("cyan") - case object sky extends ColorKind("sky") - case object blue extends ColorKind("blue") - case object indigo extends ColorKind("indigo") - case object violet extends ColorKind("violet") - case object purple extends ColorKind("purple") - case object fuchsia extends ColorKind("fuchsia") - case object pink extends ColorKind("pink") - case object rose extends ColorKind("rose") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala index a2c637c..bfa063a 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala @@ -1,13 +1,11 @@ package works.iterative.ui.components.laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorDef import com.raquo.laminar.tags.HtmlTag case class Bin[Source, +Value]( label: String, description: Option[String | HtmlElement], - color: ColorDef, valueOf: Source => Value ): def map[NewValue]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala deleted file mode 100644 index 3aac0bf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala +++ /dev/null @@ -1,34 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ButtonComponentsModule: - def buttons: ButtonComponents - - trait ButtonComponents: - def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala deleted file mode 100644 index 646e73d..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala +++ /dev/null @@ -1,191 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.color.ColorDef -import com.raquo.laminar.tags.HtmlTag -import org.scalajs.dom.html.UList -import com.raquo.laminar.nodes.ReactiveHtmlElement -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind - -trait DashboardComponentsModule: - def dashboard: DashboardComponents - - // Only methods that create HtmlElements - trait DashboardComponents(using ComponentContext): - def section(label: String, children: Modifier[HtmlElement]): Div - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI - def cardInitials( - initials: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div - def count(label: String, value: Int, color: ColorDef): Span - def counts(children: HtmlElement*): Span - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - -trait DefaultDashboardComponentsModule(using ComponentContext) - extends DashboardComponentsModule: - override val dashboard: DashboardComponents = new DashboardComponents: - def section(label: String, children: Modifier[HtmlElement]): Div = - div( - cls("mt-3"), - h2( - cls("text-gray-500 text-xs font-medium uppercase tracking-wide"), - label - ), - children - ) - - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] = - ul( - cls( - "mt-3 grid gap-5 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" - ), - children - ) - - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI = - li( - div( - cls("col-span-1 flex shadow-sm rounded-md"), - initials, - div( - cls( - "flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate" - ), - div( - cls("flex-1 px-4 py-2 text-sm truncate"), - div( - cls("text-gray-900 font-medium hover:text-gray-600 truncate"), - label - ), - p(cls("text-gray-500"), children) - ) - ) - ) - ) - - def cardInitials( - id: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div = - div( - cls( - "flex-shrink-0 flex flex-col items-center justify-center w-16 text-white text-sm font-medium rounded-l-md" - ), - color.map(_(600).bg), - div(id), - children - ) - - def count(label: String, value: Int, color: ColorDef): Span = - span(color.text, title(label), s"${value}") - - def counts(children: HtmlElement*): Span = - span(interleave(children, span(" / "))) - - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div = - div( - cls("flex flex-col mt-2"), - div( - cls( - "align-middle min-w-full shadow sm:rounded-lg" - ), - L.table( - cls("min-w-full border-separate border-spacing-0"), - styleAttr("border-spacing: 0"), // Tailwind somehow doesn't work - thead( - tr( - cls("border-t border-gray-200"), - headers.map(h => - th( - cls( - "sticky top-0 z-10 px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - h - ) - ) - ) - ), - tbody(cls("bg-white divide-y divide-gray-100"), sections) - ) - ) - ) - - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - td( - cls("bg-gray-50 border-gray-200 border-t border-b"), - cls( - "sticky top-14 z-10 bg-gray-50 px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - colSpan(cs), - label - ), - mods - ) - - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - mods, - td( - cls( - "px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900 truncate" - ), - label - ), - children.map(e => - td( - cls( - "whitespace-nowrap px-2 py-2 text-sm text-gray-500" - ), - e - ) - ) - ) - - private def interleave( - a: Seq[HtmlElement], - separator: => HtmlElement - ): Seq[HtmlElement] = - if a.size < 2 then a - else a.init.flatMap(n => Seq(n, separator)) :+ a.last diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala deleted file mode 100644 index a45beaf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.FileRef - -trait DetailComponentsModule: - - def details: DetailComponents - - trait DetailComponents: - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement - - def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement - - def files(fs: List[FileRef]): HtmlElement - - def file(f: FileRef): HtmlElement - -trait DefaultDetailComponentsModule(using ctx: ComponentContext) - extends DetailComponentsModule: - self: IconsModule => - - override val details: DetailComponents = new DetailComponents: - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement = - div( - div( - cls := "px-4 sm:px-0", - h3( - cls := "text-base font-semibold leading-7 text-gray-900", - title - ), - subtitle.map(st => - p( - cls := "mt-1 max-w-2xl text-sm leading-6 text-gray-500", - st - ) - ) - // TODO: actions - ), - div( - cls := "mt-6 border-t border-gray-100", - dl( - cls := "divide-y divide-gray-100", - fields - ) - ) - ) - - override def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0", - dt( - cls := "text-sm font-medium leading-6 text-gray-900", - title - ), - dd( - cls := "mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0", - content - ) - ) - - override def files(fs: List[FileRef]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-100 rounded-md border border-gray-200", - fs.map(file) - ) - - override def file(f: FileRef): HtmlElement = - li( - cls := "flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6", - div( - cls := "flex w-0 flex-1 items-center", - icons.`paper-clip-solid`(), - div( - cls := "ml-4 flex min-w-0 flex-1 gap-2", - span( - cls := "truncate font-medium", - f.name - ), - f.size.map(size => span(cls := "flex-shrink-0 text-gray-400", size)) - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - "Uložit" - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala new file mode 100644 index 0000000..f6c583f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait FormComponents(using ctx: ComponentContext) extends LocalDateSelectModule: + def searchIcon: SvgElement + + def renderLocalDateSelect( + id: String, + labelText: Option[String], + placeholderText: Option[String], + mods: LocalDateSelect => Modifier[HtmlElement] + ): HtmlElement = + div( + labelText, + input( + idAttr(id), + nameAttr(id), + autoComplete("date"), + tpe("date"), + cls( + "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ), + placeholderText.map(placeholder(_)), + mods(localDateSelect) + ) + ) + + def searchField( + id: String, + placeholderText: Option[String] = None + )( + mods: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls := "flex-1 min-w-0", + label(ctx.messages("forms.search.label"), forId(id), cls("sr-only")), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + searchIcon + ), + input( + tpe := "search", + nameAttr := "search", + idAttr := id, + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholderText + .orElse( + ctx.messages + .opt( + s"forms.search.${id}.placeholder", + s"form.search.placeholder" + ) + ) + .map(placeholder(_)), + mods + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala deleted file mode 100644 index 7714490..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala +++ /dev/null @@ -1,82 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -/** Form components - * - * A form is a collection of sections, each of which contains a collection of - * fields. - * - * Form -> * Section -> * Field - * -> * Action - * - * Each Field has an `id`, `label` and `input` element. The `id` is used to - * link the label to the input. - * - * The `input` element can be a simple text input, or a more complex component - * such as a date picker. The caller is expected to make the input work. - */ -trait FormComponentsModule extends LocalDateSelectModule: - def forms: FormComponents - - trait FormComponents: - - /** Layout the sections and actions of a form */ - def form(mods: Modifier[HtmlElement]*)(sections: Modifier[HtmlElement]*)( - actions: Modifier[HtmlElement]* - ): HtmlElement - - /** Section layout, with a title, optional subtitle and any content. */ - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label and any content */ - def field(label: Modifier[HtmlElement])( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label, input and optional help text */ - def field( - id: String, - label: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement - - def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout an inline form element */ - def inlineForm(mods: Modifier[HtmlElement]*): HtmlElement - - /** An input field with a search icon */ - def searchField(id: String, placeholderText: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement - - /** LocalDate input */ - def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala deleted file mode 100644 index 8acbe7f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala +++ /dev/null @@ -1,28 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.Tag - -trait GenericComponentsModule: - - def generic: GenericComponents - - trait GenericComponents: - def tag(name: String, color: ColorKind): HtmlElement - def tag(t: Tag): HtmlElement = tag(t.value, t.color) - -trait DefaultGenericComponentsModule(using ComponentContext) - extends GenericComponentsModule: - override val generic: GenericComponents = new GenericComponents: - override def tag(name: String, color: ColorKind): HtmlElement = - p( - cls( - "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" - ), - color(800).text, - color(100).bg, - name - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala deleted file mode 100644 index 8cfc1d9..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala +++ /dev/null @@ -1,123 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.CustomAttrs - -trait IconsModule: - def icons: Icons - - trait Icons: - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement - def close(mods: Modifier[SvgElement]*): SvgElement - def `search-solid`(mods: Modifier[SvgElement]*): SvgElement - def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement - def `document-chart-bar-outline`(mods: Modifier[SvgElement]*): SvgElement - def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement - -trait DefaultIconsModule(using ComponentContext) extends IconsModule: - override val icons: Icons = new Icons: - import svg.* - - object aria: - val hidden = CustomAttrs.svg.ariaHidden - - private def withDefault( - mods: Seq[Modifier[SvgElement]], - default: Modifier[SvgElement] - ): Modifier[SvgElement] = - if mods.isEmpty then default else mods - - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-8 w-8"), - 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" - ) - ) - ) - - override def close(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-2 w-2"), - stroke := "currentColor", - fill := "none", - viewBox := "0 0 8 8", - path( - strokeLineCap := "round", - strokeWidth := "1.5", - d := "M1 1l6 6m0-6L1 7" - ) - ) - - override def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 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 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") - ) - ) - - override def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 text-gray-400")), - 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") - ) - ) - - override def `document-chart-bar-outline`( - mods: Modifier[SvgElement]* - ): SvgElement = - svg( - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - strokeWidth := "1.5", - stroke := "currentColor", - withDefault(mods, cls := "h-6 w-6"), - path( - strokeLineCap := "round", - strokeLineJoin := "round", - d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" - ) - ) - - override def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-5 w-5"), - cls := "flex-shrink-0 text-gray-400", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", - clipRule := "evenodd" - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala deleted file mode 100644 index 95ab654..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait LayoutModule: - def layout: LayoutComponents - - trait LayoutComponents: - def card(content: Modifier[HtmlElement]*): HtmlElement - -trait DefaultLayoutModule(using ctx: ComponentContext) extends LayoutModule: - override val layout: LayoutComponents = new LayoutComponents: - override def card(content: Modifier[HtmlElement]*): HtmlElement = - div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala deleted file mode 100644 index 5946974..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom.html.Paragraph -import com.raquo.laminar.nodes.TextNode - -trait ListComponentsModule: - - def list: ListComponents - - trait ListComponents(using ComponentContext): - def label(text: String, color: ColorKind): HtmlElement - def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI - def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] - def listSection(header: String, list: HtmlElement): Div - def navigation(sections: Modifier[HtmlElement]): HtmlElement - -trait DefaultListComponentsModule(using ComponentContext) - extends ListComponentsModule: - self: GenericComponentsModule => - - override val list: ListComponents = new ListComponents: - override def label( - text: String, - color: ColorKind - ): HtmlElement = generic.tag(text, color) - - override def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI = - li( - cls("group"), - div( - cls( - "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" - ), - avatar.map(a => - div( - cls("flex-shrink-0"), - div( - cls( - "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" - ), - a - ) - ) - ), - div( - cls("flex-1 min-w-0"), - p( - cls("text-sm font-medium text-gray-900"), - title, - span(cls("float-right"), right) - ), - subtitle.map(st => - p( - cls("text-sm text-gray-500 truncate"), - st - ) - ) - ) - ) - ) - - override def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = - ul( - cls("relative z-0 divide-y divide-gray-200"), - role("list"), - children - ) - - override def listSection( - header: String, - list: HtmlElement - ): Div = - div( - cls("relative"), - div( - cls( - "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" - ), - header - ), - list - ) - - override def navigation(sections: Modifier[HtmlElement]): HtmlElement = - navTag( - cls("flex-1 min-h-0 overflow-y-auto"), - sections - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala deleted file mode 100644 index 5b5490f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ModalComponentsModule: - - def modal: ModalComponents - - trait ModalComponents: - def modalDialog( - content: HtmlElement, - onClose: Modifier[HtmlElement] - ): HtmlElement - -trait DefaultModalComponentsModule(using ctx: ComponentContext) - extends ModalComponentsModule: - - override val modal: ModalComponents = new ModalComponents: - - override def modalDialog( - content: HtmlElement, - closeMod: Modifier[HtmlElement] - ): 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( - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")), - closeMod - ) - ) - - div( - cls("fixed inset-0 z-20 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - 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-7xl sm:align-middle" - ), - content - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala deleted file mode 100644 index b713d8a..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala +++ /dev/null @@ -1,71 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait PageComponentsModule: - - val page: PageComponents - - trait PageComponents: - def container( - children: Modifier[HtmlElement]* - ): HtmlElement - - def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement - - def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement] = emptyMod, - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement - - /** Visage for clickable text, like links - */ - def clickable: Modifier[HtmlElement] - -trait DefaultPageComponentsModule(using ComponentContext) - extends PageComponentsModule: - - override val page: PageComponents = new PageComponents: - override def container( - children: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), - children - ) - - override def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement = - div( - cls("p-8 bg-gray-100 h-full"), - header, - children - ) - - override def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement = - div( - cls("pb-5 border-b border-gray-200"), - div(cls("float-right"), right), - h1( - cls("text-2xl leading-6 font-medium text-gray-900"), - title - ), - subtitle.map( - p( - cls("text-sm font-medium text-gray-500"), - _ - ) - ) - ) - - override def clickable: Modifier[HtmlElement] = - cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala deleted file mode 100644 index f70ee87..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala +++ /dev/null @@ -1,108 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} - -trait TableComponentsModule: - def tables: TableComponents - - trait TableComponents: - def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )(table: Modifier[HtmlElement]*): HtmlElement - - def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement - - def headerRow(mods: Modifier[HtmlElement]*)( - cells: HtmlElement* - ): HtmlElement - - def dataRow(mods: Modifier[HtmlElement]*)(cells: HtmlElement*): HtmlElement - - def headerCell(content: Modifier[HtmlElement]): HtmlElement - - def dataCell(content: Modifier[HtmlElement]): HtmlElement - -trait DefaultTableComponentsModule extends TableComponentsModule: - - override lazy val tables: TableComponents = new TableComponents: - - override def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )( - table: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("px-4 sm:px-6 lg:px-8"), - div( - cls("sm:flex sm:items-center"), - div( - cls("sm:flex-auto"), - h1(cls("text-base font-semibold leading-6 text-gray-900"), title), - subtitle.map(st => p(cls("mt-2 text-sm text-gray-700"), st)) - ), - div(cls("mt-4 sm:ml-16 sm:mt-0 sm:flex-none"), actions) - ), - div( - cls("mt-8 flow-root"), - div( - cls("-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"), - div( - cls("inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"), - table - ) - ) - ) - ) - - override def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement = - table( - cls("min-w-full divide-y divide-gray-300"), - thead(header), - tbody(cls("divide-y divide-gray-200"), body) - ) - - override def headerRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-3.5 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-3.5 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-3.5 px-3")) - ) - ) - - override def dataRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - mods, - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-4 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-4 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-4 px-3")) - ) - ) - - override def headerCell(content: Modifier[HtmlElement]): HtmlElement = - th( - cls("text-left text-sm font-semibold text-gray-900"), - content - ) - - override def dataCell(content: Modifier[HtmlElement]): HtmlElement = - td( - cls("whitespace-nowrap text-sm text-gray-500"), - content - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala index b937d45..12d01ae 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala @@ -3,7 +3,7 @@ import com.raquo.laminar.api.L.{*, given} import works.iterative.ui.components.laminar.HtmlTabular import works.iterative.ui.model.tables.Tabular -import works.iterative.ui.components.laminar.TableComponentsModule +import works.iterative.ui.components.laminar.tailwind.ui.TableComponentsModule import works.iterative.core.UserMessage import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given import works.iterative.ui.components.tailwind.ComponentContext diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala new file mode 100644 index 0000000..87f3e8d --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala @@ -0,0 +1,54 @@ +package works.iterative.ui.components.laminar.tailwind.color + +import com.raquo.laminar.api.L.{*, given} + +/** Complete color definition that can be rendered to CSS. + * + * Includes the area, kind and weight of the color. + */ +case class Color(area: ColorArea, color: ColorDef): + def toCSS: String = s"${area.name}-${color.toCSS}" + +object Color: + import ColorDef.given + + given colorToCSS: Conversion[Color, HtmlMod] with + def apply(c: Color) = cls(c.toCSS) + + given colorToSVGCSS: Conversion[Color, SvgMod] with + def apply(c: Color) = svg.cls(c.toCSS) + + given colorSignalToCSS: Conversion[Signal[Color], HtmlMod] with + def apply(c: Signal[Color]) = cls <-- c.map(_.toCSS) + + given colorSignalToSVGCSS: Conversion[Signal[Color], SvgMod] with + def apply(c: Signal[Color]) = svg.cls <-- c.map(_.toCSS) + + def current = ColorDef(ColorKind.current) + def inherit = ColorDef(ColorKind.inherit) + def transp = ColorDef(ColorKind.transp) + def auto = ColorDef(ColorKind.auto) + def black = ColorDef(ColorKind.black) + def white = ColorDef(ColorKind.white) + def slate(weight: ColorWeight) = ColorDef(ColorKind.slate, weight) + def gray(weight: ColorWeight) = ColorDef(ColorKind.gray, weight) + def zinc(weight: ColorWeight) = ColorDef(ColorKind.zinc, weight) + def neutral(weight: ColorWeight) = ColorDef(ColorKind.neutral, weight) + def stone(weight: ColorWeight) = ColorDef(ColorKind.stone, weight) + def red(weight: ColorWeight) = ColorDef(ColorKind.red, weight) + def orange(weight: ColorWeight) = ColorDef(ColorKind.orange, weight) + def amber(weight: ColorWeight) = ColorDef(ColorKind.amber, weight) + def yellow(weight: ColorWeight) = ColorDef(ColorKind.yellow, weight) + def lime(weight: ColorWeight) = ColorDef(ColorKind.lime, weight) + def green(weight: ColorWeight) = ColorDef(ColorKind.green, weight) + def emerald(weight: ColorWeight) = ColorDef(ColorKind.emerald, weight) + def teal(weight: ColorWeight) = ColorDef(ColorKind.teal, weight) + def cyan(weight: ColorWeight) = ColorDef(ColorKind.cyan, weight) + def sky(weight: ColorWeight) = ColorDef(ColorKind.sky, weight) + def blue(weight: ColorWeight) = ColorDef(ColorKind.blue, weight) + def indigo(weight: ColorWeight) = ColorDef(ColorKind.indigo, weight) + def violet(weight: ColorWeight) = ColorDef(ColorKind.violet, weight) + def purple(weight: ColorWeight) = ColorDef(ColorKind.purple, weight) + def fuchsia(weight: ColorWeight) = ColorDef(ColorKind.fuchsia, weight) + def pink(weight: ColorWeight) = ColorDef(ColorKind.pink, weight) + def rose(weight: ColorWeight) = ColorDef(ColorKind.rose, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala new file mode 100644 index 0000000..4130840 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala @@ -0,0 +1,16 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines the area the color should apply to, eg. background, text, border, + * etc. + */ +enum ColorArea(val name: String): + case bg extends ColorArea("bg") + case text extends ColorArea("text") + case decoration extends ColorArea("decoration") + case border extends ColorArea("border") + case outline extends ColorArea("outline") + case divide extends ColorArea("divide") + case ring extends ColorArea("ring") + case ringOffset extends ColorArea("ring-offset") + case shadow extends ColorArea("shadow") + case accent extends ColorArea("accent") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala new file mode 100644 index 0000000..fab9404 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala @@ -0,0 +1,43 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** A combination of ColorKind and ColorWeight, if applicable. + * + * By applying area we get the full Color definition. + */ +sealed trait ColorDef: + def toCSS: String + + def bg = Color(ColorArea.bg, this) + def text = Color(ColorArea.text, this) + def decoration = Color(ColorArea.decoration, this) + def border = Color(ColorArea.border, this) + def outline = Color(ColorArea.outline, this) + def divide = Color(ColorArea.divide, this) + def ring = Color(ColorArea.ring, this) + def ringOffset = Color(ColorArea.ringOffset, this) + def shadow = Color(ColorArea.shadow, this) + def accent = Color(ColorArea.accent, this) + +// TODO: create a macro that will output the tailwind class as string directly during compilation, where possible. +object ColorDef: + case class WeightedColorDef( + kind: ColorKind, + weight: ColorWeight + ) extends ColorDef: + override def toCSS: String = s"${kind.name}-${weight.value}" + + case class UnweightedColorDef( + kind: ColorKind + ) extends ColorDef: + override def toCSS: String = kind.name + + // TODO: check that the kind is valid unweighted kind + // that means current, inherit, auto, transparent, black, white + // tried using implicit evidence, but the type inference for enumerations + // tends to generalize to the enum, instead of the real type + def apply[T <: ColorKind](kind: T)(using + ev: T <:< ColorKind.Unweighted + ): ColorDef = + UnweightedColorDef(kind) + def apply(kind: ColorKind, weight: ColorWeight): ColorDef = + WeightedColorDef(kind, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala new file mode 100644 index 0000000..865166b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala @@ -0,0 +1,47 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines what color should be used, without specifying the area or weight. + */ +sealed abstract class ColorKind private (val name: String): + def apply(weight: ColorWeight): ColorDef = + ColorDef.WeightedColorDef(this, weight) + +object ColorKind: + trait Unweighted: + self: ColorKind => + override def apply(weight: ColorWeight): ColorDef = + ColorDef.UnweightedColorDef(self) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case object current extends ColorKind("current") with Unweighted + case object inherit extends ColorKind("inherit") with Unweighted + // Not present in for all methods + case object transp extends ColorKind("transparent") with Unweighted + // Seen in accent, not preset otherwise + case object auto extends ColorKind("auto") with Unweighted + // Black and white do not have weight + case object black extends ColorKind("black") with Unweighted + case object white extends ColorKind("white") with Unweighted + case object slate extends ColorKind("slate") + case object gray extends ColorKind("gray") + case object zinc extends ColorKind("zinc") + case object neutral extends ColorKind("neutral") + case object stone extends ColorKind("stone") + case object red extends ColorKind("red") + case object orange extends ColorKind("orange") + case object amber extends ColorKind("amber") + case object yellow extends ColorKind("yellow") + case object lime extends ColorKind("lime") + case object green extends ColorKind("green") + case object emerald extends ColorKind("emerald") + case object teal extends ColorKind("teal") + case object cyan extends ColorKind("cyan") + case object sky extends ColorKind("sky") + case object blue extends ColorKind("blue") + case object indigo extends ColorKind("indigo") + case object violet extends ColorKind("violet") + case object purple extends ColorKind("purple") + case object fuchsia extends ColorKind("fuchsia") + case object pink extends ColorKind("pink") + case object rose extends ColorKind("rose") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala new file mode 100644 index 0000000..6f1a4cc --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala @@ -0,0 +1,33 @@ +package works.iterative.ui.components.laminar.tailwind.color + +opaque type ColorWeight = String + +extension (c: ColorWeight) def value: String = c + +/** Defines weight of a color, eg. 50, 100, 200, etc. + * + * Tailwind-like. + */ +object ColorWeight: + inline given int50: Conversion[50, ColorWeight] with + inline def apply(i: 50) = "50" + inline given int100: Conversion[100, ColorWeight] with + inline def apply(i: 100) = "100" + inline given int200: Conversion[200, ColorWeight] with + inline def apply(i: 200) = "200" + inline given int300: Conversion[300, ColorWeight] with + inline def apply(i: 300) = "300" + inline given int400: Conversion[400, ColorWeight] with + inline def apply(i: 400) = "400" + inline given int500: Conversion[500, ColorWeight] with + inline def apply(i: 500) = "500" + inline given int600: Conversion[600, ColorWeight] with + inline def apply(i: 600) = "600" + inline given int700: Conversion[700, ColorWeight] with + inline def apply(i: 700) = "700" + inline given int800: Conversion[800, ColorWeight] with + inline def apply(i: 800) = "800" + inline given int900: Conversion[900, ColorWeight] with + inline def apply(i: 900) = "900" + inline given int950: Conversion[950, ColorWeight] with + inline def apply(i: 950) = "950" diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala new file mode 100644 index 0000000..21e21f0 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala @@ -0,0 +1,12 @@ +package works.iterative.ui.components.laminar.tailwind + +/** We need a generic Color model that can be used both on server and on client. + * + * We have adopted the Tailwind model for now. There is nothing inherently + * Tailwind-specific in the implementation, but all the values are taken from + * their palette and the area model is very HTML biased. + * + * Still, I think that it is a good starting point for exploration and will + * satisfy our current needs. + */ +package object color {} diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" new file mode 100644 index 0000000..5c4e218 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" @@ -0,0 +1,21 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.model.color.ColorKind +import works.iterative.ui.components.tailwind.ComponentContext +import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given +import works.iterative.ui.model.Tag + +trait TailwindUIBadgesComponentsModule(using ComponentContext): + + object badges: + override def tag(name: String, color: ColorKind): HtmlElement = + p( + cls( + "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" + ), + color(800).text, + color(100).bg, + name + ) diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" new file mode 100644 index 0000000..0c9ca25 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" @@ -0,0 +1,24 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} + +trait TailwindUIHeadingsComponentsModule: + val headings: HeadingsComponents = new HeadingsComponents + + class HeadingsComponents: + def section(title: Node, actions: HtmlElement*): HtmlElement = + div( + cls("border-b border-gray-200 pb-5"), + div( + cls("sm:flex sm:items-center sm:justify-between"), + h3(cls("text-base font-semibold leading-6 text-gray-900"), title), + div( + cls("mt-3 flex sm:ml-4 sm:mt-0"), + actions match + case Nil => emptyMod + case first :: rest => + first :: rest.map(_.amend(cls("ml-3"))) + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala new file mode 100644 index 0000000..16ca01b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala @@ -0,0 +1,19 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.laminar.tailwind.color.ColorKind + +trait BadgeComponentsModule: + + object badges: + def pill(name: String, color: ColorKind): HtmlElement = + p( + cls( + "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" + ), + color(800).text, + color(100).bg, + name + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala new file mode 100644 index 0000000..ebb2968 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} + +trait ButtonComponentsModule: + + object buttons: + + private inline def srHelp(text: String): Modifier[HtmlElement] = + span(cls := "sr-only", text) + + def primaryButton( + id: String, + text: Modifier[HtmlElement], + icon: Option[SvgElement] = None, + buttonType: String = "submit" + )(mods: Modifier[HtmlElement]*): HtmlElement = + button( + tpe(buttonType), + cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + icon, + text, + mods + ) + + def secondaryButton( + id: String, + text: Modifier[HtmlElement], + icon: Option[SvgElement] = None, + buttonType: String = "button" + )(mods: Modifier[HtmlElement]*): HtmlElement = + button( + tpe(buttonType), + cls( + "ml-2 bg-white inline-flex justify-center 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" + ), + icon, + text, + mods + ) + + def inlineButton(id: String, icon: SvgElement)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + button( + tpe := "button", + cls := "ml-1 inline-flex h-4 w-4 flex-shrink-0 rounded-full p-1 text-gray-400 hover:bg-gray-200 hover:text-gray-500", + srHelp(id), + icon + ) + + def iconButton(id: String, icon: SvgElement, srText: Option[String] = None)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + button( + tpe := "button", + cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", + icon, + srText.map(srHelp(_)) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala new file mode 100644 index 0000000..eb6dd5f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala @@ -0,0 +1,101 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import org.scalajs.dom.html +import com.raquo.laminar.modifiers.KeyUpdater + +trait FormComponentsModule: + self: IconsModule => + + object forms: + + def section( + title: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] + )(content: Modifier[HtmlElement]*): HtmlElement = + div( + cls("space-y-6 sm:space-y-5"), + div( + h3(cls("text-lg leading-6 font-medium text-gray-900"), title), + subtitle.map(st => p(cls("mt-1 max-w-2xl text-sm text-gray-500"), st)) + ), + div(cls("mt-6 sm:mt-5 space-y-6 sm:space-y-5"), content) + ) + + def label(labelText: String, forId: Option[String] = None)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + L.label( + cls("block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"), + forId.map(id => L.forId(id)), + labelText, + mods + ) + + def field( + label: Modifier[HtmlElement] + )(content: Modifier[HtmlElement]*): 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, + div(cls("mt-1 sm:mt-0 sm:col-span-2"), content) + ) + + def field( + id: String, + labelText: String, + input: HtmlElement, + help: Option[String] + ): HtmlElement = + field( + label(labelText, Some(id))() + )( + input.amend(idAttr(id)), + help.map(h => p(cls("mt-2 text-sm text-gray-500"), h)) + ) + + def form(mods: Modifier[HtmlElement]*)( + sections: Modifier[HtmlElement]* + )(actions: Modifier[HtmlElement]*): HtmlElement = + L.form( + cls("space-y-8 divide-y divide-gray-200"), + mods, + sections, + div( + cls("pt-5"), + div(cls("flex justify-end"), actions) + ) + ) + + def inlineForm( + mods: Modifier[HtmlElement]* + ): HtmlElement = + L.form(cls("flex space-x-4"), mods) + + def inputField( + id: String, + labelText: String, + placeholderText: Option[String] = None, + inputType: String = "text", + helpText: Option[String] = None + ): HtmlElement = + field( + id, + labelText, + input( + tpe(inputType), + cls( + "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:max-w-xs sm:text-sm sm:leading-6" + ), + placeholderText.map(placeholder(_)) + ), + helpText + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala new file mode 100644 index 0000000..52da07e --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala @@ -0,0 +1,22 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} + +trait HeadingsComponentsModule: + object headings: + def section(title: Node, actions: HtmlElement*): HtmlElement = + div( + cls("border-b border-gray-200 pb-5"), + div( + cls("sm:flex sm:items-center sm:justify-between"), + h3(cls("text-base font-semibold leading-6 text-gray-900"), title), + div( + cls("mt-3 flex sm:ml-4 sm:mt-0"), + actions match + case Nil => emptyMod + case first :: rest => + first :: rest.map(_.amend(cls("ml-3"))) + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala new file mode 100644 index 0000000..c543150 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala @@ -0,0 +1,112 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext +import works.iterative.ui.components.tailwind.CustomAttrs + +trait IconsModule: + object icons: + import svg.* + + object aria: + val hidden = CustomAttrs.svg.ariaHidden + + private def withDefault( + mods: Seq[Modifier[SvgElement]], + default: Modifier[SvgElement] + ): Modifier[SvgElement] = + if mods.isEmpty then default else mods + + def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-8 w-8"), + 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" + ) + ) + ) + + def close(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-2 w-2"), + stroke := "currentColor", + fill := "none", + viewBox := "0 0 8 8", + path( + strokeLineCap := "round", + strokeWidth := "1.5", + d := "M1 1l6 6m0-6L1 7" + ) + ) + + def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls("h-5 w-5 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 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") + ) + ) + + def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls("h-5 w-5 text-gray-400")), + 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") + ) + ) + + def `document-chart-bar-outline`( + mods: Modifier[SvgElement]* + ): SvgElement = + svg( + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + strokeWidth := "1.5", + stroke := "currentColor", + withDefault(mods, cls := "h-6 w-6"), + path( + strokeLineCap := "round", + strokeLineJoin := "round", + d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" + ) + ) + + def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-5 w-5"), + cls := "flex-shrink-0 text-gray-400", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", + clipRule := "evenodd" + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala new file mode 100644 index 0000000..02cbdb6 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala @@ -0,0 +1,11 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait LayoutModule: + object layout: + def card(content: Modifier[HtmlElement]*): HtmlElement = + div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala new file mode 100644 index 0000000..b652da9 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala @@ -0,0 +1,88 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.laminar.nodes.ReactiveHtmlElement +import org.scalajs.dom.html.Paragraph +import com.raquo.laminar.nodes.TextNode +import works.iterative.ui.components.laminar.tailwind.color.ColorKind + +trait ListComponentsModule: + self: BadgeComponentsModule => + + object list: + def label( + text: String, + color: ColorKind + ): HtmlElement = badges.pill(text, color) + + def item( + title: String, + subtitle: Option[String], + right: Modifier[HtmlElement] = emptyMod, + avatar: Option[Modifier[HtmlElement]] = None + ): LI = + li( + cls("group"), + div( + cls( + "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" + ), + avatar.map(a => + div( + cls("flex-shrink-0"), + div( + cls( + "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" + ), + a + ) + ) + ), + div( + cls("flex-1 min-w-0"), + p( + cls("text-sm font-medium text-gray-900"), + title, + span(cls("float-right"), right) + ), + subtitle.map(st => + p( + cls("text-sm text-gray-500 truncate"), + st + ) + ) + ) + ) + ) + + def unordered( + children: Modifier[HtmlElement] + ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = + ul( + cls("relative z-0 divide-y divide-gray-200"), + role("list"), + children + ) + + def listSection( + header: String, + list: HtmlElement + ): Div = + div( + cls("relative"), + div( + cls( + "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" + ), + header + ), + list + ) + + def navigation(sections: Modifier[HtmlElement]): HtmlElement = + navTag( + cls("flex-1 min-h-0 overflow-y-auto"), + sections + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ModalComponentsModule.scala new file mode 100644 index 0000000..6027404 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ModalComponentsModule.scala @@ -0,0 +1,54 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait ModalComponentsModule: + object modal: + def modalDialog( + content: HtmlElement, + closeMod: Modifier[HtmlElement] + ): 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( + cls("fixed inset-0 transition-opacity"), + div(cls("absolute inset-0 bg-gray-500 opacity-75")), + closeMod + ) + ) + + div( + cls("fixed inset-0 z-20 overflow-y-auto"), + div( + cls("text-center sm:block sm:p-0"), + overlay, + browserCenteringModalTrick, + 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-7xl sm:align-middle" + ), + content + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/PageComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/PageComponentsModule.scala new file mode 100644 index 0000000..d5a422e --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/PageComponentsModule.scala @@ -0,0 +1,49 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait PageComponentsModule: + + object page: + def container( + children: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), + children + ) + + def singleColumn( + header: Modifier[HtmlElement] + )(children: Modifier[HtmlElement]*): HtmlElement = + div( + cls("p-8 bg-gray-100 h-full"), + header, + children + ) + + def pageHeader( + title: Modifier[HtmlElement], + right: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] = None + ): HtmlElement = + div( + cls("pb-5 border-b border-gray-200"), + div(cls("float-right"), right), + h1( + cls("text-2xl leading-6 font-medium text-gray-900"), + title + ), + subtitle.map( + p( + cls("text-sm font-medium text-gray-500"), + _ + ) + ) + ) + + def clickable: Modifier[HtmlElement] = + cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TableComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TableComponentsModule.scala new file mode 100644 index 0000000..417ea56 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TableComponentsModule.scala @@ -0,0 +1,86 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L +import com.raquo.laminar.api.L.{*, given} + +trait TableComponentsModule: + + object tables: + + def tableSection( + title: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] = None, + actions: Modifier[HtmlElement]* + )( + table: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls("px-4 sm:px-6 lg:px-8"), + div( + cls("sm:flex sm:items-center"), + div( + cls("sm:flex-auto"), + h1(cls("text-base font-semibold leading-6 text-gray-900"), title), + subtitle.map(st => p(cls("mt-2 text-sm text-gray-700"), st)) + ), + div(cls("mt-4 sm:ml-16 sm:mt-0 sm:flex-none"), actions) + ), + div( + cls("mt-8 flow-root"), + div( + cls("-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"), + div( + cls("inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"), + table + ) + ) + ) + ) + + def simpleTable(header: Modifier[HtmlElement]*)( + body: Modifier[HtmlElement]* + ): HtmlElement = + table( + cls("min-w-full divide-y divide-gray-300"), + thead(header), + tbody(cls("divide-y divide-gray-200"), body) + ) + + def headerRow( + mods: Modifier[HtmlElement]* + )(cells: HtmlElement*): HtmlElement = + tr( + cells.zipWithIndex.map((c, i) => + if i == 0 then c.amend(cls("py-3.5 pl-4 pr-3 sm:pl-0")) + else if i == cells.length - 1 then + c.amend(cls("py-3.5 pr-4 pl-3 sm:pr-0")) + else c.amend(cls("py-3.5 px-3")) + ) + ) + + def dataRow( + mods: Modifier[HtmlElement]* + )(cells: HtmlElement*): HtmlElement = + tr( + mods, + cells.zipWithIndex.map((c, i) => + if i == 0 then c.amend(cls("py-4 pl-4 pr-3 sm:pl-0")) + else if i == cells.length - 1 then + c.amend(cls("py-4 pr-4 pl-3 sm:pr-0")) + else c.amend(cls("py-4 px-3")) + ) + ) + + def headerCell(content: Modifier[HtmlElement]): HtmlElement = + th( + cls("text-left text-sm font-semibold text-gray-900"), + content + ) + + def dataCell(content: Modifier[HtmlElement]): HtmlElement = + td( + cls("whitespace-nowrap text-sm text-gray-500"), + content + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TailwindUICatalogue.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TailwindUICatalogue.scala new file mode 100644 index 0000000..5027b10 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TailwindUICatalogue.scala @@ -0,0 +1,17 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} + +trait TailwindUICatalogueModule + extends BadgeComponentsModule + with ButtonComponentsModule + with HeadingsComponentsModule + with IconsModule + with FormComponentsModule + with LayoutModule + with ListComponentsModule + with ModalComponentsModule + with PageComponentsModule + with TableComponentsModule diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/LabelsOnLeftFormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/LabelsOnLeftFormComponentsModule.scala deleted file mode 100644 index 210bbf8..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/LabelsOnLeftFormComponentsModule.scala +++ /dev/null @@ -1,155 +0,0 @@ -package works.iterative.ui.components.laminar -package tailwindui - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -trait LabelsOnLeftFormComponentsModule(using ctx: ComponentContext) - extends FormComponentsModule: - self: IconsModule => - override val forms = new FormComponents: - - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )(content: Modifier[HtmlElement]*): HtmlElement = - div( - cls("space-y-6 sm:space-y-5"), - div( - h3(cls("text-lg leading-6 font-medium text-gray-900"), title), - subtitle.map(st => p(cls("mt-1 max-w-2xl text-sm text-gray-500"), st)) - ), - div(cls("mt-6 sm:mt-5 space-y-6 sm:space-y-5"), content) - ) - - override def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement = - L.label( - cls("block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"), - forId.map(id => L.forId(id)), - labelText, - mods - ) - - override def field( - label: Modifier[HtmlElement] - )(content: Modifier[HtmlElement]*): 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, - div(cls("mt-1 sm:mt-0 sm:col-span-2"), content) - ) - - override def field( - id: String, - labelText: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement = - field( - label(labelText, Some(id))() - )( - input.amend(idAttr(id)), - help.map(h => p(cls("mt-2 text-sm text-gray-500"), h)) - ) - - override def form(mods: Modifier[HtmlElement]*)( - sections: Modifier[HtmlElement]* - )(actions: Modifier[HtmlElement]*): HtmlElement = - L.form( - cls("space-y-8 divide-y divide-gray-200"), - mods, - sections, - div( - cls("pt-5"), - div(cls("flex justify-end"), actions) - ) - ) - - override def inlineForm( - mods: Modifier[HtmlElement]* - ): HtmlElement = - L.form(cls("flex space-x-4"), mods) - - override def searchField( - id: String, - placeholderText: Option[String] = None - )( - mods: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "flex-1 min-w-0", - label(ctx.messages("forms.search.label"), Some(id))(cls("sr-only")), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - icons.`search-solid`() - ), - input( - tpe := "search", - nameAttr := "search", - idAttr := id, - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholderText - .orElse( - ctx.messages - .opt( - s"forms.search.${id}.placeholder", - s"form.search.placeholder" - ) - ) - .map(placeholder(_)), - mods - ) - ) - ) - - override def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement = - field( - id, - labelText, - input( - tpe(inputType), - cls( - "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:max-w-xs sm:text-sm sm:leading-6" - ), - placeholderText.map(placeholder(_)) - ), - helpText - ) - - override def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement = - div( - labelText, - input( - idAttr(id), - nameAttr(id), - autoComplete("date"), - tpe("date"), - cls( - "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ), - placeholderText.map(placeholder(_)), - mods(localDateSelect) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/TailwindUIButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/TailwindUIButtonComponentsModule.scala deleted file mode 100644 index 4c44157..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/TailwindUIButtonComponentsModule.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar -package tailwindui - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait TailwindUIButtonComponentsModule(using ctx: ComponentContext) - extends ButtonComponentsModule: - - override val buttons = new ButtonComponents: - - private inline def srHelp(id: String): Modifier[HtmlElement] = - ctx.messages - .opt(s"form.button.${id}.screenReaderHelp") - .map(sr => span(cls := "sr-only", sr)) - - override def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )(mods: Modifier[HtmlElement]*): HtmlElement = - button( - tpe(buttonType), - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - icon, - text, - mods - ) - - override def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )(mods: Modifier[HtmlElement]*): HtmlElement = - button( - tpe(buttonType), - cls( - "ml-2 bg-white inline-flex justify-center 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" - ), - icon, - text, - mods - ) - - override def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement = - button( - tpe := "button", - cls := "ml-1 inline-flex h-4 w-4 flex-shrink-0 rounded-full p-1 text-gray-400 hover:bg-gray-200 hover:text-gray-500", - srHelp(id), - icon - ) - - override def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement = - button( - tpe := "button", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - icon, - srHelp(id) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/tailwind/laminar/LaminarExtensions.scala b/ui/js/src/main/scala/works/iterative/ui/components/tailwind/laminar/LaminarExtensions.scala index 4c5e68b..4c87800 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/tailwind/laminar/LaminarExtensions.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/tailwind/laminar/LaminarExtensions.scala @@ -2,23 +2,9 @@ package laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.Color import works.iterative.core.UserMessage object LaminarExtensions: - given colorToCSS: Conversion[Color, Modifier[HtmlElement]] with - def apply(c: Color) = cls(c.toCSS) - - given colorToSVGCSS: Conversion[Color, Modifier[SvgElement]] with - def apply(c: Color) = svg.cls(c.toCSS) - - given colorSignalToCSS: Conversion[Signal[Color], Modifier[HtmlElement]] with - def apply(c: Signal[Color]) = cls <-- c.map(_.toCSS) - - given colorSignalToSVGCSS: Conversion[Signal[Color], Modifier[SvgElement]] - with - def apply(c: Signal[Color]) = svg.cls <-- c.map(_.toCSS) - inline given userMessageToModifier(using ctx: ComponentContext ): Conversion[UserMessage, Modifier[HtmlElement]] with diff --git a/ui/js/src/main/scala/works/iterative/ui/scenarios/Scenario.scala b/ui/js/src/main/scala/works/iterative/ui/scenarios/Scenario.scala index 99a212a..60de9f0 100644 --- a/ui/js/src/main/scala/works/iterative/ui/scenarios/Scenario.scala +++ b/ui/js/src/main/scala/works/iterative/ui/scenarios/Scenario.scala @@ -13,8 +13,6 @@ import works.iterative.core.MessageCatalogue import works.iterative.ui.components.tailwind.ComponentContext import works.iterative.ui.components.tailwind.StyleGuide -import works.iterative.ui.model.color.Color -import works.iterative.ui.model.color.ColorWeight object Scenario: type Id = String diff --git a/ui/shared/src/main/scala/works/iterative/ui/model/Tag.scala b/ui/shared/src/main/scala/works/iterative/ui/model/Tag.scala index 7352579..958aa75 100644 --- a/ui/shared/src/main/scala/works/iterative/ui/model/Tag.scala +++ b/ui/shared/src/main/scala/works/iterative/ui/model/Tag.scala @@ -1,10 +1,16 @@ package works.iterative.ui.model -import works.iterative.ui.model.color.ColorKind - -/** Representation of colored string value. +/** Representation of a label or tag. * - * Used generally to represent tags or "labels", eg. some kind of status or - * categorization. + * The UI renderer will recognize this value as tag and render accordingly. The + * value is used to determine both the color of the tag and the text displayed. + * + * @param value + * the value of the tag */ -final case class Tag(value: String, color: ColorKind = ColorKind.gray) +opaque type Tag = String + +object Tag: + def apply(value: String): Tag = value + + extension (tag: Tag) def value: String = tag diff --git a/ui/shared/src/main/scala/works/iterative/ui/model/color/Color.scala b/ui/shared/src/main/scala/works/iterative/ui/model/color/Color.scala deleted file mode 100644 index 09d6e4d..0000000 --- a/ui/shared/src/main/scala/works/iterative/ui/model/color/Color.scala +++ /dev/null @@ -1,40 +0,0 @@ -package works.iterative.ui.model.color - -/** Complete color definition that can be rendered to CSS. - * - * Includes the area, kind and weight of the color. - */ -case class Color(area: ColorArea, color: ColorDef): - def toCSS: String = s"${area.name}-${color.toCSS}" - -object Color: - import ColorDef.given - - def current = ColorDef(ColorKind.current) - def inherit = ColorDef(ColorKind.inherit) - def transp = ColorDef(ColorKind.transp) - def auto = ColorDef(ColorKind.auto) - def black = ColorDef(ColorKind.black) - def white = ColorDef(ColorKind.white) - def slate(weight: ColorWeight) = ColorDef(ColorKind.slate, weight) - def gray(weight: ColorWeight) = ColorDef(ColorKind.gray, weight) - def zinc(weight: ColorWeight) = ColorDef(ColorKind.zinc, weight) - def neutral(weight: ColorWeight) = ColorDef(ColorKind.neutral, weight) - def stone(weight: ColorWeight) = ColorDef(ColorKind.stone, weight) - def red(weight: ColorWeight) = ColorDef(ColorKind.red, weight) - def orange(weight: ColorWeight) = ColorDef(ColorKind.orange, weight) - def amber(weight: ColorWeight) = ColorDef(ColorKind.amber, weight) - def yellow(weight: ColorWeight) = ColorDef(ColorKind.yellow, weight) - def lime(weight: ColorWeight) = ColorDef(ColorKind.lime, weight) - def green(weight: ColorWeight) = ColorDef(ColorKind.green, weight) - def emerald(weight: ColorWeight) = ColorDef(ColorKind.emerald, weight) - def teal(weight: ColorWeight) = ColorDef(ColorKind.teal, weight) - def cyan(weight: ColorWeight) = ColorDef(ColorKind.cyan, weight) - def sky(weight: ColorWeight) = ColorDef(ColorKind.sky, weight) - def blue(weight: ColorWeight) = ColorDef(ColorKind.blue, weight) - def indigo(weight: ColorWeight) = ColorDef(ColorKind.indigo, weight) - def violet(weight: ColorWeight) = ColorDef(ColorKind.violet, weight) - def purple(weight: ColorWeight) = ColorDef(ColorKind.purple, weight) - def fuchsia(weight: ColorWeight) = ColorDef(ColorKind.fuchsia, weight) - def pink(weight: ColorWeight) = ColorDef(ColorKind.pink, weight) - def rose(weight: ColorWeight) = ColorDef(ColorKind.rose, weight) diff --git a/ui/shared/src/main/scala/works/iterative/ui/model/color/ColorArea.scala b/ui/shared/src/main/scala/works/iterative/ui/model/color/ColorArea.scala deleted file mode 100644 index 1211287..0000000 --- a/ui/shared/src/main/scala/works/iterative/ui/model/color/ColorArea.scala +++ /dev/null @@ -1,16 +0,0 @@ -package works.iterative.ui.model.color - -/** Defines the area the color should apply to, eg. background, text, border, - * etc. - */ -enum ColorArea(val name: String): - case bg extends ColorArea("bg") - case text extends ColorArea("text") - case decoration extends ColorArea("decoration") - case border extends ColorArea("border") - case outline extends ColorArea("outline") - case divide extends ColorArea("divide") - case ring extends ColorArea("ring") - case ringOffset extends ColorArea("ring-offset") - case shadow extends ColorArea("shadow") - case accent extends ColorArea("accent") diff --git a/ui/shared/src/main/scala/works/iterative/ui/model/color/ColorDef.scala b/ui/shared/src/main/scala/works/iterative/ui/model/color/ColorDef.scala deleted file mode 100644 index 9c5ec61..0000000 --- a/ui/shared/src/main/scala/works/iterative/ui/model/color/ColorDef.scala +++ /dev/null @@ -1,43 +0,0 @@ -package works.iterative.ui.model.color - -/** A combination of ColorKind and ColorWeight, if applicable. - * - * By applying area we get the full Color definition. - */ -sealed trait ColorDef: - def toCSS: String - - def bg = Color(ColorArea.bg, this) - def text = Color(ColorArea.text, this) - def decoration = Color(ColorArea.decoration, this) - def border = Color(ColorArea.border, this) - def outline = Color(ColorArea.outline, this) - def divide = Color(ColorArea.divide, this) - def ring = Color(ColorArea.ring, this) - def ringOffset = Color(ColorArea.ringOffset, this) - def shadow = Color(ColorArea.shadow, this) - def accent = Color(ColorArea.accent, this) - -// TODO: create a macro that will output the tailwind class as string directly during compilation, where possible. -object ColorDef: - case class WeightedColorDef( - kind: ColorKind, - weight: ColorWeight - ) extends ColorDef: - override def toCSS: String = s"${kind.name}-${weight.value}" - - case class UnweightedColorDef( - kind: ColorKind - ) extends ColorDef: - override def toCSS: String = kind.name - - // TODO: check that the kind is valid unweighted kind - // that means current, inherit, auto, transparent, black, white - // tried using implicit evidence, but the type inference for enumerations - // tends to generalize to the enum, instead of the real type - def apply[T <: ColorKind](kind: T)(using - ev: T <:< ColorKind.Unweighted - ): ColorDef = - UnweightedColorDef(kind) - def apply(kind: ColorKind, weight: ColorWeight): ColorDef = - WeightedColorDef(kind, weight) diff --git a/ui/shared/src/main/scala/works/iterative/ui/model/color/ColorKind.scala b/ui/shared/src/main/scala/works/iterative/ui/model/color/ColorKind.scala deleted file mode 100644 index ea24372..0000000 --- a/ui/shared/src/main/scala/works/iterative/ui/model/color/ColorKind.scala +++ /dev/null @@ -1,47 +0,0 @@ -package works.iterative.ui.model.color - -/** Defines what color should be used, without specifying the area or weight. - */ -sealed abstract class ColorKind private (val name: String): - def apply(weight: ColorWeight): ColorDef = - ColorDef.WeightedColorDef(this, weight) - -object ColorKind: - trait Unweighted: - self: ColorKind => - override def apply(weight: ColorWeight): ColorDef = - ColorDef.UnweightedColorDef(self) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case object current extends ColorKind("current") with Unweighted - case object inherit extends ColorKind("inherit") with Unweighted - // Not present in for all methods - case object transp extends ColorKind("transparent") with Unweighted - // Seen in accent, not preset otherwise - case object auto extends ColorKind("auto") with Unweighted - // Black and white do not have weight - case object black extends ColorKind("black") with Unweighted - case object white extends ColorKind("white") with Unweighted - case object slate extends ColorKind("slate") - case object gray extends ColorKind("gray") - case object zinc extends ColorKind("zinc") - case object neutral extends ColorKind("neutral") - case object stone extends ColorKind("stone") - case object red extends ColorKind("red") - case object orange extends ColorKind("orange") - case object amber extends ColorKind("amber") - case object yellow extends ColorKind("yellow") - case object lime extends ColorKind("lime") - case object green extends ColorKind("green") - case object emerald extends ColorKind("emerald") - case object teal extends ColorKind("teal") - case object cyan extends ColorKind("cyan") - case object sky extends ColorKind("sky") - case object blue extends ColorKind("blue") - case object indigo extends ColorKind("indigo") - case object violet extends ColorKind("violet") - case object purple extends ColorKind("purple") - case object fuchsia extends ColorKind("fuchsia") - case object pink extends ColorKind("pink") - case object rose extends ColorKind("rose") diff --git a/ui/shared/src/main/scala/works/iterative/ui/model/color/ColorWeight.scala b/ui/shared/src/main/scala/works/iterative/ui/model/color/ColorWeight.scala deleted file mode 100644 index 7767ac5..0000000 --- a/ui/shared/src/main/scala/works/iterative/ui/model/color/ColorWeight.scala +++ /dev/null @@ -1,33 +0,0 @@ -package works.iterative.ui.model.color - -opaque type ColorWeight = String - -extension (c: ColorWeight) def value: String = c - -/** Defines weight of a color, eg. 50, 100, 200, etc. - * - * Tailwind-like. - */ -object ColorWeight: - inline given int50: Conversion[50, ColorWeight] with - inline def apply(i: 50) = "50" - inline given int100: Conversion[100, ColorWeight] with - inline def apply(i: 100) = "100" - inline given int200: Conversion[200, ColorWeight] with - inline def apply(i: 200) = "200" - inline given int300: Conversion[300, ColorWeight] with - inline def apply(i: 300) = "300" - inline given int400: Conversion[400, ColorWeight] with - inline def apply(i: 400) = "400" - inline given int500: Conversion[500, ColorWeight] with - inline def apply(i: 500) = "500" - inline given int600: Conversion[600, ColorWeight] with - inline def apply(i: 600) = "600" - inline given int700: Conversion[700, ColorWeight] with - inline def apply(i: 700) = "700" - inline given int800: Conversion[800, ColorWeight] with - inline def apply(i: 800) = "800" - inline given int900: Conversion[900, ColorWeight] with - inline def apply(i: 900) = "900" - inline given int950: Conversion[950, ColorWeight] with - inline def apply(i: 950) = "950" diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala index a2c637c..bfa063a 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/Bins.scala @@ -1,13 +1,11 @@ package works.iterative.ui.components.laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorDef import com.raquo.laminar.tags.HtmlTag case class Bin[Source, +Value]( label: String, description: Option[String | HtmlElement], - color: ColorDef, valueOf: Source => Value ): def map[NewValue]( diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala deleted file mode 100644 index 3aac0bf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala +++ /dev/null @@ -1,34 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ButtonComponentsModule: - def buttons: ButtonComponents - - trait ButtonComponents: - def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala deleted file mode 100644 index 646e73d..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DashboardComponentsModule.scala +++ /dev/null @@ -1,191 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.color.ColorDef -import com.raquo.laminar.tags.HtmlTag -import org.scalajs.dom.html.UList -import com.raquo.laminar.nodes.ReactiveHtmlElement -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind - -trait DashboardComponentsModule: - def dashboard: DashboardComponents - - // Only methods that create HtmlElements - trait DashboardComponents(using ComponentContext): - def section(label: String, children: Modifier[HtmlElement]): Div - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI - def cardInitials( - initials: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div - def count(label: String, value: Int, color: ColorDef): Span - def counts(children: HtmlElement*): Span - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] - -trait DefaultDashboardComponentsModule(using ComponentContext) - extends DashboardComponentsModule: - override val dashboard: DashboardComponents = new DashboardComponents: - def section(label: String, children: Modifier[HtmlElement]): Div = - div( - cls("mt-3"), - h2( - cls("text-gray-500 text-xs font-medium uppercase tracking-wide"), - label - ), - children - ) - - def cardList(children: Modifier[HtmlElement]): ReactiveHtmlElement[UList] = - ul( - cls( - "mt-3 grid gap-5 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" - ), - children - ) - - def card( - id: String, - label: String, - initials: Div, - children: Modifier[HtmlElement] - ): LI = - li( - div( - cls("col-span-1 flex shadow-sm rounded-md"), - initials, - div( - cls( - "flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate" - ), - div( - cls("flex-1 px-4 py-2 text-sm truncate"), - div( - cls("text-gray-900 font-medium hover:text-gray-600 truncate"), - label - ), - p(cls("text-gray-500"), children) - ) - ) - ) - ) - - def cardInitials( - id: String, - color: Signal[ColorKind], - children: Modifier[HtmlElement] - ): Div = - div( - cls( - "flex-shrink-0 flex flex-col items-center justify-center w-16 text-white text-sm font-medium rounded-l-md" - ), - color.map(_(600).bg), - div(id), - children - ) - - def count(label: String, value: Int, color: ColorDef): Span = - span(color.text, title(label), s"${value}") - - def counts(children: HtmlElement*): Span = - span(interleave(children, span(" / "))) - - def table( - headers: List[String], - sections: Seq[ReactiveHtmlElement[org.scalajs.dom.html.TableRow]] - ): Div = - div( - cls("flex flex-col mt-2"), - div( - cls( - "align-middle min-w-full shadow sm:rounded-lg" - ), - L.table( - cls("min-w-full border-separate border-spacing-0"), - styleAttr("border-spacing: 0"), // Tailwind somehow doesn't work - thead( - tr( - cls("border-t border-gray-200"), - headers.map(h => - th( - cls( - "sticky top-0 z-10 px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - h - ) - ) - ) - ), - tbody(cls("bg-white divide-y divide-gray-100"), sections) - ) - ) - ) - - def tableSectionHeader( - label: Modifier[HtmlElement], - cs: Int, - mods: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - td( - cls("bg-gray-50 border-gray-200 border-t border-b"), - cls( - "sticky top-14 z-10 bg-gray-50 px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider" - ), - colSpan(cs), - label - ), - mods - ) - - def tableRow( - label: String, - mods: Modifier[HtmlElement], - children: HtmlElement* - ): ReactiveHtmlElement[org.scalajs.dom.html.TableRow] = - tr( - mods, - td( - cls( - "px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900 truncate" - ), - label - ), - children.map(e => - td( - cls( - "whitespace-nowrap px-2 py-2 text-sm text-gray-500" - ), - e - ) - ) - ) - - private def interleave( - a: Seq[HtmlElement], - separator: => HtmlElement - ): Seq[HtmlElement] = - if a.size < 2 then a - else a.init.flatMap(n => Seq(n, separator)) :+ a.last diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala deleted file mode 100644 index a45beaf..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/DetailComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.FileRef - -trait DetailComponentsModule: - - def details: DetailComponents - - trait DetailComponents: - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement - - def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement - - def files(fs: List[FileRef]): HtmlElement - - def file(f: FileRef): HtmlElement - -trait DefaultDetailComponentsModule(using ctx: ComponentContext) - extends DetailComponentsModule: - self: IconsModule => - - override val details: DetailComponents = new DetailComponents: - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]], - actions: Modifier[HtmlElement]* - )(fields: HtmlElement*): HtmlElement = - div( - div( - cls := "px-4 sm:px-0", - h3( - cls := "text-base font-semibold leading-7 text-gray-900", - title - ), - subtitle.map(st => - p( - cls := "mt-1 max-w-2xl text-sm leading-6 text-gray-500", - st - ) - ) - // TODO: actions - ), - div( - cls := "mt-6 border-t border-gray-100", - dl( - cls := "divide-y divide-gray-100", - fields - ) - ) - ) - - override def field( - title: String, - content: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0", - dt( - cls := "text-sm font-medium leading-6 text-gray-900", - title - ), - dd( - cls := "mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0", - content - ) - ) - - override def files(fs: List[FileRef]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-100 rounded-md border border-gray-200", - fs.map(file) - ) - - override def file(f: FileRef): HtmlElement = - li( - cls := "flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6", - div( - cls := "flex w-0 flex-1 items-center", - icons.`paper-clip-solid`(), - div( - cls := "ml-4 flex min-w-0 flex-1 gap-2", - span( - cls := "truncate font-medium", - f.name - ), - f.size.map(size => span(cls := "flex-shrink-0 text-gray-400", size)) - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - "Uložit" - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala new file mode 100644 index 0000000..f6c583f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponents.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait FormComponents(using ctx: ComponentContext) extends LocalDateSelectModule: + def searchIcon: SvgElement + + def renderLocalDateSelect( + id: String, + labelText: Option[String], + placeholderText: Option[String], + mods: LocalDateSelect => Modifier[HtmlElement] + ): HtmlElement = + div( + labelText, + input( + idAttr(id), + nameAttr(id), + autoComplete("date"), + tpe("date"), + cls( + "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ), + placeholderText.map(placeholder(_)), + mods(localDateSelect) + ) + ) + + def searchField( + id: String, + placeholderText: Option[String] = None + )( + mods: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls := "flex-1 min-w-0", + label(ctx.messages("forms.search.label"), forId(id), cls("sr-only")), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + searchIcon + ), + input( + tpe := "search", + nameAttr := "search", + idAttr := id, + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholderText + .orElse( + ctx.messages + .opt( + s"forms.search.${id}.placeholder", + s"form.search.placeholder" + ) + ) + .map(placeholder(_)), + mods + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala deleted file mode 100644 index 7714490..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala +++ /dev/null @@ -1,82 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -/** Form components - * - * A form is a collection of sections, each of which contains a collection of - * fields. - * - * Form -> * Section -> * Field - * -> * Action - * - * Each Field has an `id`, `label` and `input` element. The `id` is used to - * link the label to the input. - * - * The `input` element can be a simple text input, or a more complex component - * such as a date picker. The caller is expected to make the input work. - */ -trait FormComponentsModule extends LocalDateSelectModule: - def forms: FormComponents - - trait FormComponents: - - /** Layout the sections and actions of a form */ - def form(mods: Modifier[HtmlElement]*)(sections: Modifier[HtmlElement]*)( - actions: Modifier[HtmlElement]* - ): HtmlElement - - /** Section layout, with a title, optional subtitle and any content. */ - def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label and any content */ - def field(label: Modifier[HtmlElement])( - content: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout a field with label, input and optional help text */ - def field( - id: String, - label: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement - - def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - /** Layout an inline form element */ - def inlineForm(mods: Modifier[HtmlElement]*): HtmlElement - - /** An input field with a search icon */ - def searchField(id: String, placeholderText: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement - - def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement - - /** LocalDate input */ - def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala deleted file mode 100644 index 8acbe7f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/GenericComponentsModule.scala +++ /dev/null @@ -1,28 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import works.iterative.ui.model.Tag - -trait GenericComponentsModule: - - def generic: GenericComponents - - trait GenericComponents: - def tag(name: String, color: ColorKind): HtmlElement - def tag(t: Tag): HtmlElement = tag(t.value, t.color) - -trait DefaultGenericComponentsModule(using ComponentContext) - extends GenericComponentsModule: - override val generic: GenericComponents = new GenericComponents: - override def tag(name: String, color: ColorKind): HtmlElement = - p( - cls( - "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" - ), - color(800).text, - color(100).bg, - name - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala deleted file mode 100644 index 8cfc1d9..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/IconsModule.scala +++ /dev/null @@ -1,123 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.components.tailwind.CustomAttrs - -trait IconsModule: - def icons: Icons - - trait Icons: - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement - def close(mods: Modifier[SvgElement]*): SvgElement - def `search-solid`(mods: Modifier[SvgElement]*): SvgElement - def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement - def `document-chart-bar-outline`(mods: Modifier[SvgElement]*): SvgElement - def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement - -trait DefaultIconsModule(using ComponentContext) extends IconsModule: - override val icons: Icons = new Icons: - import svg.* - - object aria: - val hidden = CustomAttrs.svg.ariaHidden - - private def withDefault( - mods: Seq[Modifier[SvgElement]], - default: Modifier[SvgElement] - ): Modifier[SvgElement] = - if mods.isEmpty then default else mods - - def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-8 w-8"), - 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" - ) - ) - ) - - override def close(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-2 w-2"), - stroke := "currentColor", - fill := "none", - viewBox := "0 0 8 8", - path( - strokeLineCap := "round", - strokeWidth := "1.5", - d := "M1 1l6 6m0-6L1 7" - ) - ) - - override def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 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 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") - ) - ) - - override def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls("h-5 w-5 text-gray-400")), - 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") - ) - ) - - override def `document-chart-bar-outline`( - mods: Modifier[SvgElement]* - ): SvgElement = - svg( - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - strokeWidth := "1.5", - stroke := "currentColor", - withDefault(mods, cls := "h-6 w-6"), - path( - strokeLineCap := "round", - strokeLineJoin := "round", - d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" - ) - ) - - override def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = - svg( - withDefault(mods, cls := "h-5 w-5"), - cls := "flex-shrink-0 text-gray-400", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", - clipRule := "evenodd" - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala deleted file mode 100644 index 95ab654..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LayoutModule.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait LayoutModule: - def layout: LayoutComponents - - trait LayoutComponents: - def card(content: Modifier[HtmlElement]*): HtmlElement - -trait DefaultLayoutModule(using ctx: ComponentContext) extends LayoutModule: - override val layout: LayoutComponents = new LayoutComponents: - override def card(content: Modifier[HtmlElement]*): HtmlElement = - div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala deleted file mode 100644 index 5946974..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ListComponentsModule.scala +++ /dev/null @@ -1,107 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import works.iterative.ui.model.color.ColorKind -import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom.html.Paragraph -import com.raquo.laminar.nodes.TextNode - -trait ListComponentsModule: - - def list: ListComponents - - trait ListComponents(using ComponentContext): - def label(text: String, color: ColorKind): HtmlElement - def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI - def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] - def listSection(header: String, list: HtmlElement): Div - def navigation(sections: Modifier[HtmlElement]): HtmlElement - -trait DefaultListComponentsModule(using ComponentContext) - extends ListComponentsModule: - self: GenericComponentsModule => - - override val list: ListComponents = new ListComponents: - override def label( - text: String, - color: ColorKind - ): HtmlElement = generic.tag(text, color) - - override def item( - title: String, - subtitle: Option[String], - right: Modifier[HtmlElement] = emptyMod, - avatar: Option[Modifier[HtmlElement]] = None - ): LI = - li( - cls("group"), - div( - cls( - "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" - ), - avatar.map(a => - div( - cls("flex-shrink-0"), - div( - cls( - "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" - ), - a - ) - ) - ), - div( - cls("flex-1 min-w-0"), - p( - cls("text-sm font-medium text-gray-900"), - title, - span(cls("float-right"), right) - ), - subtitle.map(st => - p( - cls("text-sm text-gray-500 truncate"), - st - ) - ) - ) - ) - ) - - override def unordered( - children: Modifier[HtmlElement] - ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = - ul( - cls("relative z-0 divide-y divide-gray-200"), - role("list"), - children - ) - - override def listSection( - header: String, - list: HtmlElement - ): Div = - div( - cls("relative"), - div( - cls( - "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" - ), - header - ), - list - ) - - override def navigation(sections: Modifier[HtmlElement]): HtmlElement = - navTag( - cls("flex-1 min-h-0 overflow-y-auto"), - sections - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala deleted file mode 100644 index 5b5490f..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ModalComponentsModule.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait ModalComponentsModule: - - def modal: ModalComponents - - trait ModalComponents: - def modalDialog( - content: HtmlElement, - onClose: Modifier[HtmlElement] - ): HtmlElement - -trait DefaultModalComponentsModule(using ctx: ComponentContext) - extends ModalComponentsModule: - - override val modal: ModalComponents = new ModalComponents: - - override def modalDialog( - content: HtmlElement, - closeMod: Modifier[HtmlElement] - ): 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( - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")), - closeMod - ) - ) - - div( - cls("fixed inset-0 z-20 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - 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-7xl sm:align-middle" - ), - content - ) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala deleted file mode 100644 index b713d8a..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/PagesModule.scala +++ /dev/null @@ -1,71 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait PageComponentsModule: - - val page: PageComponents - - trait PageComponents: - def container( - children: Modifier[HtmlElement]* - ): HtmlElement - - def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement - - def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement] = emptyMod, - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement - - /** Visage for clickable text, like links - */ - def clickable: Modifier[HtmlElement] - -trait DefaultPageComponentsModule(using ComponentContext) - extends PageComponentsModule: - - override val page: PageComponents = new PageComponents: - override def container( - children: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), - children - ) - - override def singleColumn( - header: Modifier[HtmlElement] - )(children: Modifier[HtmlElement]*): HtmlElement = - div( - cls("p-8 bg-gray-100 h-full"), - header, - children - ) - - override def pageHeader( - title: Modifier[HtmlElement], - right: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None - ): HtmlElement = - div( - cls("pb-5 border-b border-gray-200"), - div(cls("float-right"), right), - h1( - cls("text-2xl leading-6 font-medium text-gray-900"), - title - ), - subtitle.map( - p( - cls("text-sm font-medium text-gray-500"), - _ - ) - ) - ) - - override def clickable: Modifier[HtmlElement] = - cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala deleted file mode 100644 index f70ee87..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala +++ /dev/null @@ -1,108 +0,0 @@ -package works.iterative.ui.components.laminar - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} - -trait TableComponentsModule: - def tables: TableComponents - - trait TableComponents: - def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )(table: Modifier[HtmlElement]*): HtmlElement - - def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement - - def headerRow(mods: Modifier[HtmlElement]*)( - cells: HtmlElement* - ): HtmlElement - - def dataRow(mods: Modifier[HtmlElement]*)(cells: HtmlElement*): HtmlElement - - def headerCell(content: Modifier[HtmlElement]): HtmlElement - - def dataCell(content: Modifier[HtmlElement]): HtmlElement - -trait DefaultTableComponentsModule extends TableComponentsModule: - - override lazy val tables: TableComponents = new TableComponents: - - override def tableSection( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] = None, - actions: Modifier[HtmlElement]* - )( - table: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls("px-4 sm:px-6 lg:px-8"), - div( - cls("sm:flex sm:items-center"), - div( - cls("sm:flex-auto"), - h1(cls("text-base font-semibold leading-6 text-gray-900"), title), - subtitle.map(st => p(cls("mt-2 text-sm text-gray-700"), st)) - ), - div(cls("mt-4 sm:ml-16 sm:mt-0 sm:flex-none"), actions) - ), - div( - cls("mt-8 flow-root"), - div( - cls("-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"), - div( - cls("inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"), - table - ) - ) - ) - ) - - override def simpleTable(header: Modifier[HtmlElement]*)( - body: Modifier[HtmlElement]* - ): HtmlElement = - table( - cls("min-w-full divide-y divide-gray-300"), - thead(header), - tbody(cls("divide-y divide-gray-200"), body) - ) - - override def headerRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-3.5 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-3.5 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-3.5 px-3")) - ) - ) - - override def dataRow( - mods: Modifier[HtmlElement]* - )(cells: HtmlElement*): HtmlElement = - tr( - mods, - cells.zipWithIndex.map((c, i) => - if i == 0 then c.amend(cls("py-4 pl-4 pr-3 sm:pl-0")) - else if i == cells.length - 1 then - c.amend(cls("py-4 pr-4 pl-3 sm:pr-0")) - else c.amend(cls("py-4 px-3")) - ) - ) - - override def headerCell(content: Modifier[HtmlElement]): HtmlElement = - th( - cls("text-left text-sm font-semibold text-gray-900"), - content - ) - - override def dataCell(content: Modifier[HtmlElement]): HtmlElement = - td( - cls("whitespace-nowrap text-sm text-gray-500"), - content - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala index b937d45..12d01ae 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tables/TableBuilderModule.scala @@ -3,7 +3,7 @@ import com.raquo.laminar.api.L.{*, given} import works.iterative.ui.components.laminar.HtmlTabular import works.iterative.ui.model.tables.Tabular -import works.iterative.ui.components.laminar.TableComponentsModule +import works.iterative.ui.components.laminar.tailwind.ui.TableComponentsModule import works.iterative.core.UserMessage import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given import works.iterative.ui.components.tailwind.ComponentContext diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala new file mode 100644 index 0000000..87f3e8d --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/Color.scala @@ -0,0 +1,54 @@ +package works.iterative.ui.components.laminar.tailwind.color + +import com.raquo.laminar.api.L.{*, given} + +/** Complete color definition that can be rendered to CSS. + * + * Includes the area, kind and weight of the color. + */ +case class Color(area: ColorArea, color: ColorDef): + def toCSS: String = s"${area.name}-${color.toCSS}" + +object Color: + import ColorDef.given + + given colorToCSS: Conversion[Color, HtmlMod] with + def apply(c: Color) = cls(c.toCSS) + + given colorToSVGCSS: Conversion[Color, SvgMod] with + def apply(c: Color) = svg.cls(c.toCSS) + + given colorSignalToCSS: Conversion[Signal[Color], HtmlMod] with + def apply(c: Signal[Color]) = cls <-- c.map(_.toCSS) + + given colorSignalToSVGCSS: Conversion[Signal[Color], SvgMod] with + def apply(c: Signal[Color]) = svg.cls <-- c.map(_.toCSS) + + def current = ColorDef(ColorKind.current) + def inherit = ColorDef(ColorKind.inherit) + def transp = ColorDef(ColorKind.transp) + def auto = ColorDef(ColorKind.auto) + def black = ColorDef(ColorKind.black) + def white = ColorDef(ColorKind.white) + def slate(weight: ColorWeight) = ColorDef(ColorKind.slate, weight) + def gray(weight: ColorWeight) = ColorDef(ColorKind.gray, weight) + def zinc(weight: ColorWeight) = ColorDef(ColorKind.zinc, weight) + def neutral(weight: ColorWeight) = ColorDef(ColorKind.neutral, weight) + def stone(weight: ColorWeight) = ColorDef(ColorKind.stone, weight) + def red(weight: ColorWeight) = ColorDef(ColorKind.red, weight) + def orange(weight: ColorWeight) = ColorDef(ColorKind.orange, weight) + def amber(weight: ColorWeight) = ColorDef(ColorKind.amber, weight) + def yellow(weight: ColorWeight) = ColorDef(ColorKind.yellow, weight) + def lime(weight: ColorWeight) = ColorDef(ColorKind.lime, weight) + def green(weight: ColorWeight) = ColorDef(ColorKind.green, weight) + def emerald(weight: ColorWeight) = ColorDef(ColorKind.emerald, weight) + def teal(weight: ColorWeight) = ColorDef(ColorKind.teal, weight) + def cyan(weight: ColorWeight) = ColorDef(ColorKind.cyan, weight) + def sky(weight: ColorWeight) = ColorDef(ColorKind.sky, weight) + def blue(weight: ColorWeight) = ColorDef(ColorKind.blue, weight) + def indigo(weight: ColorWeight) = ColorDef(ColorKind.indigo, weight) + def violet(weight: ColorWeight) = ColorDef(ColorKind.violet, weight) + def purple(weight: ColorWeight) = ColorDef(ColorKind.purple, weight) + def fuchsia(weight: ColorWeight) = ColorDef(ColorKind.fuchsia, weight) + def pink(weight: ColorWeight) = ColorDef(ColorKind.pink, weight) + def rose(weight: ColorWeight) = ColorDef(ColorKind.rose, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala new file mode 100644 index 0000000..4130840 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorArea.scala @@ -0,0 +1,16 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines the area the color should apply to, eg. background, text, border, + * etc. + */ +enum ColorArea(val name: String): + case bg extends ColorArea("bg") + case text extends ColorArea("text") + case decoration extends ColorArea("decoration") + case border extends ColorArea("border") + case outline extends ColorArea("outline") + case divide extends ColorArea("divide") + case ring extends ColorArea("ring") + case ringOffset extends ColorArea("ring-offset") + case shadow extends ColorArea("shadow") + case accent extends ColorArea("accent") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala new file mode 100644 index 0000000..fab9404 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorDef.scala @@ -0,0 +1,43 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** A combination of ColorKind and ColorWeight, if applicable. + * + * By applying area we get the full Color definition. + */ +sealed trait ColorDef: + def toCSS: String + + def bg = Color(ColorArea.bg, this) + def text = Color(ColorArea.text, this) + def decoration = Color(ColorArea.decoration, this) + def border = Color(ColorArea.border, this) + def outline = Color(ColorArea.outline, this) + def divide = Color(ColorArea.divide, this) + def ring = Color(ColorArea.ring, this) + def ringOffset = Color(ColorArea.ringOffset, this) + def shadow = Color(ColorArea.shadow, this) + def accent = Color(ColorArea.accent, this) + +// TODO: create a macro that will output the tailwind class as string directly during compilation, where possible. +object ColorDef: + case class WeightedColorDef( + kind: ColorKind, + weight: ColorWeight + ) extends ColorDef: + override def toCSS: String = s"${kind.name}-${weight.value}" + + case class UnweightedColorDef( + kind: ColorKind + ) extends ColorDef: + override def toCSS: String = kind.name + + // TODO: check that the kind is valid unweighted kind + // that means current, inherit, auto, transparent, black, white + // tried using implicit evidence, but the type inference for enumerations + // tends to generalize to the enum, instead of the real type + def apply[T <: ColorKind](kind: T)(using + ev: T <:< ColorKind.Unweighted + ): ColorDef = + UnweightedColorDef(kind) + def apply(kind: ColorKind, weight: ColorWeight): ColorDef = + WeightedColorDef(kind, weight) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala new file mode 100644 index 0000000..865166b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorKind.scala @@ -0,0 +1,47 @@ +package works.iterative.ui.components.laminar.tailwind.color + +/** Defines what color should be used, without specifying the area or weight. + */ +sealed abstract class ColorKind private (val name: String): + def apply(weight: ColorWeight): ColorDef = + ColorDef.WeightedColorDef(this, weight) + +object ColorKind: + trait Unweighted: + self: ColorKind => + override def apply(weight: ColorWeight): ColorDef = + ColorDef.UnweightedColorDef(self) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case object current extends ColorKind("current") with Unweighted + case object inherit extends ColorKind("inherit") with Unweighted + // Not present in for all methods + case object transp extends ColorKind("transparent") with Unweighted + // Seen in accent, not preset otherwise + case object auto extends ColorKind("auto") with Unweighted + // Black and white do not have weight + case object black extends ColorKind("black") with Unweighted + case object white extends ColorKind("white") with Unweighted + case object slate extends ColorKind("slate") + case object gray extends ColorKind("gray") + case object zinc extends ColorKind("zinc") + case object neutral extends ColorKind("neutral") + case object stone extends ColorKind("stone") + case object red extends ColorKind("red") + case object orange extends ColorKind("orange") + case object amber extends ColorKind("amber") + case object yellow extends ColorKind("yellow") + case object lime extends ColorKind("lime") + case object green extends ColorKind("green") + case object emerald extends ColorKind("emerald") + case object teal extends ColorKind("teal") + case object cyan extends ColorKind("cyan") + case object sky extends ColorKind("sky") + case object blue extends ColorKind("blue") + case object indigo extends ColorKind("indigo") + case object violet extends ColorKind("violet") + case object purple extends ColorKind("purple") + case object fuchsia extends ColorKind("fuchsia") + case object pink extends ColorKind("pink") + case object rose extends ColorKind("rose") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala new file mode 100644 index 0000000..6f1a4cc --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/ColorWeight.scala @@ -0,0 +1,33 @@ +package works.iterative.ui.components.laminar.tailwind.color + +opaque type ColorWeight = String + +extension (c: ColorWeight) def value: String = c + +/** Defines weight of a color, eg. 50, 100, 200, etc. + * + * Tailwind-like. + */ +object ColorWeight: + inline given int50: Conversion[50, ColorWeight] with + inline def apply(i: 50) = "50" + inline given int100: Conversion[100, ColorWeight] with + inline def apply(i: 100) = "100" + inline given int200: Conversion[200, ColorWeight] with + inline def apply(i: 200) = "200" + inline given int300: Conversion[300, ColorWeight] with + inline def apply(i: 300) = "300" + inline given int400: Conversion[400, ColorWeight] with + inline def apply(i: 400) = "400" + inline given int500: Conversion[500, ColorWeight] with + inline def apply(i: 500) = "500" + inline given int600: Conversion[600, ColorWeight] with + inline def apply(i: 600) = "600" + inline given int700: Conversion[700, ColorWeight] with + inline def apply(i: 700) = "700" + inline given int800: Conversion[800, ColorWeight] with + inline def apply(i: 800) = "800" + inline given int900: Conversion[900, ColorWeight] with + inline def apply(i: 900) = "900" + inline given int950: Conversion[950, ColorWeight] with + inline def apply(i: 950) = "950" diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala new file mode 100644 index 0000000..21e21f0 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/color/package.scala @@ -0,0 +1,12 @@ +package works.iterative.ui.components.laminar.tailwind + +/** We need a generic Color model that can be used both on server and on client. + * + * We have adopted the Tailwind model for now. There is nothing inherently + * Tailwind-specific in the implementation, but all the values are taken from + * their palette and the area model is very HTML biased. + * + * Still, I think that it is a good starting point for exploration and will + * satisfy our current needs. + */ +package object color {} diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" new file mode 100644 index 0000000..5c4e218 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043c345f949e6bd77d6f06ab70216ca8ed1bf211ccc\043" @@ -0,0 +1,21 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.model.color.ColorKind +import works.iterative.ui.components.tailwind.ComponentContext +import works.iterative.ui.components.tailwind.laminar.LaminarExtensions.given +import works.iterative.ui.model.Tag + +trait TailwindUIBadgesComponentsModule(using ComponentContext): + + object badges: + override def tag(name: String, color: ColorKind): HtmlElement = + p( + cls( + "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" + ), + color(800).text, + color(100).bg, + name + ) diff --git "a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" new file mode 100644 index 0000000..0c9ca25 --- /dev/null +++ "b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/\043f80c1e3581c37fbcd6e5ccac1e89e7b1c46920f1\043" @@ -0,0 +1,24 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} + +trait TailwindUIHeadingsComponentsModule: + val headings: HeadingsComponents = new HeadingsComponents + + class HeadingsComponents: + def section(title: Node, actions: HtmlElement*): HtmlElement = + div( + cls("border-b border-gray-200 pb-5"), + div( + cls("sm:flex sm:items-center sm:justify-between"), + h3(cls("text-base font-semibold leading-6 text-gray-900"), title), + div( + cls("mt-3 flex sm:ml-4 sm:mt-0"), + actions match + case Nil => emptyMod + case first :: rest => + first :: rest.map(_.amend(cls("ml-3"))) + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala new file mode 100644 index 0000000..16ca01b --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/BadgeComponentsModule.scala @@ -0,0 +1,19 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.laminar.tailwind.color.ColorKind + +trait BadgeComponentsModule: + + object badges: + def pill(name: String, color: ColorKind): HtmlElement = + p( + cls( + "px-2 inline-flex text-xs leading-5 font-semibold rounded-full" + ), + color(800).text, + color(100).bg, + name + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala new file mode 100644 index 0000000..ebb2968 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ButtonComponentsModule.scala @@ -0,0 +1,62 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} + +trait ButtonComponentsModule: + + object buttons: + + private inline def srHelp(text: String): Modifier[HtmlElement] = + span(cls := "sr-only", text) + + def primaryButton( + id: String, + text: Modifier[HtmlElement], + icon: Option[SvgElement] = None, + buttonType: String = "submit" + )(mods: Modifier[HtmlElement]*): HtmlElement = + button( + tpe(buttonType), + cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + icon, + text, + mods + ) + + def secondaryButton( + id: String, + text: Modifier[HtmlElement], + icon: Option[SvgElement] = None, + buttonType: String = "button" + )(mods: Modifier[HtmlElement]*): HtmlElement = + button( + tpe(buttonType), + cls( + "ml-2 bg-white inline-flex justify-center 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" + ), + icon, + text, + mods + ) + + def inlineButton(id: String, icon: SvgElement)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + button( + tpe := "button", + cls := "ml-1 inline-flex h-4 w-4 flex-shrink-0 rounded-full p-1 text-gray-400 hover:bg-gray-200 hover:text-gray-500", + srHelp(id), + icon + ) + + def iconButton(id: String, icon: SvgElement, srText: Option[String] = None)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + button( + tpe := "button", + cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", + icon, + srText.map(srHelp(_)) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala new file mode 100644 index 0000000..eb6dd5f --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/FormComponentsModule.scala @@ -0,0 +1,101 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import org.scalajs.dom.html +import com.raquo.laminar.modifiers.KeyUpdater + +trait FormComponentsModule: + self: IconsModule => + + object forms: + + def section( + title: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] + )(content: Modifier[HtmlElement]*): HtmlElement = + div( + cls("space-y-6 sm:space-y-5"), + div( + h3(cls("text-lg leading-6 font-medium text-gray-900"), title), + subtitle.map(st => p(cls("mt-1 max-w-2xl text-sm text-gray-500"), st)) + ), + div(cls("mt-6 sm:mt-5 space-y-6 sm:space-y-5"), content) + ) + + def label(labelText: String, forId: Option[String] = None)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + L.label( + cls("block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"), + forId.map(id => L.forId(id)), + labelText, + mods + ) + + def field( + label: Modifier[HtmlElement] + )(content: Modifier[HtmlElement]*): 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, + div(cls("mt-1 sm:mt-0 sm:col-span-2"), content) + ) + + def field( + id: String, + labelText: String, + input: HtmlElement, + help: Option[String] + ): HtmlElement = + field( + label(labelText, Some(id))() + )( + input.amend(idAttr(id)), + help.map(h => p(cls("mt-2 text-sm text-gray-500"), h)) + ) + + def form(mods: Modifier[HtmlElement]*)( + sections: Modifier[HtmlElement]* + )(actions: Modifier[HtmlElement]*): HtmlElement = + L.form( + cls("space-y-8 divide-y divide-gray-200"), + mods, + sections, + div( + cls("pt-5"), + div(cls("flex justify-end"), actions) + ) + ) + + def inlineForm( + mods: Modifier[HtmlElement]* + ): HtmlElement = + L.form(cls("flex space-x-4"), mods) + + def inputField( + id: String, + labelText: String, + placeholderText: Option[String] = None, + inputType: String = "text", + helpText: Option[String] = None + ): HtmlElement = + field( + id, + labelText, + input( + tpe(inputType), + cls( + "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:max-w-xs sm:text-sm sm:leading-6" + ), + placeholderText.map(placeholder(_)) + ), + helpText + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala new file mode 100644 index 0000000..52da07e --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/HeadingsComponentsModule.scala @@ -0,0 +1,22 @@ +package works.iterative.ui.components.laminar +package tailwind + +import com.raquo.laminar.api.L.{*, given} + +trait HeadingsComponentsModule: + object headings: + def section(title: Node, actions: HtmlElement*): HtmlElement = + div( + cls("border-b border-gray-200 pb-5"), + div( + cls("sm:flex sm:items-center sm:justify-between"), + h3(cls("text-base font-semibold leading-6 text-gray-900"), title), + div( + cls("mt-3 flex sm:ml-4 sm:mt-0"), + actions match + case Nil => emptyMod + case first :: rest => + first :: rest.map(_.amend(cls("ml-3"))) + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala new file mode 100644 index 0000000..c543150 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/IconsModule.scala @@ -0,0 +1,112 @@ +package works.iterative.ui.components.laminar + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext +import works.iterative.ui.components.tailwind.CustomAttrs + +trait IconsModule: + object icons: + import svg.* + + object aria: + val hidden = CustomAttrs.svg.ariaHidden + + private def withDefault( + mods: Seq[Modifier[SvgElement]], + default: Modifier[SvgElement] + ): Modifier[SvgElement] = + if mods.isEmpty then default else mods + + def avatarPlaceholder(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-8 w-8"), + 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" + ) + ) + ) + + def close(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-2 w-2"), + stroke := "currentColor", + fill := "none", + viewBox := "0 0 8 8", + path( + strokeLineCap := "round", + strokeWidth := "1.5", + d := "M1 1l6 6m0-6L1 7" + ) + ) + + def `search-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls("h-5 w-5 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 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") + ) + ) + + def `filter-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls("h-5 w-5 text-gray-400")), + 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") + ) + ) + + def `document-chart-bar-outline`( + mods: Modifier[SvgElement]* + ): SvgElement = + svg( + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + strokeWidth := "1.5", + stroke := "currentColor", + withDefault(mods, cls := "h-6 w-6"), + path( + strokeLineCap := "round", + strokeLineJoin := "round", + d := "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" + ) + ) + + def `paper-clip-solid`(mods: Modifier[SvgElement]*): SvgElement = + svg( + withDefault(mods, cls := "h-5 w-5"), + cls := "flex-shrink-0 text-gray-400", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z", + clipRule := "evenodd" + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala new file mode 100644 index 0000000..02cbdb6 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/LayoutModule.scala @@ -0,0 +1,11 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait LayoutModule: + object layout: + def card(content: Modifier[HtmlElement]*): HtmlElement = + div(cls("bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6"), content) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala new file mode 100644 index 0000000..b652da9 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ListComponentsModule.scala @@ -0,0 +1,88 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.laminar.nodes.ReactiveHtmlElement +import org.scalajs.dom.html.Paragraph +import com.raquo.laminar.nodes.TextNode +import works.iterative.ui.components.laminar.tailwind.color.ColorKind + +trait ListComponentsModule: + self: BadgeComponentsModule => + + object list: + def label( + text: String, + color: ColorKind + ): HtmlElement = badges.pill(text, color) + + def item( + title: String, + subtitle: Option[String], + right: Modifier[HtmlElement] = emptyMod, + avatar: Option[Modifier[HtmlElement]] = None + ): LI = + li( + cls("group"), + div( + cls( + "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500" + ), + avatar.map(a => + div( + cls("flex-shrink-0"), + div( + cls( + "rounded-full text-indigo-200 bg-indigo-600 flex items-center justify-center w-10 h-10" + ), + a + ) + ) + ), + div( + cls("flex-1 min-w-0"), + p( + cls("text-sm font-medium text-gray-900"), + title, + span(cls("float-right"), right) + ), + subtitle.map(st => + p( + cls("text-sm text-gray-500 truncate"), + st + ) + ) + ) + ) + ) + + def unordered( + children: Modifier[HtmlElement] + ): ReactiveHtmlElement[org.scalajs.dom.html.UList] = + ul( + cls("relative z-0 divide-y divide-gray-200"), + role("list"), + children + ) + + def listSection( + header: String, + list: HtmlElement + ): Div = + div( + cls("relative"), + div( + cls( + "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500" + ), + header + ), + list + ) + + def navigation(sections: Modifier[HtmlElement]): HtmlElement = + navTag( + cls("flex-1 min-h-0 overflow-y-auto"), + sections + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ModalComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ModalComponentsModule.scala new file mode 100644 index 0000000..6027404 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/ModalComponentsModule.scala @@ -0,0 +1,54 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait ModalComponentsModule: + object modal: + def modalDialog( + content: HtmlElement, + closeMod: Modifier[HtmlElement] + ): 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( + cls("fixed inset-0 transition-opacity"), + div(cls("absolute inset-0 bg-gray-500 opacity-75")), + closeMod + ) + ) + + div( + cls("fixed inset-0 z-20 overflow-y-auto"), + div( + cls("text-center sm:block sm:p-0"), + overlay, + browserCenteringModalTrick, + 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-7xl sm:align-middle" + ), + content + ) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/PageComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/PageComponentsModule.scala new file mode 100644 index 0000000..d5a422e --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/PageComponentsModule.scala @@ -0,0 +1,49 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait PageComponentsModule: + + object page: + def container( + children: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls("max-w-7xl mx-auto h-full px-4 sm:px-6 lg:px-8 overflow-y-auto"), + children + ) + + def singleColumn( + header: Modifier[HtmlElement] + )(children: Modifier[HtmlElement]*): HtmlElement = + div( + cls("p-8 bg-gray-100 h-full"), + header, + children + ) + + def pageHeader( + title: Modifier[HtmlElement], + right: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] = None + ): HtmlElement = + div( + cls("pb-5 border-b border-gray-200"), + div(cls("float-right"), right), + h1( + cls("text-2xl leading-6 font-medium text-gray-900"), + title + ), + subtitle.map( + p( + cls("text-sm font-medium text-gray-500"), + _ + ) + ) + ) + + def clickable: Modifier[HtmlElement] = + cls("text-sm font-medium text-indigo-600 hover:text-indigo-400") diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TableComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TableComponentsModule.scala new file mode 100644 index 0000000..417ea56 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TableComponentsModule.scala @@ -0,0 +1,86 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L +import com.raquo.laminar.api.L.{*, given} + +trait TableComponentsModule: + + object tables: + + def tableSection( + title: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] = None, + actions: Modifier[HtmlElement]* + )( + table: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls("px-4 sm:px-6 lg:px-8"), + div( + cls("sm:flex sm:items-center"), + div( + cls("sm:flex-auto"), + h1(cls("text-base font-semibold leading-6 text-gray-900"), title), + subtitle.map(st => p(cls("mt-2 text-sm text-gray-700"), st)) + ), + div(cls("mt-4 sm:ml-16 sm:mt-0 sm:flex-none"), actions) + ), + div( + cls("mt-8 flow-root"), + div( + cls("-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"), + div( + cls("inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"), + table + ) + ) + ) + ) + + def simpleTable(header: Modifier[HtmlElement]*)( + body: Modifier[HtmlElement]* + ): HtmlElement = + table( + cls("min-w-full divide-y divide-gray-300"), + thead(header), + tbody(cls("divide-y divide-gray-200"), body) + ) + + def headerRow( + mods: Modifier[HtmlElement]* + )(cells: HtmlElement*): HtmlElement = + tr( + cells.zipWithIndex.map((c, i) => + if i == 0 then c.amend(cls("py-3.5 pl-4 pr-3 sm:pl-0")) + else if i == cells.length - 1 then + c.amend(cls("py-3.5 pr-4 pl-3 sm:pr-0")) + else c.amend(cls("py-3.5 px-3")) + ) + ) + + def dataRow( + mods: Modifier[HtmlElement]* + )(cells: HtmlElement*): HtmlElement = + tr( + mods, + cells.zipWithIndex.map((c, i) => + if i == 0 then c.amend(cls("py-4 pl-4 pr-3 sm:pl-0")) + else if i == cells.length - 1 then + c.amend(cls("py-4 pr-4 pl-3 sm:pr-0")) + else c.amend(cls("py-4 px-3")) + ) + ) + + def headerCell(content: Modifier[HtmlElement]): HtmlElement = + th( + cls("text-left text-sm font-semibold text-gray-900"), + content + ) + + def dataCell(content: Modifier[HtmlElement]): HtmlElement = + td( + cls("whitespace-nowrap text-sm text-gray-500"), + content + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TailwindUICatalogue.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TailwindUICatalogue.scala new file mode 100644 index 0000000..5027b10 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwind/ui/TailwindUICatalogue.scala @@ -0,0 +1,17 @@ +package works.iterative.ui.components.laminar +package tailwind +package ui + +import com.raquo.laminar.api.L.{*, given} + +trait TailwindUICatalogueModule + extends BadgeComponentsModule + with ButtonComponentsModule + with HeadingsComponentsModule + with IconsModule + with FormComponentsModule + with LayoutModule + with ListComponentsModule + with ModalComponentsModule + with PageComponentsModule + with TableComponentsModule diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/LabelsOnLeftFormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/LabelsOnLeftFormComponentsModule.scala deleted file mode 100644 index 210bbf8..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/LabelsOnLeftFormComponentsModule.scala +++ /dev/null @@ -1,155 +0,0 @@ -package works.iterative.ui.components.laminar -package tailwindui - -import com.raquo.laminar.api.L -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import org.scalajs.dom.html -import com.raquo.laminar.modifiers.KeyUpdater - -trait LabelsOnLeftFormComponentsModule(using ctx: ComponentContext) - extends FormComponentsModule: - self: IconsModule => - override val forms = new FormComponents: - - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )(content: Modifier[HtmlElement]*): HtmlElement = - div( - cls("space-y-6 sm:space-y-5"), - div( - h3(cls("text-lg leading-6 font-medium text-gray-900"), title), - subtitle.map(st => p(cls("mt-1 max-w-2xl text-sm text-gray-500"), st)) - ), - div(cls("mt-6 sm:mt-5 space-y-6 sm:space-y-5"), content) - ) - - override def label(labelText: String, forId: Option[String] = None)( - mods: Modifier[HtmlElement]* - ): HtmlElement = - L.label( - cls("block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"), - forId.map(id => L.forId(id)), - labelText, - mods - ) - - override def field( - label: Modifier[HtmlElement] - )(content: Modifier[HtmlElement]*): 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, - div(cls("mt-1 sm:mt-0 sm:col-span-2"), content) - ) - - override def field( - id: String, - labelText: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement = - field( - label(labelText, Some(id))() - )( - input.amend(idAttr(id)), - help.map(h => p(cls("mt-2 text-sm text-gray-500"), h)) - ) - - override def form(mods: Modifier[HtmlElement]*)( - sections: Modifier[HtmlElement]* - )(actions: Modifier[HtmlElement]*): HtmlElement = - L.form( - cls("space-y-8 divide-y divide-gray-200"), - mods, - sections, - div( - cls("pt-5"), - div(cls("flex justify-end"), actions) - ) - ) - - override def inlineForm( - mods: Modifier[HtmlElement]* - ): HtmlElement = - L.form(cls("flex space-x-4"), mods) - - override def searchField( - id: String, - placeholderText: Option[String] = None - )( - mods: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "flex-1 min-w-0", - label(ctx.messages("forms.search.label"), Some(id))(cls("sr-only")), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - icons.`search-solid`() - ), - input( - tpe := "search", - nameAttr := "search", - idAttr := id, - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholderText - .orElse( - ctx.messages - .opt( - s"forms.search.${id}.placeholder", - s"form.search.placeholder" - ) - ) - .map(placeholder(_)), - mods - ) - ) - ) - - override def inputField( - id: String, - labelText: String, - placeholderText: Option[String] = None, - inputType: String = "text", - helpText: Option[String] = None - ): HtmlElement = - field( - id, - labelText, - input( - tpe(inputType), - cls( - "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:max-w-xs sm:text-sm sm:leading-6" - ), - placeholderText.map(placeholder(_)) - ), - helpText - ) - - override def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement = - div( - labelText, - input( - idAttr(id), - nameAttr(id), - autoComplete("date"), - tpe("date"), - cls( - "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ), - placeholderText.map(placeholder(_)), - mods(localDateSelect) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/TailwindUIButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/TailwindUIButtonComponentsModule.scala deleted file mode 100644 index 4c44157..0000000 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/TailwindUIButtonComponentsModule.scala +++ /dev/null @@ -1,65 +0,0 @@ -package works.iterative.ui.components.laminar -package tailwindui - -import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.components.tailwind.ComponentContext - -trait TailwindUIButtonComponentsModule(using ctx: ComponentContext) - extends ButtonComponentsModule: - - override val buttons = new ButtonComponents: - - private inline def srHelp(id: String): Modifier[HtmlElement] = - ctx.messages - .opt(s"form.button.${id}.screenReaderHelp") - .map(sr => span(cls := "sr-only", sr)) - - override def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "submit" - )(mods: Modifier[HtmlElement]*): HtmlElement = - button( - tpe(buttonType), - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - icon, - text, - mods - ) - - override def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None, - buttonType: String = "button" - )(mods: Modifier[HtmlElement]*): HtmlElement = - button( - tpe(buttonType), - cls( - "ml-2 bg-white inline-flex justify-center 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" - ), - icon, - text, - mods - ) - - override def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement = - button( - tpe := "button", - cls := "ml-1 inline-flex h-4 w-4 flex-shrink-0 rounded-full p-1 text-gray-400 hover:bg-gray-200 hover:text-gray-500", - srHelp(id), - icon - ) - - override def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement = - button( - tpe := "button", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - icon, - srHelp(id) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/tailwind/laminar/LaminarExtensions.scala b/ui/js/src/main/scala/works/iterative/ui/components/tailwind/laminar/LaminarExtensions.scala index 4c5e68b..4c87800 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/tailwind/laminar/LaminarExtensions.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/tailwind/laminar/LaminarExtensions.scala @@ -2,23 +2,9 @@ package laminar import com.raquo.laminar.api.L.{*, given} -import works.iterative.ui.model.color.Color import works.iterative.core.UserMessage object LaminarExtensions: - given colorToCSS: Conversion[Color, Modifier[HtmlElement]] with - def apply(c: Color) = cls(c.toCSS) - - given colorToSVGCSS: Conversion[Color, Modifier[SvgElement]] with - def apply(c: Color) = svg.cls(c.toCSS) - - given colorSignalToCSS: Conversion[Signal[Color], Modifier[HtmlElement]] with - def apply(c: Signal[Color]) = cls <-- c.map(_.toCSS) - - given colorSignalToSVGCSS: Conversion[Signal[Color], Modifier[SvgElement]] - with - def apply(c: Signal[Color]) = svg.cls <-- c.map(_.toCSS) - inline given userMessageToModifier(using ctx: ComponentContext ): Conversion[UserMessage, Modifier[HtmlElement]] with diff --git a/ui/js/src/main/scala/works/iterative/ui/scenarios/Scenario.scala b/ui/js/src/main/scala/works/iterative/ui/scenarios/Scenario.scala index 99a212a..60de9f0 100644 --- a/ui/js/src/main/scala/works/iterative/ui/scenarios/Scenario.scala +++ b/ui/js/src/main/scala/works/iterative/ui/scenarios/Scenario.scala @@ -13,8 +13,6 @@ import works.iterative.core.MessageCatalogue import works.iterative.ui.components.tailwind.ComponentContext import works.iterative.ui.components.tailwind.StyleGuide -import works.iterative.ui.model.color.Color -import works.iterative.ui.model.color.ColorWeight object Scenario: type Id = String diff --git a/ui/shared/src/main/scala/works/iterative/ui/model/Tag.scala b/ui/shared/src/main/scala/works/iterative/ui/model/Tag.scala index 7352579..958aa75 100644 --- a/ui/shared/src/main/scala/works/iterative/ui/model/Tag.scala +++ b/ui/shared/src/main/scala/works/iterative/ui/model/Tag.scala @@ -1,10 +1,16 @@ package works.iterative.ui.model -import works.iterative.ui.model.color.ColorKind - -/** Representation of colored string value. +/** Representation of a label or tag. * - * Used generally to represent tags or "labels", eg. some kind of status or - * categorization. + * The UI renderer will recognize this value as tag and render accordingly. The + * value is used to determine both the color of the tag and the text displayed. + * + * @param value + * the value of the tag */ -final case class Tag(value: String, color: ColorKind = ColorKind.gray) +opaque type Tag = String + +object Tag: + def apply(value: String): Tag = value + + extension (tag: Tag) def value: String = tag diff --git a/ui/shared/src/main/scala/works/iterative/ui/model/color/Color.scala b/ui/shared/src/main/scala/works/iterative/ui/model/color/Color.scala deleted file mode 100644 index 09d6e4d..0000000 --- a/ui/shared/src/main/scala/works/iterative/ui/model/color/Color.scala +++ /dev/null @@ -1,40 +0,0 @@ -package works.iterative.ui.model.color - -/** Complete color definition that can be rendered to CSS. - * - * Includes the area, kind and weight of the color. - */ -case class Color(area: ColorArea, color: ColorDef): - def toCSS: String = s"${area.name}-${color.toCSS}" - -object Color: - import ColorDef.given - - def current = ColorDef(ColorKind.current) - def inherit = ColorDef(ColorKind.inherit) - def transp = ColorDef(ColorKind.transp) - def auto = ColorDef(ColorKind.auto) - def black = ColorDef(ColorKind.black) - def white = ColorDef(ColorKind.white) - def slate(weight: ColorWeight) = ColorDef(ColorKind.slate, weight) - def gray(weight: ColorWeight) = ColorDef(ColorKind.gray, weight) - def zinc(weight: ColorWeight) = ColorDef(ColorKind.zinc, weight) - def neutral(weight: ColorWeight) = ColorDef(ColorKind.neutral, weight) - def stone(weight: ColorWeight) = ColorDef(ColorKind.stone, weight) - def red(weight: ColorWeight) = ColorDef(ColorKind.red, weight) - def orange(weight: ColorWeight) = ColorDef(ColorKind.orange, weight) - def amber(weight: ColorWeight) = ColorDef(ColorKind.amber, weight) - def yellow(weight: ColorWeight) = ColorDef(ColorKind.yellow, weight) - def lime(weight: ColorWeight) = ColorDef(ColorKind.lime, weight) - def green(weight: ColorWeight) = ColorDef(ColorKind.green, weight) - def emerald(weight: ColorWeight) = ColorDef(ColorKind.emerald, weight) - def teal(weight: ColorWeight) = ColorDef(ColorKind.teal, weight) - def cyan(weight: ColorWeight) = ColorDef(ColorKind.cyan, weight) - def sky(weight: ColorWeight) = ColorDef(ColorKind.sky, weight) - def blue(weight: ColorWeight) = ColorDef(ColorKind.blue, weight) - def indigo(weight: ColorWeight) = ColorDef(ColorKind.indigo, weight) - def violet(weight: ColorWeight) = ColorDef(ColorKind.violet, weight) - def purple(weight: ColorWeight) = ColorDef(ColorKind.purple, weight) - def fuchsia(weight: ColorWeight) = ColorDef(ColorKind.fuchsia, weight) - def pink(weight: ColorWeight) = ColorDef(ColorKind.pink, weight) - def rose(weight: ColorWeight) = ColorDef(ColorKind.rose, weight) diff --git a/ui/shared/src/main/scala/works/iterative/ui/model/color/ColorArea.scala b/ui/shared/src/main/scala/works/iterative/ui/model/color/ColorArea.scala deleted file mode 100644 index 1211287..0000000 --- a/ui/shared/src/main/scala/works/iterative/ui/model/color/ColorArea.scala +++ /dev/null @@ -1,16 +0,0 @@ -package works.iterative.ui.model.color - -/** Defines the area the color should apply to, eg. background, text, border, - * etc. - */ -enum ColorArea(val name: String): - case bg extends ColorArea("bg") - case text extends ColorArea("text") - case decoration extends ColorArea("decoration") - case border extends ColorArea("border") - case outline extends ColorArea("outline") - case divide extends ColorArea("divide") - case ring extends ColorArea("ring") - case ringOffset extends ColorArea("ring-offset") - case shadow extends ColorArea("shadow") - case accent extends ColorArea("accent") diff --git a/ui/shared/src/main/scala/works/iterative/ui/model/color/ColorDef.scala b/ui/shared/src/main/scala/works/iterative/ui/model/color/ColorDef.scala deleted file mode 100644 index 9c5ec61..0000000 --- a/ui/shared/src/main/scala/works/iterative/ui/model/color/ColorDef.scala +++ /dev/null @@ -1,43 +0,0 @@ -package works.iterative.ui.model.color - -/** A combination of ColorKind and ColorWeight, if applicable. - * - * By applying area we get the full Color definition. - */ -sealed trait ColorDef: - def toCSS: String - - def bg = Color(ColorArea.bg, this) - def text = Color(ColorArea.text, this) - def decoration = Color(ColorArea.decoration, this) - def border = Color(ColorArea.border, this) - def outline = Color(ColorArea.outline, this) - def divide = Color(ColorArea.divide, this) - def ring = Color(ColorArea.ring, this) - def ringOffset = Color(ColorArea.ringOffset, this) - def shadow = Color(ColorArea.shadow, this) - def accent = Color(ColorArea.accent, this) - -// TODO: create a macro that will output the tailwind class as string directly during compilation, where possible. -object ColorDef: - case class WeightedColorDef( - kind: ColorKind, - weight: ColorWeight - ) extends ColorDef: - override def toCSS: String = s"${kind.name}-${weight.value}" - - case class UnweightedColorDef( - kind: ColorKind - ) extends ColorDef: - override def toCSS: String = kind.name - - // TODO: check that the kind is valid unweighted kind - // that means current, inherit, auto, transparent, black, white - // tried using implicit evidence, but the type inference for enumerations - // tends to generalize to the enum, instead of the real type - def apply[T <: ColorKind](kind: T)(using - ev: T <:< ColorKind.Unweighted - ): ColorDef = - UnweightedColorDef(kind) - def apply(kind: ColorKind, weight: ColorWeight): ColorDef = - WeightedColorDef(kind, weight) diff --git a/ui/shared/src/main/scala/works/iterative/ui/model/color/ColorKind.scala b/ui/shared/src/main/scala/works/iterative/ui/model/color/ColorKind.scala deleted file mode 100644 index ea24372..0000000 --- a/ui/shared/src/main/scala/works/iterative/ui/model/color/ColorKind.scala +++ /dev/null @@ -1,47 +0,0 @@ -package works.iterative.ui.model.color - -/** Defines what color should be used, without specifying the area or weight. - */ -sealed abstract class ColorKind private (val name: String): - def apply(weight: ColorWeight): ColorDef = - ColorDef.WeightedColorDef(this, weight) - -object ColorKind: - trait Unweighted: - self: ColorKind => - override def apply(weight: ColorWeight): ColorDef = - ColorDef.UnweightedColorDef(self) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case object current extends ColorKind("current") with Unweighted - case object inherit extends ColorKind("inherit") with Unweighted - // Not present in for all methods - case object transp extends ColorKind("transparent") with Unweighted - // Seen in accent, not preset otherwise - case object auto extends ColorKind("auto") with Unweighted - // Black and white do not have weight - case object black extends ColorKind("black") with Unweighted - case object white extends ColorKind("white") with Unweighted - case object slate extends ColorKind("slate") - case object gray extends ColorKind("gray") - case object zinc extends ColorKind("zinc") - case object neutral extends ColorKind("neutral") - case object stone extends ColorKind("stone") - case object red extends ColorKind("red") - case object orange extends ColorKind("orange") - case object amber extends ColorKind("amber") - case object yellow extends ColorKind("yellow") - case object lime extends ColorKind("lime") - case object green extends ColorKind("green") - case object emerald extends ColorKind("emerald") - case object teal extends ColorKind("teal") - case object cyan extends ColorKind("cyan") - case object sky extends ColorKind("sky") - case object blue extends ColorKind("blue") - case object indigo extends ColorKind("indigo") - case object violet extends ColorKind("violet") - case object purple extends ColorKind("purple") - case object fuchsia extends ColorKind("fuchsia") - case object pink extends ColorKind("pink") - case object rose extends ColorKind("rose") diff --git a/ui/shared/src/main/scala/works/iterative/ui/model/color/ColorWeight.scala b/ui/shared/src/main/scala/works/iterative/ui/model/color/ColorWeight.scala deleted file mode 100644 index 7767ac5..0000000 --- a/ui/shared/src/main/scala/works/iterative/ui/model/color/ColorWeight.scala +++ /dev/null @@ -1,33 +0,0 @@ -package works.iterative.ui.model.color - -opaque type ColorWeight = String - -extension (c: ColorWeight) def value: String = c - -/** Defines weight of a color, eg. 50, 100, 200, etc. - * - * Tailwind-like. - */ -object ColorWeight: - inline given int50: Conversion[50, ColorWeight] with - inline def apply(i: 50) = "50" - inline given int100: Conversion[100, ColorWeight] with - inline def apply(i: 100) = "100" - inline given int200: Conversion[200, ColorWeight] with - inline def apply(i: 200) = "200" - inline given int300: Conversion[300, ColorWeight] with - inline def apply(i: 300) = "300" - inline given int400: Conversion[400, ColorWeight] with - inline def apply(i: 400) = "400" - inline given int500: Conversion[500, ColorWeight] with - inline def apply(i: 500) = "500" - inline given int600: Conversion[600, ColorWeight] with - inline def apply(i: 600) = "600" - inline given int700: Conversion[700, ColorWeight] with - inline def apply(i: 700) = "700" - inline given int800: Conversion[800, ColorWeight] with - inline def apply(i: 800) = "800" - inline given int900: Conversion[900, ColorWeight] with - inline def apply(i: 900) = "900" - inline given int950: Conversion[950, ColorWeight] with - inline def apply(i: 950) = "950" diff --git a/ui/shared/src/main/scala/works/iterative/ui/model/color/package.scala b/ui/shared/src/main/scala/works/iterative/ui/model/color/package.scala deleted file mode 100644 index 2b380e6..0000000 --- a/ui/shared/src/main/scala/works/iterative/ui/model/color/package.scala +++ /dev/null @@ -1,12 +0,0 @@ -package works.iterative.ui.model - -/** We need a generic Color model that can be used both on server and on client. - * - * We have adopted the Tailwind model for now. There is nothing inherently - * Tailwind-specific in the implementation, but all the values are taken from - * their palette and the area model is very HTML biased. - * - * Still, I think that it is a good starting point for exploration and will - * satisfy our current needs. - */ -package object color {}