diff --git a/ui/src/ui/Component.scala b/ui/src/ui/Component.scala deleted file mode 100644 index b912053..0000000 --- a/ui/src/ui/Component.scala +++ /dev/null @@ -1,13 +0,0 @@ -package ui - -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -trait Component[Ref <: dom.html.Element]: - def element: ReactiveHtmlElement[Ref] - -object Component: - given [Ref <: dom.html.Element] - : Conversion[Component[Ref], ReactiveHtmlElement[Ref]] with - def apply(component: Component[Ref]): ReactiveHtmlElement[Ref] = - component.element diff --git a/ui/src/ui/Component.scala b/ui/src/ui/Component.scala deleted file mode 100644 index b912053..0000000 --- a/ui/src/ui/Component.scala +++ /dev/null @@ -1,13 +0,0 @@ -package ui - -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -trait Component[Ref <: dom.html.Element]: - def element: ReactiveHtmlElement[Ref] - -object Component: - given [Ref <: dom.html.Element] - : Conversion[Component[Ref], ReactiveHtmlElement[Ref]] with - def apply(component: Component[Ref]): ReactiveHtmlElement[Ref] = - component.element diff --git a/ui/src/ui/components/tailwind/Component.scala b/ui/src/ui/components/tailwind/Component.scala new file mode 100644 index 0000000..c22e6e4 --- /dev/null +++ b/ui/src/ui/components/tailwind/Component.scala @@ -0,0 +1,13 @@ +package works.iterative.ui.components.tailwind + +import com.raquo.laminar.nodes.ReactiveHtmlElement +import org.scalajs.dom + +trait Component[Ref <: dom.html.Element]: + def element: ReactiveHtmlElement[Ref] + +object Component: + given [Ref <: dom.html.Element] + : Conversion[Component[Ref], ReactiveHtmlElement[Ref]] with + def apply(component: Component[Ref]): ReactiveHtmlElement[Ref] = + component.element diff --git a/ui/src/ui/Component.scala b/ui/src/ui/Component.scala deleted file mode 100644 index b912053..0000000 --- a/ui/src/ui/Component.scala +++ /dev/null @@ -1,13 +0,0 @@ -package ui - -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -trait Component[Ref <: dom.html.Element]: - def element: ReactiveHtmlElement[Ref] - -object Component: - given [Ref <: dom.html.Element] - : Conversion[Component[Ref], ReactiveHtmlElement[Ref]] with - def apply(component: Component[Ref]): ReactiveHtmlElement[Ref] = - component.element diff --git a/ui/src/ui/components/tailwind/Component.scala b/ui/src/ui/components/tailwind/Component.scala new file mode 100644 index 0000000..c22e6e4 --- /dev/null +++ b/ui/src/ui/components/tailwind/Component.scala @@ -0,0 +1,13 @@ +package works.iterative.ui.components.tailwind + +import com.raquo.laminar.nodes.ReactiveHtmlElement +import org.scalajs.dom + +trait Component[Ref <: dom.html.Element]: + def element: ReactiveHtmlElement[Ref] + +object Component: + given [Ref <: dom.html.Element] + : Conversion[Component[Ref], ReactiveHtmlElement[Ref]] with + def apply(component: Component[Ref]): ReactiveHtmlElement[Ref] = + component.element diff --git a/ui/src/ui/components/tailwind/Switch.scala b/ui/src/ui/components/tailwind/Switch.scala new file mode 100644 index 0000000..a02ae58 --- /dev/null +++ b/ui/src/ui/components/tailwind/Switch.scala @@ -0,0 +1,35 @@ +package ui.components.tailwind + +import com.raquo.laminar.api.L.{*, given} + +object Switch { + def apply(name: String, toggle: Var[Boolean]): HtmlElement = + div( + cls := "flex items-center", + button( + tpe := "button", + cls := "relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + cls <-- toggle.signal.map(a => + if a then "bg-indigo-600" else "bg-gray-200" + ), + role := "switch", + dataAttr("aria-checked") := "false", + dataAttr("aria-labelledby") := "active-only-label", + span( + dataAttr("aria-hidden") := "true", + cls := "pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200", + cls <-- toggle.signal.map(a => + if a then "translate-x-5" else "translate-x-0" + ) + ), + composeEvents(onClick)( + _.sample(toggle.signal).map(a => !a) + ) --> toggle.writer + ), + span( + cls := "ml-3", + idAttr := "active-only-label", + span(cls := "text-sm font-medium text-gray-900", name) + ) + ) +} diff --git a/ui/src/ui/Component.scala b/ui/src/ui/Component.scala deleted file mode 100644 index b912053..0000000 --- a/ui/src/ui/Component.scala +++ /dev/null @@ -1,13 +0,0 @@ -package ui - -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -trait Component[Ref <: dom.html.Element]: - def element: ReactiveHtmlElement[Ref] - -object Component: - given [Ref <: dom.html.Element] - : Conversion[Component[Ref], ReactiveHtmlElement[Ref]] with - def apply(component: Component[Ref]): ReactiveHtmlElement[Ref] = - component.element diff --git a/ui/src/ui/components/tailwind/Component.scala b/ui/src/ui/components/tailwind/Component.scala new file mode 100644 index 0000000..c22e6e4 --- /dev/null +++ b/ui/src/ui/components/tailwind/Component.scala @@ -0,0 +1,13 @@ +package works.iterative.ui.components.tailwind + +import com.raquo.laminar.nodes.ReactiveHtmlElement +import org.scalajs.dom + +trait Component[Ref <: dom.html.Element]: + def element: ReactiveHtmlElement[Ref] + +object Component: + given [Ref <: dom.html.Element] + : Conversion[Component[Ref], ReactiveHtmlElement[Ref]] with + def apply(component: Component[Ref]): ReactiveHtmlElement[Ref] = + component.element diff --git a/ui/src/ui/components/tailwind/Switch.scala b/ui/src/ui/components/tailwind/Switch.scala new file mode 100644 index 0000000..a02ae58 --- /dev/null +++ b/ui/src/ui/components/tailwind/Switch.scala @@ -0,0 +1,35 @@ +package ui.components.tailwind + +import com.raquo.laminar.api.L.{*, given} + +object Switch { + def apply(name: String, toggle: Var[Boolean]): HtmlElement = + div( + cls := "flex items-center", + button( + tpe := "button", + cls := "relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + cls <-- toggle.signal.map(a => + if a then "bg-indigo-600" else "bg-gray-200" + ), + role := "switch", + dataAttr("aria-checked") := "false", + dataAttr("aria-labelledby") := "active-only-label", + span( + dataAttr("aria-hidden") := "true", + cls := "pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200", + cls <-- toggle.signal.map(a => + if a then "translate-x-5" else "translate-x-0" + ) + ), + composeEvents(onClick)( + _.sample(toggle.signal).map(a => !a) + ) --> toggle.writer + ), + span( + cls := "ml-3", + idAttr := "active-only-label", + span(cls := "text-sm font-medium text-gray-900", name) + ) + ) +} diff --git a/ui/src/ui/components/tailwind/list/BaseList.scala b/ui/src/ui/components/tailwind/list/BaseList.scala deleted file mode 100644 index 04972e8..0000000 --- a/ui/src/ui/components/tailwind/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package works.iterative.ui.components.tailwind -package list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`("w-6 h-6 text-gray-400") - ) diff --git a/ui/src/ui/Component.scala b/ui/src/ui/Component.scala deleted file mode 100644 index b912053..0000000 --- a/ui/src/ui/Component.scala +++ /dev/null @@ -1,13 +0,0 @@ -package ui - -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -trait Component[Ref <: dom.html.Element]: - def element: ReactiveHtmlElement[Ref] - -object Component: - given [Ref <: dom.html.Element] - : Conversion[Component[Ref], ReactiveHtmlElement[Ref]] with - def apply(component: Component[Ref]): ReactiveHtmlElement[Ref] = - component.element diff --git a/ui/src/ui/components/tailwind/Component.scala b/ui/src/ui/components/tailwind/Component.scala new file mode 100644 index 0000000..c22e6e4 --- /dev/null +++ b/ui/src/ui/components/tailwind/Component.scala @@ -0,0 +1,13 @@ +package works.iterative.ui.components.tailwind + +import com.raquo.laminar.nodes.ReactiveHtmlElement +import org.scalajs.dom + +trait Component[Ref <: dom.html.Element]: + def element: ReactiveHtmlElement[Ref] + +object Component: + given [Ref <: dom.html.Element] + : Conversion[Component[Ref], ReactiveHtmlElement[Ref]] with + def apply(component: Component[Ref]): ReactiveHtmlElement[Ref] = + component.element diff --git a/ui/src/ui/components/tailwind/Switch.scala b/ui/src/ui/components/tailwind/Switch.scala new file mode 100644 index 0000000..a02ae58 --- /dev/null +++ b/ui/src/ui/components/tailwind/Switch.scala @@ -0,0 +1,35 @@ +package ui.components.tailwind + +import com.raquo.laminar.api.L.{*, given} + +object Switch { + def apply(name: String, toggle: Var[Boolean]): HtmlElement = + div( + cls := "flex items-center", + button( + tpe := "button", + cls := "relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + cls <-- toggle.signal.map(a => + if a then "bg-indigo-600" else "bg-gray-200" + ), + role := "switch", + dataAttr("aria-checked") := "false", + dataAttr("aria-labelledby") := "active-only-label", + span( + dataAttr("aria-hidden") := "true", + cls := "pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200", + cls <-- toggle.signal.map(a => + if a then "translate-x-5" else "translate-x-0" + ) + ), + composeEvents(onClick)( + _.sample(toggle.signal).map(a => !a) + ) --> toggle.writer + ), + span( + cls := "ml-3", + idAttr := "active-only-label", + span(cls := "text-sm font-medium text-gray-900", name) + ) + ) +} diff --git a/ui/src/ui/components/tailwind/list/BaseList.scala b/ui/src/ui/components/tailwind/list/BaseList.scala deleted file mode 100644 index 04972e8..0000000 --- a/ui/src/ui/components/tailwind/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package works.iterative.ui.components.tailwind -package list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`("w-6 h-6 text-gray-400") - ) diff --git a/ui/src/ui/components/tailwind/list/ListRow.scala b/ui/src/ui/components/tailwind/list/ListRow.scala index efe11fa..e4a5bc5 100644 --- a/ui/src/ui/components/tailwind/list/ListRow.scala +++ b/ui/src/ui/components/tailwind/list/ListRow.scala @@ -4,18 +4,21 @@ import com.raquo.laminar.api.L.{*, given} import org.scalajs.dom import com.raquo.laminar.builders.HtmlTag +import com.raquo.laminar.nodes.ReactiveHtmlElement -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - linkMods: Option[Modifier[Anchor]] = None - ) +trait AsListRow[A]: + extension (a: A) def asListRow: ListRow - def content(m: ViewModel): Modifier[HtmlElement] = +final case class ListRow( + title: String, + topRight: Modifier[HtmlElement], + bottomLeft: Modifier[HtmlElement], + bottomRight: Modifier[HtmlElement], + farRight: Modifier[HtmlElement], + linkMods: Option[Modifier[Anchor]] = None +) extends Component[org.scalajs.dom.html.LI]: + + def content: Modifier[HtmlElement] = Seq( cls := "block hover:bg-gray-50", div( @@ -26,29 +29,27 @@ cls := "flex items-center justify-between", p( cls := "text-sm font-medium text-indigo-600 truncate", - m.title + title ), div( cls := "ml-2 flex-shrink-0 flex", - m.topRight + topRight ) ), div( cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight + bottomLeft, + bottomRight ) ), - m.farRight + farRight ) ) - def apply($m: Signal[ViewModel]): HtmlElement = + def element: ReactiveHtmlElement[org.scalajs.dom.html.LI] = + val c = content li( - child <-- $m.map { m => - val c = content(m) - m.linkMods match - case Some(m) => a(m, c) - case _ => div(c) - } + linkMods match + case Some(m) => a(m, c) + case _ => div(c) ) diff --git a/ui/src/ui/Component.scala b/ui/src/ui/Component.scala deleted file mode 100644 index b912053..0000000 --- a/ui/src/ui/Component.scala +++ /dev/null @@ -1,13 +0,0 @@ -package ui - -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -trait Component[Ref <: dom.html.Element]: - def element: ReactiveHtmlElement[Ref] - -object Component: - given [Ref <: dom.html.Element] - : Conversion[Component[Ref], ReactiveHtmlElement[Ref]] with - def apply(component: Component[Ref]): ReactiveHtmlElement[Ref] = - component.element diff --git a/ui/src/ui/components/tailwind/Component.scala b/ui/src/ui/components/tailwind/Component.scala new file mode 100644 index 0000000..c22e6e4 --- /dev/null +++ b/ui/src/ui/components/tailwind/Component.scala @@ -0,0 +1,13 @@ +package works.iterative.ui.components.tailwind + +import com.raquo.laminar.nodes.ReactiveHtmlElement +import org.scalajs.dom + +trait Component[Ref <: dom.html.Element]: + def element: ReactiveHtmlElement[Ref] + +object Component: + given [Ref <: dom.html.Element] + : Conversion[Component[Ref], ReactiveHtmlElement[Ref]] with + def apply(component: Component[Ref]): ReactiveHtmlElement[Ref] = + component.element diff --git a/ui/src/ui/components/tailwind/Switch.scala b/ui/src/ui/components/tailwind/Switch.scala new file mode 100644 index 0000000..a02ae58 --- /dev/null +++ b/ui/src/ui/components/tailwind/Switch.scala @@ -0,0 +1,35 @@ +package ui.components.tailwind + +import com.raquo.laminar.api.L.{*, given} + +object Switch { + def apply(name: String, toggle: Var[Boolean]): HtmlElement = + div( + cls := "flex items-center", + button( + tpe := "button", + cls := "relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + cls <-- toggle.signal.map(a => + if a then "bg-indigo-600" else "bg-gray-200" + ), + role := "switch", + dataAttr("aria-checked") := "false", + dataAttr("aria-labelledby") := "active-only-label", + span( + dataAttr("aria-hidden") := "true", + cls := "pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200", + cls <-- toggle.signal.map(a => + if a then "translate-x-5" else "translate-x-0" + ) + ), + composeEvents(onClick)( + _.sample(toggle.signal).map(a => !a) + ) --> toggle.writer + ), + span( + cls := "ml-3", + idAttr := "active-only-label", + span(cls := "text-sm font-medium text-gray-900", name) + ) + ) +} diff --git a/ui/src/ui/components/tailwind/list/BaseList.scala b/ui/src/ui/components/tailwind/list/BaseList.scala deleted file mode 100644 index 04972e8..0000000 --- a/ui/src/ui/components/tailwind/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package works.iterative.ui.components.tailwind -package list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`("w-6 h-6 text-gray-400") - ) diff --git a/ui/src/ui/components/tailwind/list/ListRow.scala b/ui/src/ui/components/tailwind/list/ListRow.scala index efe11fa..e4a5bc5 100644 --- a/ui/src/ui/components/tailwind/list/ListRow.scala +++ b/ui/src/ui/components/tailwind/list/ListRow.scala @@ -4,18 +4,21 @@ import com.raquo.laminar.api.L.{*, given} import org.scalajs.dom import com.raquo.laminar.builders.HtmlTag +import com.raquo.laminar.nodes.ReactiveHtmlElement -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - linkMods: Option[Modifier[Anchor]] = None - ) +trait AsListRow[A]: + extension (a: A) def asListRow: ListRow - def content(m: ViewModel): Modifier[HtmlElement] = +final case class ListRow( + title: String, + topRight: Modifier[HtmlElement], + bottomLeft: Modifier[HtmlElement], + bottomRight: Modifier[HtmlElement], + farRight: Modifier[HtmlElement], + linkMods: Option[Modifier[Anchor]] = None +) extends Component[org.scalajs.dom.html.LI]: + + def content: Modifier[HtmlElement] = Seq( cls := "block hover:bg-gray-50", div( @@ -26,29 +29,27 @@ cls := "flex items-center justify-between", p( cls := "text-sm font-medium text-indigo-600 truncate", - m.title + title ), div( cls := "ml-2 flex-shrink-0 flex", - m.topRight + topRight ) ), div( cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight + bottomLeft, + bottomRight ) ), - m.farRight + farRight ) ) - def apply($m: Signal[ViewModel]): HtmlElement = + def element: ReactiveHtmlElement[org.scalajs.dom.html.LI] = + val c = content li( - child <-- $m.map { m => - val c = content(m) - m.linkMods match - case Some(m) => a(m, c) - case _ => div(c) - } + linkMods match + case Some(m) => a(m, c) + case _ => div(c) ) diff --git a/ui/src/ui/components/tailwind/list/RowTag.scala b/ui/src/ui/components/tailwind/list/RowTag.scala index 129abe4..8599be5 100644 --- a/ui/src/ui/components/tailwind/list/RowTag.scala +++ b/ui/src/ui/components/tailwind/list/RowTag.scala @@ -4,14 +4,13 @@ import com.raquo.laminar.api.L.{*, given} object RowTag: - case class ViewModel(text: String, color: Color) - def render($m: Signal[ViewModel]): HtmlElement = + def apply(text: String, color: Color): HtmlElement = inline def colorClass(color: Color): Seq[String] = import ColorWeight._ List(color.bg(w100), color.text(w800)) p( cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls <-- $m.map(t => colorClass(t.color)), - child.text <-- $m.map(_.text) + cls(colorClass(color)), + text ) diff --git a/ui/src/ui/Component.scala b/ui/src/ui/Component.scala deleted file mode 100644 index b912053..0000000 --- a/ui/src/ui/Component.scala +++ /dev/null @@ -1,13 +0,0 @@ -package ui - -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -trait Component[Ref <: dom.html.Element]: - def element: ReactiveHtmlElement[Ref] - -object Component: - given [Ref <: dom.html.Element] - : Conversion[Component[Ref], ReactiveHtmlElement[Ref]] with - def apply(component: Component[Ref]): ReactiveHtmlElement[Ref] = - component.element diff --git a/ui/src/ui/components/tailwind/Component.scala b/ui/src/ui/components/tailwind/Component.scala new file mode 100644 index 0000000..c22e6e4 --- /dev/null +++ b/ui/src/ui/components/tailwind/Component.scala @@ -0,0 +1,13 @@ +package works.iterative.ui.components.tailwind + +import com.raquo.laminar.nodes.ReactiveHtmlElement +import org.scalajs.dom + +trait Component[Ref <: dom.html.Element]: + def element: ReactiveHtmlElement[Ref] + +object Component: + given [Ref <: dom.html.Element] + : Conversion[Component[Ref], ReactiveHtmlElement[Ref]] with + def apply(component: Component[Ref]): ReactiveHtmlElement[Ref] = + component.element diff --git a/ui/src/ui/components/tailwind/Switch.scala b/ui/src/ui/components/tailwind/Switch.scala new file mode 100644 index 0000000..a02ae58 --- /dev/null +++ b/ui/src/ui/components/tailwind/Switch.scala @@ -0,0 +1,35 @@ +package ui.components.tailwind + +import com.raquo.laminar.api.L.{*, given} + +object Switch { + def apply(name: String, toggle: Var[Boolean]): HtmlElement = + div( + cls := "flex items-center", + button( + tpe := "button", + cls := "relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + cls <-- toggle.signal.map(a => + if a then "bg-indigo-600" else "bg-gray-200" + ), + role := "switch", + dataAttr("aria-checked") := "false", + dataAttr("aria-labelledby") := "active-only-label", + span( + dataAttr("aria-hidden") := "true", + cls := "pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200", + cls <-- toggle.signal.map(a => + if a then "translate-x-5" else "translate-x-0" + ) + ), + composeEvents(onClick)( + _.sample(toggle.signal).map(a => !a) + ) --> toggle.writer + ), + span( + cls := "ml-3", + idAttr := "active-only-label", + span(cls := "text-sm font-medium text-gray-900", name) + ) + ) +} diff --git a/ui/src/ui/components/tailwind/list/BaseList.scala b/ui/src/ui/components/tailwind/list/BaseList.scala deleted file mode 100644 index 04972e8..0000000 --- a/ui/src/ui/components/tailwind/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package works.iterative.ui.components.tailwind -package list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`("w-6 h-6 text-gray-400") - ) diff --git a/ui/src/ui/components/tailwind/list/ListRow.scala b/ui/src/ui/components/tailwind/list/ListRow.scala index efe11fa..e4a5bc5 100644 --- a/ui/src/ui/components/tailwind/list/ListRow.scala +++ b/ui/src/ui/components/tailwind/list/ListRow.scala @@ -4,18 +4,21 @@ import com.raquo.laminar.api.L.{*, given} import org.scalajs.dom import com.raquo.laminar.builders.HtmlTag +import com.raquo.laminar.nodes.ReactiveHtmlElement -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - linkMods: Option[Modifier[Anchor]] = None - ) +trait AsListRow[A]: + extension (a: A) def asListRow: ListRow - def content(m: ViewModel): Modifier[HtmlElement] = +final case class ListRow( + title: String, + topRight: Modifier[HtmlElement], + bottomLeft: Modifier[HtmlElement], + bottomRight: Modifier[HtmlElement], + farRight: Modifier[HtmlElement], + linkMods: Option[Modifier[Anchor]] = None +) extends Component[org.scalajs.dom.html.LI]: + + def content: Modifier[HtmlElement] = Seq( cls := "block hover:bg-gray-50", div( @@ -26,29 +29,27 @@ cls := "flex items-center justify-between", p( cls := "text-sm font-medium text-indigo-600 truncate", - m.title + title ), div( cls := "ml-2 flex-shrink-0 flex", - m.topRight + topRight ) ), div( cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight + bottomLeft, + bottomRight ) ), - m.farRight + farRight ) ) - def apply($m: Signal[ViewModel]): HtmlElement = + def element: ReactiveHtmlElement[org.scalajs.dom.html.LI] = + val c = content li( - child <-- $m.map { m => - val c = content(m) - m.linkMods match - case Some(m) => a(m, c) - case _ => div(c) - } + linkMods match + case Some(m) => a(m, c) + case _ => div(c) ) diff --git a/ui/src/ui/components/tailwind/list/RowTag.scala b/ui/src/ui/components/tailwind/list/RowTag.scala index 129abe4..8599be5 100644 --- a/ui/src/ui/components/tailwind/list/RowTag.scala +++ b/ui/src/ui/components/tailwind/list/RowTag.scala @@ -4,14 +4,13 @@ import com.raquo.laminar.api.L.{*, given} object RowTag: - case class ViewModel(text: String, color: Color) - def render($m: Signal[ViewModel]): HtmlElement = + def apply(text: String, color: Color): HtmlElement = inline def colorClass(color: Color): Seq[String] = import ColorWeight._ List(color.bg(w100), color.text(w800)) p( cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls <-- $m.map(t => colorClass(t.color)), - child.text <-- $m.map(_.text) + cls(colorClass(color)), + text ) diff --git a/ui/src/ui/components/tailwind/list/StackedList.scala b/ui/src/ui/components/tailwind/list/StackedList.scala index 6059362..47b8de5 100644 --- a/ui/src/ui/components/tailwind/list/StackedList.scala +++ b/ui/src/ui/components/tailwind/list/StackedList.scala @@ -2,15 +2,44 @@ package list import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom.html.UList +import com.raquo.laminar.nodes.ReactiveHtmlElement -class StackedList[Item]: - type ViewModel = List[Item] - def apply( - $m: Signal[ViewModel], - keyF: Item => String - )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = +class StackedList[Item: AsListRow]: + import StackedList.* + def apply(items: List[Item]): ReactiveHtmlElement[UList] = ul( role := "list", cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) + items.map(d => d.asListRow) + ) + + def withMod( + items: List[Item] + ): Modifier[HtmlElement] => ReactiveHtmlElement[UList] = mods => + ul( + role := "list", + cls := "divide-y divide-gray-200", + mods, + items.map(d => d.asListRow) + ) + + def grouped(items: List[Item], groupBy: Item => String): List[HtmlElement] = + items.groupBy(groupBy).to(List).sortBy(_._1).map { case (c, i) => + withHeader(c)(withMod(i)) + } + +object StackedList: + def withHeader(header: String)( + content: Modifier[HtmlElement] => ReactiveHtmlElement[UList] + ): HtmlElement = + div( + cls("relative"), + h3( + 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 + ), + content(cls("relative")) )