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 index bf149dc..db5e08f 100644 --- 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 @@ -9,7 +9,7 @@ trait ButtonComponents: def primaryButton( id: String, - text: String, + text: Modifier[HtmlElement], icon: Option[SvgElement] = None )( mods: Modifier[HtmlElement]* @@ -17,7 +17,7 @@ def secondaryButton( id: String, - text: String, + text: Modifier[HtmlElement], icon: Option[SvgElement] = None )( mods: Modifier[HtmlElement]* @@ -43,7 +43,7 @@ override def primaryButton( id: String, - text: String, + text: Modifier[HtmlElement], icon: Option[SvgElement] = None )(mods: Modifier[HtmlElement]*): HtmlElement = button( @@ -56,7 +56,7 @@ override def secondaryButton( id: String, - text: String, + text: Modifier[HtmlElement], icon: Option[SvgElement] = None )(mods: Modifier[HtmlElement]*): HtmlElement = button( 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 index bf149dc..db5e08f 100644 --- 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 @@ -9,7 +9,7 @@ trait ButtonComponents: def primaryButton( id: String, - text: String, + text: Modifier[HtmlElement], icon: Option[SvgElement] = None )( mods: Modifier[HtmlElement]* @@ -17,7 +17,7 @@ def secondaryButton( id: String, - text: String, + text: Modifier[HtmlElement], icon: Option[SvgElement] = None )( mods: Modifier[HtmlElement]* @@ -43,7 +43,7 @@ override def primaryButton( id: String, - text: String, + text: Modifier[HtmlElement], icon: Option[SvgElement] = None )(mods: Modifier[HtmlElement]*): HtmlElement = button( @@ -56,7 +56,7 @@ override def secondaryButton( id: String, - text: String, + text: Modifier[HtmlElement], icon: Option[SvgElement] = None )(mods: Modifier[HtmlElement]*): HtmlElement = button( 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 new file mode 100644 index 0000000..3f911eb --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala @@ -0,0 +1,101 @@ +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(cells: HtmlElement*): HtmlElement + + def dataRow(cells: HtmlElement*): HtmlElement + + def headerCell(content: Modifier[HtmlElement]): HtmlElement + + def dataCell(content: Modifier[HtmlElement]): HtmlElement + +trait DefaultTableComponentsModule extends TableComponentsModule: + + override 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(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(cells: HtmlElement*): HtmlElement = + tr( + 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/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala index bf149dc..db5e08f 100644 --- 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 @@ -9,7 +9,7 @@ trait ButtonComponents: def primaryButton( id: String, - text: String, + text: Modifier[HtmlElement], icon: Option[SvgElement] = None )( mods: Modifier[HtmlElement]* @@ -17,7 +17,7 @@ def secondaryButton( id: String, - text: String, + text: Modifier[HtmlElement], icon: Option[SvgElement] = None )( mods: Modifier[HtmlElement]* @@ -43,7 +43,7 @@ override def primaryButton( id: String, - text: String, + text: Modifier[HtmlElement], icon: Option[SvgElement] = None )(mods: Modifier[HtmlElement]*): HtmlElement = button( @@ -56,7 +56,7 @@ override def secondaryButton( id: String, - text: String, + text: Modifier[HtmlElement], icon: Option[SvgElement] = None )(mods: Modifier[HtmlElement]*): HtmlElement = button( 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 new file mode 100644 index 0000000..3f911eb --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala @@ -0,0 +1,101 @@ +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(cells: HtmlElement*): HtmlElement + + def dataRow(cells: HtmlElement*): HtmlElement + + def headerCell(content: Modifier[HtmlElement]): HtmlElement + + def dataCell(content: Modifier[HtmlElement]): HtmlElement + +trait DefaultTableComponentsModule extends TableComponentsModule: + + override 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(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(cells: HtmlElement*): HtmlElement = + tr( + 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/tailwind/laminar/LaminarExtensions.scala b/ui/js/src/main/scala/works/iterative/ui/components/tailwind/laminar/LaminarExtensions.scala index 3899808..a5093b3 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 @@ -3,6 +3,7 @@ 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 @@ -17,3 +18,8 @@ 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 + inline def apply(msg: UserMessage) = ctx.messages(msg) 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 index bf149dc..db5e08f 100644 --- 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 @@ -9,7 +9,7 @@ trait ButtonComponents: def primaryButton( id: String, - text: String, + text: Modifier[HtmlElement], icon: Option[SvgElement] = None )( mods: Modifier[HtmlElement]* @@ -17,7 +17,7 @@ def secondaryButton( id: String, - text: String, + text: Modifier[HtmlElement], icon: Option[SvgElement] = None )( mods: Modifier[HtmlElement]* @@ -43,7 +43,7 @@ override def primaryButton( id: String, - text: String, + text: Modifier[HtmlElement], icon: Option[SvgElement] = None )(mods: Modifier[HtmlElement]*): HtmlElement = button( @@ -56,7 +56,7 @@ override def secondaryButton( id: String, - text: String, + text: Modifier[HtmlElement], icon: Option[SvgElement] = None )(mods: Modifier[HtmlElement]*): HtmlElement = button( 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 new file mode 100644 index 0000000..3f911eb --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala @@ -0,0 +1,101 @@ +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(cells: HtmlElement*): HtmlElement + + def dataRow(cells: HtmlElement*): HtmlElement + + def headerCell(content: Modifier[HtmlElement]): HtmlElement + + def dataCell(content: Modifier[HtmlElement]): HtmlElement + +trait DefaultTableComponentsModule extends TableComponentsModule: + + override 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(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(cells: HtmlElement*): HtmlElement = + tr( + 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/tailwind/laminar/LaminarExtensions.scala b/ui/js/src/main/scala/works/iterative/ui/components/tailwind/laminar/LaminarExtensions.scala index 3899808..a5093b3 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 @@ -3,6 +3,7 @@ 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 @@ -17,3 +18,8 @@ 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 + inline def apply(msg: UserMessage) = ctx.messages(msg) diff --git a/ui/js/src/main/scala/works/iterative/ui/services/ConsoleNotificationService.scala b/ui/js/src/main/scala/works/iterative/ui/services/ConsoleNotificationService.scala new file mode 100644 index 0000000..25b57d8 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/services/ConsoleNotificationService.scala @@ -0,0 +1,15 @@ +package works.iterative.ui.services + +import zio.* +import works.iterative.core.UserMessage + +class ConsoleNotificationService extends UserNotificationService: + override def notify( + level: UserNotificationService.Level, + msg: UserMessage + ): UIO[Unit] = + ZIO.succeed(org.scalajs.dom.console.log(s"[$level] $msg")) + +object ConsoleNotificationService: + val layer: ULayer[UserNotificationService] = + ZLayer.succeed(ConsoleNotificationService()) 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 index bf149dc..db5e08f 100644 --- 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 @@ -9,7 +9,7 @@ trait ButtonComponents: def primaryButton( id: String, - text: String, + text: Modifier[HtmlElement], icon: Option[SvgElement] = None )( mods: Modifier[HtmlElement]* @@ -17,7 +17,7 @@ def secondaryButton( id: String, - text: String, + text: Modifier[HtmlElement], icon: Option[SvgElement] = None )( mods: Modifier[HtmlElement]* @@ -43,7 +43,7 @@ override def primaryButton( id: String, - text: String, + text: Modifier[HtmlElement], icon: Option[SvgElement] = None )(mods: Modifier[HtmlElement]*): HtmlElement = button( @@ -56,7 +56,7 @@ override def secondaryButton( id: String, - text: String, + text: Modifier[HtmlElement], icon: Option[SvgElement] = None )(mods: Modifier[HtmlElement]*): HtmlElement = button( 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 new file mode 100644 index 0000000..3f911eb --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala @@ -0,0 +1,101 @@ +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(cells: HtmlElement*): HtmlElement + + def dataRow(cells: HtmlElement*): HtmlElement + + def headerCell(content: Modifier[HtmlElement]): HtmlElement + + def dataCell(content: Modifier[HtmlElement]): HtmlElement + +trait DefaultTableComponentsModule extends TableComponentsModule: + + override 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(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(cells: HtmlElement*): HtmlElement = + tr( + 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/tailwind/laminar/LaminarExtensions.scala b/ui/js/src/main/scala/works/iterative/ui/components/tailwind/laminar/LaminarExtensions.scala index 3899808..a5093b3 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 @@ -3,6 +3,7 @@ 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 @@ -17,3 +18,8 @@ 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 + inline def apply(msg: UserMessage) = ctx.messages(msg) diff --git a/ui/js/src/main/scala/works/iterative/ui/services/ConsoleNotificationService.scala b/ui/js/src/main/scala/works/iterative/ui/services/ConsoleNotificationService.scala new file mode 100644 index 0000000..25b57d8 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/services/ConsoleNotificationService.scala @@ -0,0 +1,15 @@ +package works.iterative.ui.services + +import zio.* +import works.iterative.core.UserMessage + +class ConsoleNotificationService extends UserNotificationService: + override def notify( + level: UserNotificationService.Level, + msg: UserMessage + ): UIO[Unit] = + ZIO.succeed(org.scalajs.dom.console.log(s"[$level] $msg")) + +object ConsoleNotificationService: + val layer: ULayer[UserNotificationService] = + ZLayer.succeed(ConsoleNotificationService()) diff --git a/ui/shared/src/main/scala/works/iterative/ui/ZIOEffectHandler.scala b/ui/shared/src/main/scala/works/iterative/ui/ZIOEffectHandler.scala index 875c7a8..a3ea176 100644 --- a/ui/shared/src/main/scala/works/iterative/ui/ZIOEffectHandler.scala +++ b/ui/shared/src/main/scala/works/iterative/ui/ZIOEffectHandler.scala @@ -1,6 +1,22 @@ package works.iterative.ui +import zio.* import zio.stream.* trait ZIOEffectHandler[Env, Effect, Action]: def handle(e: Effect): ZStream[Env, Throwable, Action] + + def fromZIO( + zio: ZIO[Env, Throwable, Action] + ): ZStream[Env, Throwable, Action] = + ZStream.fromZIO(zio) + + def fromZIOOption( + zio: ZIO[Env, Throwable, Option[Action]] + ): ZStream[Env, Throwable, Action] = + ZStream.fromZIO(zio).collect { case Some(a) => a } + + def fromZIOUnit( + zio: ZIO[Env, Throwable, Unit] + ): ZStream[Env, Throwable, Action] = + ZStream.fromZIO(zio.as(Option.empty[Action])).collect { case Some(a) => a } 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 index bf149dc..db5e08f 100644 --- 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 @@ -9,7 +9,7 @@ trait ButtonComponents: def primaryButton( id: String, - text: String, + text: Modifier[HtmlElement], icon: Option[SvgElement] = None )( mods: Modifier[HtmlElement]* @@ -17,7 +17,7 @@ def secondaryButton( id: String, - text: String, + text: Modifier[HtmlElement], icon: Option[SvgElement] = None )( mods: Modifier[HtmlElement]* @@ -43,7 +43,7 @@ override def primaryButton( id: String, - text: String, + text: Modifier[HtmlElement], icon: Option[SvgElement] = None )(mods: Modifier[HtmlElement]*): HtmlElement = button( @@ -56,7 +56,7 @@ override def secondaryButton( id: String, - text: String, + text: Modifier[HtmlElement], icon: Option[SvgElement] = None )(mods: Modifier[HtmlElement]*): HtmlElement = button( 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 new file mode 100644 index 0000000..3f911eb --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala @@ -0,0 +1,101 @@ +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(cells: HtmlElement*): HtmlElement + + def dataRow(cells: HtmlElement*): HtmlElement + + def headerCell(content: Modifier[HtmlElement]): HtmlElement + + def dataCell(content: Modifier[HtmlElement]): HtmlElement + +trait DefaultTableComponentsModule extends TableComponentsModule: + + override 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(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(cells: HtmlElement*): HtmlElement = + tr( + 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/tailwind/laminar/LaminarExtensions.scala b/ui/js/src/main/scala/works/iterative/ui/components/tailwind/laminar/LaminarExtensions.scala index 3899808..a5093b3 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 @@ -3,6 +3,7 @@ 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 @@ -17,3 +18,8 @@ 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 + inline def apply(msg: UserMessage) = ctx.messages(msg) diff --git a/ui/js/src/main/scala/works/iterative/ui/services/ConsoleNotificationService.scala b/ui/js/src/main/scala/works/iterative/ui/services/ConsoleNotificationService.scala new file mode 100644 index 0000000..25b57d8 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/services/ConsoleNotificationService.scala @@ -0,0 +1,15 @@ +package works.iterative.ui.services + +import zio.* +import works.iterative.core.UserMessage + +class ConsoleNotificationService extends UserNotificationService: + override def notify( + level: UserNotificationService.Level, + msg: UserMessage + ): UIO[Unit] = + ZIO.succeed(org.scalajs.dom.console.log(s"[$level] $msg")) + +object ConsoleNotificationService: + val layer: ULayer[UserNotificationService] = + ZLayer.succeed(ConsoleNotificationService()) diff --git a/ui/shared/src/main/scala/works/iterative/ui/ZIOEffectHandler.scala b/ui/shared/src/main/scala/works/iterative/ui/ZIOEffectHandler.scala index 875c7a8..a3ea176 100644 --- a/ui/shared/src/main/scala/works/iterative/ui/ZIOEffectHandler.scala +++ b/ui/shared/src/main/scala/works/iterative/ui/ZIOEffectHandler.scala @@ -1,6 +1,22 @@ package works.iterative.ui +import zio.* import zio.stream.* trait ZIOEffectHandler[Env, Effect, Action]: def handle(e: Effect): ZStream[Env, Throwable, Action] + + def fromZIO( + zio: ZIO[Env, Throwable, Action] + ): ZStream[Env, Throwable, Action] = + ZStream.fromZIO(zio) + + def fromZIOOption( + zio: ZIO[Env, Throwable, Option[Action]] + ): ZStream[Env, Throwable, Action] = + ZStream.fromZIO(zio).collect { case Some(a) => a } + + def fromZIOUnit( + zio: ZIO[Env, Throwable, Unit] + ): ZStream[Env, Throwable, Action] = + ZStream.fromZIO(zio.as(Option.empty[Action])).collect { case Some(a) => a } diff --git a/ui/shared/src/main/scala/works/iterative/ui/model/Computable.scala b/ui/shared/src/main/scala/works/iterative/ui/model/Computable.scala index 29287ba..fa537d0 100644 --- a/ui/shared/src/main/scala/works/iterative/ui/model/Computable.scala +++ b/ui/shared/src/main/scala/works/iterative/ui/model/Computable.scala @@ -1,12 +1,48 @@ package works.iterative.ui.model import works.iterative.core.UserMessage +import java.time.Instant /** A class representing the states of a model that needs computation */ -// TODO: move to core when stable -enum Computable[Model]: - case Uninitialized extends Computable[Nothing] - case Computing extends Computable[Nothing] - case Ready(model: Model) extends Computable[Model] - case Failed(error: UserMessage) extends Computable[Nothing] +sealed trait Computable[+Model]: + /** Update the computation state with new data + */ + def update[B >: Model](m: B): Computable[B] + + /** Mark the computation as started + */ + def started: Computable[Model] + +object Computable: + /** The initial state of a computable model + */ + case object Uninitialized extends Computable[Nothing]: + override def update[B](m: B): Computable[B] = Ready(m) + override def started: Computable[Nothing] = Computing(Instant.now()) + + /** The computation is in progress + */ + case class Computing(start: Instant) extends Computable[Nothing]: + override def update[B](m: B): Computable[B] = Ready(m) + override def started: Computable[Nothing] = this + + /** The computation is finished and the data is available + */ + case class Ready[Model](model: Model) extends Computable[Model]: + override def update[B >: Model](m: B): Computable[B] = Ready(m) + override def started: Computable[Model] = Recomputing(Instant.now(), model) + + /** The computation is finished and the data is available, but it is being + * recomputed + */ + case class Recomputing[Model](start: Instant, model: Model) + extends Computable[Model]: + override def update[B >: Model](m: B): Computable[B] = Ready(m) + override def started: Computable[Model] = this + + /** The computation failed + */ + case class Failed(error: UserMessage) extends Computable[Nothing]: + override def update[B](m: B): Computable[B] = Ready(m) + override def started: Computable[Nothing] = Computing(Instant.now()) 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 index bf149dc..db5e08f 100644 --- 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 @@ -9,7 +9,7 @@ trait ButtonComponents: def primaryButton( id: String, - text: String, + text: Modifier[HtmlElement], icon: Option[SvgElement] = None )( mods: Modifier[HtmlElement]* @@ -17,7 +17,7 @@ def secondaryButton( id: String, - text: String, + text: Modifier[HtmlElement], icon: Option[SvgElement] = None )( mods: Modifier[HtmlElement]* @@ -43,7 +43,7 @@ override def primaryButton( id: String, - text: String, + text: Modifier[HtmlElement], icon: Option[SvgElement] = None )(mods: Modifier[HtmlElement]*): HtmlElement = button( @@ -56,7 +56,7 @@ override def secondaryButton( id: String, - text: String, + text: Modifier[HtmlElement], icon: Option[SvgElement] = None )(mods: Modifier[HtmlElement]*): HtmlElement = button( 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 new file mode 100644 index 0000000..3f911eb --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/TableComponentsModule.scala @@ -0,0 +1,101 @@ +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(cells: HtmlElement*): HtmlElement + + def dataRow(cells: HtmlElement*): HtmlElement + + def headerCell(content: Modifier[HtmlElement]): HtmlElement + + def dataCell(content: Modifier[HtmlElement]): HtmlElement + +trait DefaultTableComponentsModule extends TableComponentsModule: + + override 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(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(cells: HtmlElement*): HtmlElement = + tr( + 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/tailwind/laminar/LaminarExtensions.scala b/ui/js/src/main/scala/works/iterative/ui/components/tailwind/laminar/LaminarExtensions.scala index 3899808..a5093b3 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 @@ -3,6 +3,7 @@ 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 @@ -17,3 +18,8 @@ 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 + inline def apply(msg: UserMessage) = ctx.messages(msg) diff --git a/ui/js/src/main/scala/works/iterative/ui/services/ConsoleNotificationService.scala b/ui/js/src/main/scala/works/iterative/ui/services/ConsoleNotificationService.scala new file mode 100644 index 0000000..25b57d8 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/services/ConsoleNotificationService.scala @@ -0,0 +1,15 @@ +package works.iterative.ui.services + +import zio.* +import works.iterative.core.UserMessage + +class ConsoleNotificationService extends UserNotificationService: + override def notify( + level: UserNotificationService.Level, + msg: UserMessage + ): UIO[Unit] = + ZIO.succeed(org.scalajs.dom.console.log(s"[$level] $msg")) + +object ConsoleNotificationService: + val layer: ULayer[UserNotificationService] = + ZLayer.succeed(ConsoleNotificationService()) diff --git a/ui/shared/src/main/scala/works/iterative/ui/ZIOEffectHandler.scala b/ui/shared/src/main/scala/works/iterative/ui/ZIOEffectHandler.scala index 875c7a8..a3ea176 100644 --- a/ui/shared/src/main/scala/works/iterative/ui/ZIOEffectHandler.scala +++ b/ui/shared/src/main/scala/works/iterative/ui/ZIOEffectHandler.scala @@ -1,6 +1,22 @@ package works.iterative.ui +import zio.* import zio.stream.* trait ZIOEffectHandler[Env, Effect, Action]: def handle(e: Effect): ZStream[Env, Throwable, Action] + + def fromZIO( + zio: ZIO[Env, Throwable, Action] + ): ZStream[Env, Throwable, Action] = + ZStream.fromZIO(zio) + + def fromZIOOption( + zio: ZIO[Env, Throwable, Option[Action]] + ): ZStream[Env, Throwable, Action] = + ZStream.fromZIO(zio).collect { case Some(a) => a } + + def fromZIOUnit( + zio: ZIO[Env, Throwable, Unit] + ): ZStream[Env, Throwable, Action] = + ZStream.fromZIO(zio.as(Option.empty[Action])).collect { case Some(a) => a } diff --git a/ui/shared/src/main/scala/works/iterative/ui/model/Computable.scala b/ui/shared/src/main/scala/works/iterative/ui/model/Computable.scala index 29287ba..fa537d0 100644 --- a/ui/shared/src/main/scala/works/iterative/ui/model/Computable.scala +++ b/ui/shared/src/main/scala/works/iterative/ui/model/Computable.scala @@ -1,12 +1,48 @@ package works.iterative.ui.model import works.iterative.core.UserMessage +import java.time.Instant /** A class representing the states of a model that needs computation */ -// TODO: move to core when stable -enum Computable[Model]: - case Uninitialized extends Computable[Nothing] - case Computing extends Computable[Nothing] - case Ready(model: Model) extends Computable[Model] - case Failed(error: UserMessage) extends Computable[Nothing] +sealed trait Computable[+Model]: + /** Update the computation state with new data + */ + def update[B >: Model](m: B): Computable[B] + + /** Mark the computation as started + */ + def started: Computable[Model] + +object Computable: + /** The initial state of a computable model + */ + case object Uninitialized extends Computable[Nothing]: + override def update[B](m: B): Computable[B] = Ready(m) + override def started: Computable[Nothing] = Computing(Instant.now()) + + /** The computation is in progress + */ + case class Computing(start: Instant) extends Computable[Nothing]: + override def update[B](m: B): Computable[B] = Ready(m) + override def started: Computable[Nothing] = this + + /** The computation is finished and the data is available + */ + case class Ready[Model](model: Model) extends Computable[Model]: + override def update[B >: Model](m: B): Computable[B] = Ready(m) + override def started: Computable[Model] = Recomputing(Instant.now(), model) + + /** The computation is finished and the data is available, but it is being + * recomputed + */ + case class Recomputing[Model](start: Instant, model: Model) + extends Computable[Model]: + override def update[B >: Model](m: B): Computable[B] = Ready(m) + override def started: Computable[Model] = this + + /** The computation failed + */ + case class Failed(error: UserMessage) extends Computable[Nothing]: + override def update[B](m: B): Computable[B] = Ready(m) + override def started: Computable[Nothing] = Computing(Instant.now()) diff --git a/ui/shared/src/main/scala/works/iterative/ui/services/UserNotificationService.scala b/ui/shared/src/main/scala/works/iterative/ui/services/UserNotificationService.scala new file mode 100644 index 0000000..9a6cc94 --- /dev/null +++ b/ui/shared/src/main/scala/works/iterative/ui/services/UserNotificationService.scala @@ -0,0 +1,45 @@ +package works.iterative.ui.services + +import works.iterative.core.UserMessage + +import zio.* + +/** A way for any module to notify the user about a success or failure + */ +trait UserNotificationService: + def notify(level: UserNotificationService.Level, msg: UserMessage): UIO[Unit] + def info(msg: UserMessage): UIO[Unit] = + notify(UserNotificationService.Level.Info, msg) + def warning(msg: UserMessage): UIO[Unit] = + notify(UserNotificationService.Level.Warning, msg) + def error(msg: UserMessage): UIO[Unit] = + notify(UserNotificationService.Level.Error, msg) + def debug(msg: UserMessage): UIO[Unit] = + notify(UserNotificationService.Level.Debug, msg) + def success(msg: UserMessage): UIO[Unit] = + notify(UserNotificationService.Level.Success, msg) + +object UserNotificationService: + enum Level: + case Info, Warning, Error, Debug, Success + + def notify( + level: Level, + msg: UserMessage + ): URIO[UserNotificationService, Unit] = + ZIO.serviceWithZIO(_.notify(level, msg)) + + def info(msg: UserMessage): URIO[UserNotificationService, Unit] = + ZIO.serviceWithZIO(_.info(msg)) + + def warning(msg: UserMessage): URIO[UserNotificationService, Unit] = + ZIO.serviceWithZIO(_.warning(msg)) + + def error(msg: UserMessage): URIO[UserNotificationService, Unit] = + ZIO.serviceWithZIO(_.error(msg)) + + def debug(msg: UserMessage): URIO[UserNotificationService, Unit] = + ZIO.serviceWithZIO(_.debug(msg)) + + def success(msg: UserMessage): URIO[UserNotificationService, Unit] = + ZIO.serviceWithZIO(_.success(msg))