Newer
Older
support / ui / src / ui / components / tailwind / list / BaseList.scala
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")
    )