diff --git a/ui/src/ui/components/headless/Items.scala b/ui/src/ui/components/headless/Items.scala index 3f217da..aea5ec0 100644 --- a/ui/src/ui/components/headless/Items.scala +++ b/ui/src/ui/components/headless/Items.scala @@ -4,11 +4,30 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.laminar.nodes.ReactiveHtmlElement -class Items[A]( - frame: Modifier[HtmlElement] => HtmlElement, - item: A => HtmlElement -): - def apply(items: Seq[A]): HtmlElement = frame(items.map(a => li(item(a)))) - def contramap[B](f: B => A): Items[B] = Items(frame, b => item(f(b))) - def mapItem(f: A => HtmlElement => HtmlElement): Items[A] = - Items(frame, a => f(a)(item(a))) +trait ItemContainer[A]: + def contramap[B](f: B => A): ItemContainer[B] + def map(f: A => HtmlElement => HtmlElement): ItemContainer[A] + +final case class Items[A]( + frame: Seq[HtmlElement] => HtmlElement, + renderItem: A => HtmlElement +) extends ItemContainer[A]: + def apply(items: Seq[A]): HtmlElement = frame( + items.map(a => li(renderItem(a))) + ) + def contramap[B](f: B => A): Items[B] = + Items(frame, b => renderItem(f(b))) + def map(f: A => HtmlElement => HtmlElement): Items[A] = + Items(frame, a => f(a)(renderItem(a))) + +final case class GroupedItems[Key, A]( + frame: Seq[HtmlElement] => HtmlElement, + renderItems: Key => Items[A] +) extends ItemContainer[A]: + def apply(items: Seq[(Key, Seq[A])]): HtmlElement = frame( + items.map(renderItems(_)(_)) + ) + def contramap[B](f: B => A): GroupedItems[Key, B] = + GroupedItems(frame, k => renderItems(k).contramap(f)) + def map(f: A => HtmlElement => HtmlElement): GroupedItems[Key, A] = + GroupedItems(frame, k => renderItems(k).map(f)) diff --git a/ui/src/ui/components/headless/Items.scala b/ui/src/ui/components/headless/Items.scala index 3f217da..aea5ec0 100644 --- a/ui/src/ui/components/headless/Items.scala +++ b/ui/src/ui/components/headless/Items.scala @@ -4,11 +4,30 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.laminar.nodes.ReactiveHtmlElement -class Items[A]( - frame: Modifier[HtmlElement] => HtmlElement, - item: A => HtmlElement -): - def apply(items: Seq[A]): HtmlElement = frame(items.map(a => li(item(a)))) - def contramap[B](f: B => A): Items[B] = Items(frame, b => item(f(b))) - def mapItem(f: A => HtmlElement => HtmlElement): Items[A] = - Items(frame, a => f(a)(item(a))) +trait ItemContainer[A]: + def contramap[B](f: B => A): ItemContainer[B] + def map(f: A => HtmlElement => HtmlElement): ItemContainer[A] + +final case class Items[A]( + frame: Seq[HtmlElement] => HtmlElement, + renderItem: A => HtmlElement +) extends ItemContainer[A]: + def apply(items: Seq[A]): HtmlElement = frame( + items.map(a => li(renderItem(a))) + ) + def contramap[B](f: B => A): Items[B] = + Items(frame, b => renderItem(f(b))) + def map(f: A => HtmlElement => HtmlElement): Items[A] = + Items(frame, a => f(a)(renderItem(a))) + +final case class GroupedItems[Key, A]( + frame: Seq[HtmlElement] => HtmlElement, + renderItems: Key => Items[A] +) extends ItemContainer[A]: + def apply(items: Seq[(Key, Seq[A])]): HtmlElement = frame( + items.map(renderItems(_)(_)) + ) + def contramap[B](f: B => A): GroupedItems[Key, B] = + GroupedItems(frame, k => renderItems(k).contramap(f)) + def map(f: A => HtmlElement => HtmlElement): GroupedItems[Key, A] = + GroupedItems(frame, k => renderItems(k).map(f)) diff --git a/ui/src/ui/components/headless/Toggle.scala b/ui/src/ui/components/headless/Toggle.scala new file mode 100644 index 0000000..8e6cab2 --- /dev/null +++ b/ui/src/ui/components/headless/Toggle.scala @@ -0,0 +1,31 @@ +package works.iterative +package ui.components.headless + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.laminar.nodes.ReactiveHtmlElement + +object Toggle: + + final case class Ctx( + trigger: Modifier[HtmlElement], + toggle: Seq[HtmlElement] => Signal[Seq[HtmlElement]] + ) + + def apply[U <: org.scalajs.dom.html.Element]( + children: Ctx => ReactiveHtmlElement[U] + ): ReactiveHtmlElement[U] = apply(true)(children) + + def apply[U <: org.scalajs.dom.html.Element](initialValue: Boolean)( + children: Ctx => ReactiveHtmlElement[U] + ): ReactiveHtmlElement[U] = + val state: Var[Boolean] = Var(initialValue) + children( + Ctx( + composeEvents(onClick)(_.sample(state).map(v => !v)) --> state, + el => + state.signal.map { + case true => el + case _ => Nil + } + ) + ) diff --git a/ui/src/ui/components/headless/Items.scala b/ui/src/ui/components/headless/Items.scala index 3f217da..aea5ec0 100644 --- a/ui/src/ui/components/headless/Items.scala +++ b/ui/src/ui/components/headless/Items.scala @@ -4,11 +4,30 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.laminar.nodes.ReactiveHtmlElement -class Items[A]( - frame: Modifier[HtmlElement] => HtmlElement, - item: A => HtmlElement -): - def apply(items: Seq[A]): HtmlElement = frame(items.map(a => li(item(a)))) - def contramap[B](f: B => A): Items[B] = Items(frame, b => item(f(b))) - def mapItem(f: A => HtmlElement => HtmlElement): Items[A] = - Items(frame, a => f(a)(item(a))) +trait ItemContainer[A]: + def contramap[B](f: B => A): ItemContainer[B] + def map(f: A => HtmlElement => HtmlElement): ItemContainer[A] + +final case class Items[A]( + frame: Seq[HtmlElement] => HtmlElement, + renderItem: A => HtmlElement +) extends ItemContainer[A]: + def apply(items: Seq[A]): HtmlElement = frame( + items.map(a => li(renderItem(a))) + ) + def contramap[B](f: B => A): Items[B] = + Items(frame, b => renderItem(f(b))) + def map(f: A => HtmlElement => HtmlElement): Items[A] = + Items(frame, a => f(a)(renderItem(a))) + +final case class GroupedItems[Key, A]( + frame: Seq[HtmlElement] => HtmlElement, + renderItems: Key => Items[A] +) extends ItemContainer[A]: + def apply(items: Seq[(Key, Seq[A])]): HtmlElement = frame( + items.map(renderItems(_)(_)) + ) + def contramap[B](f: B => A): GroupedItems[Key, B] = + GroupedItems(frame, k => renderItems(k).contramap(f)) + def map(f: A => HtmlElement => HtmlElement): GroupedItems[Key, A] = + GroupedItems(frame, k => renderItems(k).map(f)) diff --git a/ui/src/ui/components/headless/Toggle.scala b/ui/src/ui/components/headless/Toggle.scala new file mode 100644 index 0000000..8e6cab2 --- /dev/null +++ b/ui/src/ui/components/headless/Toggle.scala @@ -0,0 +1,31 @@ +package works.iterative +package ui.components.headless + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.laminar.nodes.ReactiveHtmlElement + +object Toggle: + + final case class Ctx( + trigger: Modifier[HtmlElement], + toggle: Seq[HtmlElement] => Signal[Seq[HtmlElement]] + ) + + def apply[U <: org.scalajs.dom.html.Element]( + children: Ctx => ReactiveHtmlElement[U] + ): ReactiveHtmlElement[U] = apply(true)(children) + + def apply[U <: org.scalajs.dom.html.Element](initialValue: Boolean)( + children: Ctx => ReactiveHtmlElement[U] + ): ReactiveHtmlElement[U] = + val state: Var[Boolean] = Var(initialValue) + children( + Ctx( + composeEvents(onClick)(_.sample(state).map(v => !v)) --> state, + el => + state.signal.map { + case true => el + case _ => Nil + } + ) + ) diff --git a/ui/src/ui/components/tailwind/list/StackedList.scala b/ui/src/ui/components/tailwind/list/StackedList.scala index 0602afa..176791e 100644 --- a/ui/src/ui/components/tailwind/list/StackedList.scala +++ b/ui/src/ui/components/tailwind/list/StackedList.scala @@ -5,6 +5,8 @@ import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement import works.iterative.ui.components.headless.Items +import works.iterative.ui.components.headless.GroupedItems +import works.iterative.ui.components.headless.Toggle class StackedList[Item: AsListRow]: import StackedList.* @@ -86,9 +88,12 @@ text ) - def item(i: Item): Div = + private def item(i: Item): Div = item(i, None) + + private def item(i: Item, extraClasses: Option[String]): Div = div( cls := "px-4 py-4 sm:px-6 items-center flex", + extraClasses.map(cls(_)), div( cls := "min-w-0 flex-1 pr-4", div( @@ -107,7 +112,23 @@ ) ) - def frame: Modifier[HtmlElement] => Div = + private def headerFrame(text: String): Seq[HtmlElement] => Div = + content => + Toggle(ctx => + 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" + ), + text, + ctx.trigger + ), + children <-- ctx.toggle(content) + ) + ) + + private def frame: Seq[HtmlElement] => Div = el => div( cls("bg-white shadow overflow-hidden sm:rounded-md"), @@ -116,3 +137,9 @@ def apply[A](f: this.type => A => Item): Items[A] = Items(frame, item).contramap(f(this)) + + def grouped[A](f: this.type => A => Item): GroupedItems[String, A] = + GroupedItems( + frame, + k => Items(headerFrame(k), item(_, Some("relative"))).contramap(f(this)) + )