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 db5e08f..3aac0bf 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 @@ -10,7 +10,8 @@ def primaryButton( id: String, text: Modifier[HtmlElement], - icon: Option[SvgElement] = None + icon: Option[SvgElement] = None, + buttonType: String = "submit" )( mods: Modifier[HtmlElement]* ): HtmlElement @@ -18,7 +19,8 @@ def secondaryButton( id: String, text: Modifier[HtmlElement], - icon: Option[SvgElement] = None + icon: Option[SvgElement] = None, + buttonType: String = "button" )( mods: Modifier[HtmlElement]* ): HtmlElement @@ -30,61 +32,3 @@ def iconButton(id: String, icon: SvgElement)( mods: Modifier[HtmlElement]* ): HtmlElement - -trait DefaultButtonComponentsModule(using ctx: ComponentContext) - extends ButtonComponentsModule: - - override val buttons = new ButtonComponents: - - private inline def srHelp(id: String): Modifier[HtmlElement] = - ctx.messages - .opt(s"form.button.${id}.screenReaderHelp") - .map(sr => span(cls := "sr-only", sr)) - - override def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None - )(mods: Modifier[HtmlElement]*): HtmlElement = - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - icon, - text, - mods - ) - - override def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None - )(mods: Modifier[HtmlElement]*): HtmlElement = - button( - tpe("button"), - cls( - "ml-2 bg-white inline-flex justify-center py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" - ), - icon, - text, - mods - ) - - override def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement = - button( - tpe := "button", - cls := "ml-1 inline-flex h-4 w-4 flex-shrink-0 rounded-full p-1 text-gray-400 hover:bg-gray-200 hover:text-gray-500", - srHelp(id), - icon - ) - - override def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement = - button( - tpe := "button", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - icon, - srHelp(id) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala index db5e08f..3aac0bf 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 @@ -10,7 +10,8 @@ def primaryButton( id: String, text: Modifier[HtmlElement], - icon: Option[SvgElement] = None + icon: Option[SvgElement] = None, + buttonType: String = "submit" )( mods: Modifier[HtmlElement]* ): HtmlElement @@ -18,7 +19,8 @@ def secondaryButton( id: String, text: Modifier[HtmlElement], - icon: Option[SvgElement] = None + icon: Option[SvgElement] = None, + buttonType: String = "button" )( mods: Modifier[HtmlElement]* ): HtmlElement @@ -30,61 +32,3 @@ def iconButton(id: String, icon: SvgElement)( mods: Modifier[HtmlElement]* ): HtmlElement - -trait DefaultButtonComponentsModule(using ctx: ComponentContext) - extends ButtonComponentsModule: - - override val buttons = new ButtonComponents: - - private inline def srHelp(id: String): Modifier[HtmlElement] = - ctx.messages - .opt(s"form.button.${id}.screenReaderHelp") - .map(sr => span(cls := "sr-only", sr)) - - override def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None - )(mods: Modifier[HtmlElement]*): HtmlElement = - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - icon, - text, - mods - ) - - override def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None - )(mods: Modifier[HtmlElement]*): HtmlElement = - button( - tpe("button"), - cls( - "ml-2 bg-white inline-flex justify-center py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" - ), - icon, - text, - mods - ) - - override def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement = - button( - tpe := "button", - cls := "ml-1 inline-flex h-4 w-4 flex-shrink-0 rounded-full p-1 text-gray-400 hover:bg-gray-200 hover:text-gray-500", - srHelp(id), - icon - ) - - override def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement = - button( - tpe := "button", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - icon, - srHelp(id) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala index 66094fd..c97ca24 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala @@ -10,10 +10,31 @@ import org.scalajs.dom.html import com.raquo.laminar.modifiers.KeyUpdater +/** Form components + * + * A form is a collection of sections, each of which contains a collection of + * fields. + * + * Form -> * Section -> * Field + * -> * Action + * + * Each Field has an `id`, `label` and `input` element. The `id` is used to + * link the label to the input. + * + * The `input` element can be a simple text input, or a more complex component + * such as a date picker. The caller is expected to make the input work. + */ trait FormComponentsModule extends LocalDateSelectModule: def forms: FormComponents trait FormComponents: + + /** Layout the sections and actions of a form */ + def form(mods: Modifier[HtmlElement]*)(sections: Modifier[HtmlElement]*)( + actions: Modifier[HtmlElement]* + ): HtmlElement + + /** Section layout, with a title, optional subtitle and any content. */ def section( title: Modifier[HtmlElement], subtitle: Option[Modifier[HtmlElement]] @@ -21,10 +42,12 @@ content: Modifier[HtmlElement]* ): HtmlElement + /** Layout a field with label and any content */ def field(label: Modifier[HtmlElement])( content: Modifier[HtmlElement]* ): HtmlElement + /** Layout a field with label, input and optional help text */ def field( id: String, label: String, @@ -32,142 +55,30 @@ help: Option[String] ): HtmlElement - def form(mods: Modifier[HtmlElement]*)(sections: Modifier[HtmlElement]*)( - actions: Modifier[HtmlElement]* + def label(labelText: String, forId: Option[String] = None)( + mods: Modifier[HtmlElement]* ): HtmlElement + /** Layout an inline form element */ def inlineForm(mods: Modifier[HtmlElement]*): HtmlElement + /** An input field with a search icon */ def searchField(id: String, placeholderText: Option[String] = None)( mods: Modifier[HtmlElement]* ): HtmlElement + def inputField( + id: String, + labelText: String, + placeholderText: Option[String] = None, + inputType: String = "text", + helpText: Option[String] = None + ): HtmlElement + + /** LocalDate input */ def renderLocalDateSelect( id: String, labelText: Option[String], placeholderText: Option[String], mods: LocalDateSelect => Modifier[HtmlElement] ): HtmlElement - -trait DefaultFormComponentsModule(using ctx: ComponentContext) - extends FormComponentsModule: - self: IconsModule => - override val forms = new FormComponents: - - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )(content: Modifier[HtmlElement]*): HtmlElement = - div( - cls("space-y-6 sm:space-y-5"), - div( - h3(cls("text-lg leading-6 font-medium text-gray-900"), title), - subtitle.map(st => p(cls("mt-1 max-w-2xl text-sm text-gray-500"), st)) - ), - div(cls("mt-6 sm:mt-5 space-y-6 sm:space-y-5"), content) - ) - - override def field( - label: Modifier[HtmlElement] - )(content: Modifier[HtmlElement]*): HtmlElement = - div( - cls( - "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5" - ), - label, - div(cls("mt-1 sm:mt-0 sm:col-span-2"), content) - ) - - override def field( - id: String, - label: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement = - field( - L.label( - cls("block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"), - forId(id), - label - ) - )( - input.amend(idAttr(id)), - help.map(h => p(cls("mt-2 text-sm text-gray-500"), h)) - ) - - override def form(mods: Modifier[HtmlElement]*)( - sections: Modifier[HtmlElement]* - )(actions: Modifier[HtmlElement]*): HtmlElement = - L.form( - cls("space-y-8 divide-y divide-gray-200"), - mods, - sections, - div( - cls("pt-5"), - div(cls("flex justify-end"), actions) - ) - ) - - override def inlineForm( - mods: Modifier[HtmlElement]* - ): HtmlElement = - L.form(cls("flex space-x-4"), mods) - - override def searchField( - id: String, - placeholderText: Option[String] = None - )( - mods: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "flex-1 min-w-0", - label( - forId := id, - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - icons.`search-solid`() - ), - input( - tpe := "search", - name := "search", - idAttr := id, - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholderText - .orElse( - ctx.messages - .opt( - s"forms.search.${id}.placeholder", - s"form.search.placeholder" - ) - ) - .map(placeholder(_)), - mods - ) - ) - ) - - override def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement = - div( - labelText, - input( - idAttr(id), - name(id), - autoComplete("date"), - tpe("date"), - cls( - "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ), - placeholderText.map(placeholder(_)), - mods(localDateSelect) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala index db5e08f..3aac0bf 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 @@ -10,7 +10,8 @@ def primaryButton( id: String, text: Modifier[HtmlElement], - icon: Option[SvgElement] = None + icon: Option[SvgElement] = None, + buttonType: String = "submit" )( mods: Modifier[HtmlElement]* ): HtmlElement @@ -18,7 +19,8 @@ def secondaryButton( id: String, text: Modifier[HtmlElement], - icon: Option[SvgElement] = None + icon: Option[SvgElement] = None, + buttonType: String = "button" )( mods: Modifier[HtmlElement]* ): HtmlElement @@ -30,61 +32,3 @@ def iconButton(id: String, icon: SvgElement)( mods: Modifier[HtmlElement]* ): HtmlElement - -trait DefaultButtonComponentsModule(using ctx: ComponentContext) - extends ButtonComponentsModule: - - override val buttons = new ButtonComponents: - - private inline def srHelp(id: String): Modifier[HtmlElement] = - ctx.messages - .opt(s"form.button.${id}.screenReaderHelp") - .map(sr => span(cls := "sr-only", sr)) - - override def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None - )(mods: Modifier[HtmlElement]*): HtmlElement = - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - icon, - text, - mods - ) - - override def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None - )(mods: Modifier[HtmlElement]*): HtmlElement = - button( - tpe("button"), - cls( - "ml-2 bg-white inline-flex justify-center py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" - ), - icon, - text, - mods - ) - - override def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement = - button( - tpe := "button", - cls := "ml-1 inline-flex h-4 w-4 flex-shrink-0 rounded-full p-1 text-gray-400 hover:bg-gray-200 hover:text-gray-500", - srHelp(id), - icon - ) - - override def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement = - button( - tpe := "button", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - icon, - srHelp(id) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala index 66094fd..c97ca24 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala @@ -10,10 +10,31 @@ import org.scalajs.dom.html import com.raquo.laminar.modifiers.KeyUpdater +/** Form components + * + * A form is a collection of sections, each of which contains a collection of + * fields. + * + * Form -> * Section -> * Field + * -> * Action + * + * Each Field has an `id`, `label` and `input` element. The `id` is used to + * link the label to the input. + * + * The `input` element can be a simple text input, or a more complex component + * such as a date picker. The caller is expected to make the input work. + */ trait FormComponentsModule extends LocalDateSelectModule: def forms: FormComponents trait FormComponents: + + /** Layout the sections and actions of a form */ + def form(mods: Modifier[HtmlElement]*)(sections: Modifier[HtmlElement]*)( + actions: Modifier[HtmlElement]* + ): HtmlElement + + /** Section layout, with a title, optional subtitle and any content. */ def section( title: Modifier[HtmlElement], subtitle: Option[Modifier[HtmlElement]] @@ -21,10 +42,12 @@ content: Modifier[HtmlElement]* ): HtmlElement + /** Layout a field with label and any content */ def field(label: Modifier[HtmlElement])( content: Modifier[HtmlElement]* ): HtmlElement + /** Layout a field with label, input and optional help text */ def field( id: String, label: String, @@ -32,142 +55,30 @@ help: Option[String] ): HtmlElement - def form(mods: Modifier[HtmlElement]*)(sections: Modifier[HtmlElement]*)( - actions: Modifier[HtmlElement]* + def label(labelText: String, forId: Option[String] = None)( + mods: Modifier[HtmlElement]* ): HtmlElement + /** Layout an inline form element */ def inlineForm(mods: Modifier[HtmlElement]*): HtmlElement + /** An input field with a search icon */ def searchField(id: String, placeholderText: Option[String] = None)( mods: Modifier[HtmlElement]* ): HtmlElement + def inputField( + id: String, + labelText: String, + placeholderText: Option[String] = None, + inputType: String = "text", + helpText: Option[String] = None + ): HtmlElement + + /** LocalDate input */ def renderLocalDateSelect( id: String, labelText: Option[String], placeholderText: Option[String], mods: LocalDateSelect => Modifier[HtmlElement] ): HtmlElement - -trait DefaultFormComponentsModule(using ctx: ComponentContext) - extends FormComponentsModule: - self: IconsModule => - override val forms = new FormComponents: - - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )(content: Modifier[HtmlElement]*): HtmlElement = - div( - cls("space-y-6 sm:space-y-5"), - div( - h3(cls("text-lg leading-6 font-medium text-gray-900"), title), - subtitle.map(st => p(cls("mt-1 max-w-2xl text-sm text-gray-500"), st)) - ), - div(cls("mt-6 sm:mt-5 space-y-6 sm:space-y-5"), content) - ) - - override def field( - label: Modifier[HtmlElement] - )(content: Modifier[HtmlElement]*): HtmlElement = - div( - cls( - "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5" - ), - label, - div(cls("mt-1 sm:mt-0 sm:col-span-2"), content) - ) - - override def field( - id: String, - label: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement = - field( - L.label( - cls("block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"), - forId(id), - label - ) - )( - input.amend(idAttr(id)), - help.map(h => p(cls("mt-2 text-sm text-gray-500"), h)) - ) - - override def form(mods: Modifier[HtmlElement]*)( - sections: Modifier[HtmlElement]* - )(actions: Modifier[HtmlElement]*): HtmlElement = - L.form( - cls("space-y-8 divide-y divide-gray-200"), - mods, - sections, - div( - cls("pt-5"), - div(cls("flex justify-end"), actions) - ) - ) - - override def inlineForm( - mods: Modifier[HtmlElement]* - ): HtmlElement = - L.form(cls("flex space-x-4"), mods) - - override def searchField( - id: String, - placeholderText: Option[String] = None - )( - mods: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "flex-1 min-w-0", - label( - forId := id, - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - icons.`search-solid`() - ), - input( - tpe := "search", - name := "search", - idAttr := id, - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholderText - .orElse( - ctx.messages - .opt( - s"forms.search.${id}.placeholder", - s"form.search.placeholder" - ) - ) - .map(placeholder(_)), - mods - ) - ) - ) - - override def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement = - div( - labelText, - input( - idAttr(id), - name(id), - autoComplete("date"), - tpe("date"), - cls( - "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ), - placeholderText.map(placeholder(_)), - mods(localDateSelect) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/LabelsOnLeftFormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/LabelsOnLeftFormComponentsModule.scala new file mode 100644 index 0000000..318d3d3 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/LabelsOnLeftFormComponentsModule.scala @@ -0,0 +1,157 @@ +package works.iterative.ui.components.laminar +package tailwindui + +import com.raquo.laminar.api.L +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import com.raquo.laminar.keys.ReactiveProp +import com.raquo.domtypes.jsdom.defs.events.TypedTargetEvent +import org.scalajs.dom.html +import com.raquo.laminar.modifiers.KeyUpdater + +trait LabelsOnLeftFormComponentsModule(using ctx: ComponentContext) + extends FormComponentsModule: + self: IconsModule => + override val forms = new FormComponents: + + override def section( + title: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] + )(content: Modifier[HtmlElement]*): HtmlElement = + div( + cls("space-y-6 sm:space-y-5"), + div( + h3(cls("text-lg leading-6 font-medium text-gray-900"), title), + subtitle.map(st => p(cls("mt-1 max-w-2xl text-sm text-gray-500"), st)) + ), + div(cls("mt-6 sm:mt-5 space-y-6 sm:space-y-5"), content) + ) + + override def label(labelText: String, forId: Option[String] = None)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + L.label( + cls("block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"), + forId.map(id => L.forId(id)), + labelText, + mods + ) + + override def field( + label: Modifier[HtmlElement] + )(content: Modifier[HtmlElement]*): HtmlElement = + div( + cls( + "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5" + ), + label, + div(cls("mt-1 sm:mt-0 sm:col-span-2"), content) + ) + + override def field( + id: String, + labelText: String, + input: HtmlElement, + help: Option[String] + ): HtmlElement = + field( + label(labelText, Some(id))() + )( + input.amend(idAttr(id)), + help.map(h => p(cls("mt-2 text-sm text-gray-500"), h)) + ) + + override def form(mods: Modifier[HtmlElement]*)( + sections: Modifier[HtmlElement]* + )(actions: Modifier[HtmlElement]*): HtmlElement = + L.form( + cls("space-y-8 divide-y divide-gray-200"), + mods, + sections, + div( + cls("pt-5"), + div(cls("flex justify-end"), actions) + ) + ) + + override def inlineForm( + mods: Modifier[HtmlElement]* + ): HtmlElement = + L.form(cls("flex space-x-4"), mods) + + override def searchField( + id: String, + placeholderText: Option[String] = None + )( + mods: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls := "flex-1 min-w-0", + label(ctx.messages("forms.search.label"), Some(id))(cls("sr-only")), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + icons.`search-solid`() + ), + input( + tpe := "search", + name := "search", + idAttr := id, + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholderText + .orElse( + ctx.messages + .opt( + s"forms.search.${id}.placeholder", + s"form.search.placeholder" + ) + ) + .map(placeholder(_)), + mods + ) + ) + ) + + override def inputField( + id: String, + labelText: String, + placeholderText: Option[String] = None, + inputType: String = "text", + helpText: Option[String] = None + ): HtmlElement = + field( + id, + labelText, + input( + tpe(inputType), + cls( + "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:max-w-xs sm:text-sm sm:leading-6" + ), + placeholderText.map(placeholder(_)) + ), + helpText + ) + + override def renderLocalDateSelect( + id: String, + labelText: Option[String], + placeholderText: Option[String], + mods: LocalDateSelect => Modifier[HtmlElement] + ): HtmlElement = + div( + labelText, + input( + idAttr(id), + name(id), + autoComplete("date"), + tpe("date"), + cls( + "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ), + placeholderText.map(placeholder(_)), + mods(localDateSelect) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/ButtonComponentsModule.scala index db5e08f..3aac0bf 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 @@ -10,7 +10,8 @@ def primaryButton( id: String, text: Modifier[HtmlElement], - icon: Option[SvgElement] = None + icon: Option[SvgElement] = None, + buttonType: String = "submit" )( mods: Modifier[HtmlElement]* ): HtmlElement @@ -18,7 +19,8 @@ def secondaryButton( id: String, text: Modifier[HtmlElement], - icon: Option[SvgElement] = None + icon: Option[SvgElement] = None, + buttonType: String = "button" )( mods: Modifier[HtmlElement]* ): HtmlElement @@ -30,61 +32,3 @@ def iconButton(id: String, icon: SvgElement)( mods: Modifier[HtmlElement]* ): HtmlElement - -trait DefaultButtonComponentsModule(using ctx: ComponentContext) - extends ButtonComponentsModule: - - override val buttons = new ButtonComponents: - - private inline def srHelp(id: String): Modifier[HtmlElement] = - ctx.messages - .opt(s"form.button.${id}.screenReaderHelp") - .map(sr => span(cls := "sr-only", sr)) - - override def primaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None - )(mods: Modifier[HtmlElement]*): HtmlElement = - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - icon, - text, - mods - ) - - override def secondaryButton( - id: String, - text: Modifier[HtmlElement], - icon: Option[SvgElement] = None - )(mods: Modifier[HtmlElement]*): HtmlElement = - button( - tpe("button"), - cls( - "ml-2 bg-white inline-flex justify-center py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" - ), - icon, - text, - mods - ) - - override def inlineButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement = - button( - tpe := "button", - cls := "ml-1 inline-flex h-4 w-4 flex-shrink-0 rounded-full p-1 text-gray-400 hover:bg-gray-200 hover:text-gray-500", - srHelp(id), - icon - ) - - override def iconButton(id: String, icon: SvgElement)( - mods: Modifier[HtmlElement]* - ): HtmlElement = - button( - tpe := "button", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - icon, - srHelp(id) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala index 66094fd..c97ca24 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/FormComponentsModule.scala @@ -10,10 +10,31 @@ import org.scalajs.dom.html import com.raquo.laminar.modifiers.KeyUpdater +/** Form components + * + * A form is a collection of sections, each of which contains a collection of + * fields. + * + * Form -> * Section -> * Field + * -> * Action + * + * Each Field has an `id`, `label` and `input` element. The `id` is used to + * link the label to the input. + * + * The `input` element can be a simple text input, or a more complex component + * such as a date picker. The caller is expected to make the input work. + */ trait FormComponentsModule extends LocalDateSelectModule: def forms: FormComponents trait FormComponents: + + /** Layout the sections and actions of a form */ + def form(mods: Modifier[HtmlElement]*)(sections: Modifier[HtmlElement]*)( + actions: Modifier[HtmlElement]* + ): HtmlElement + + /** Section layout, with a title, optional subtitle and any content. */ def section( title: Modifier[HtmlElement], subtitle: Option[Modifier[HtmlElement]] @@ -21,10 +42,12 @@ content: Modifier[HtmlElement]* ): HtmlElement + /** Layout a field with label and any content */ def field(label: Modifier[HtmlElement])( content: Modifier[HtmlElement]* ): HtmlElement + /** Layout a field with label, input and optional help text */ def field( id: String, label: String, @@ -32,142 +55,30 @@ help: Option[String] ): HtmlElement - def form(mods: Modifier[HtmlElement]*)(sections: Modifier[HtmlElement]*)( - actions: Modifier[HtmlElement]* + def label(labelText: String, forId: Option[String] = None)( + mods: Modifier[HtmlElement]* ): HtmlElement + /** Layout an inline form element */ def inlineForm(mods: Modifier[HtmlElement]*): HtmlElement + /** An input field with a search icon */ def searchField(id: String, placeholderText: Option[String] = None)( mods: Modifier[HtmlElement]* ): HtmlElement + def inputField( + id: String, + labelText: String, + placeholderText: Option[String] = None, + inputType: String = "text", + helpText: Option[String] = None + ): HtmlElement + + /** LocalDate input */ def renderLocalDateSelect( id: String, labelText: Option[String], placeholderText: Option[String], mods: LocalDateSelect => Modifier[HtmlElement] ): HtmlElement - -trait DefaultFormComponentsModule(using ctx: ComponentContext) - extends FormComponentsModule: - self: IconsModule => - override val forms = new FormComponents: - - override def section( - title: Modifier[HtmlElement], - subtitle: Option[Modifier[HtmlElement]] - )(content: Modifier[HtmlElement]*): HtmlElement = - div( - cls("space-y-6 sm:space-y-5"), - div( - h3(cls("text-lg leading-6 font-medium text-gray-900"), title), - subtitle.map(st => p(cls("mt-1 max-w-2xl text-sm text-gray-500"), st)) - ), - div(cls("mt-6 sm:mt-5 space-y-6 sm:space-y-5"), content) - ) - - override def field( - label: Modifier[HtmlElement] - )(content: Modifier[HtmlElement]*): HtmlElement = - div( - cls( - "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5" - ), - label, - div(cls("mt-1 sm:mt-0 sm:col-span-2"), content) - ) - - override def field( - id: String, - label: String, - input: HtmlElement, - help: Option[String] - ): HtmlElement = - field( - L.label( - cls("block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"), - forId(id), - label - ) - )( - input.amend(idAttr(id)), - help.map(h => p(cls("mt-2 text-sm text-gray-500"), h)) - ) - - override def form(mods: Modifier[HtmlElement]*)( - sections: Modifier[HtmlElement]* - )(actions: Modifier[HtmlElement]*): HtmlElement = - L.form( - cls("space-y-8 divide-y divide-gray-200"), - mods, - sections, - div( - cls("pt-5"), - div(cls("flex justify-end"), actions) - ) - ) - - override def inlineForm( - mods: Modifier[HtmlElement]* - ): HtmlElement = - L.form(cls("flex space-x-4"), mods) - - override def searchField( - id: String, - placeholderText: Option[String] = None - )( - mods: Modifier[HtmlElement]* - ): HtmlElement = - div( - cls := "flex-1 min-w-0", - label( - forId := id, - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - icons.`search-solid`() - ), - input( - tpe := "search", - name := "search", - idAttr := id, - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholderText - .orElse( - ctx.messages - .opt( - s"forms.search.${id}.placeholder", - s"form.search.placeholder" - ) - ) - .map(placeholder(_)), - mods - ) - ) - ) - - override def renderLocalDateSelect( - id: String, - labelText: Option[String], - placeholderText: Option[String], - mods: LocalDateSelect => Modifier[HtmlElement] - ): HtmlElement = - div( - labelText, - input( - idAttr(id), - name(id), - autoComplete("date"), - tpe("date"), - cls( - "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ), - placeholderText.map(placeholder(_)), - mods(localDateSelect) - ) - ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/LabelsOnLeftFormComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/LabelsOnLeftFormComponentsModule.scala new file mode 100644 index 0000000..318d3d3 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/LabelsOnLeftFormComponentsModule.scala @@ -0,0 +1,157 @@ +package works.iterative.ui.components.laminar +package tailwindui + +import com.raquo.laminar.api.L +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import com.raquo.laminar.keys.ReactiveProp +import com.raquo.domtypes.jsdom.defs.events.TypedTargetEvent +import org.scalajs.dom.html +import com.raquo.laminar.modifiers.KeyUpdater + +trait LabelsOnLeftFormComponentsModule(using ctx: ComponentContext) + extends FormComponentsModule: + self: IconsModule => + override val forms = new FormComponents: + + override def section( + title: Modifier[HtmlElement], + subtitle: Option[Modifier[HtmlElement]] + )(content: Modifier[HtmlElement]*): HtmlElement = + div( + cls("space-y-6 sm:space-y-5"), + div( + h3(cls("text-lg leading-6 font-medium text-gray-900"), title), + subtitle.map(st => p(cls("mt-1 max-w-2xl text-sm text-gray-500"), st)) + ), + div(cls("mt-6 sm:mt-5 space-y-6 sm:space-y-5"), content) + ) + + override def label(labelText: String, forId: Option[String] = None)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + L.label( + cls("block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"), + forId.map(id => L.forId(id)), + labelText, + mods + ) + + override def field( + label: Modifier[HtmlElement] + )(content: Modifier[HtmlElement]*): HtmlElement = + div( + cls( + "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5" + ), + label, + div(cls("mt-1 sm:mt-0 sm:col-span-2"), content) + ) + + override def field( + id: String, + labelText: String, + input: HtmlElement, + help: Option[String] + ): HtmlElement = + field( + label(labelText, Some(id))() + )( + input.amend(idAttr(id)), + help.map(h => p(cls("mt-2 text-sm text-gray-500"), h)) + ) + + override def form(mods: Modifier[HtmlElement]*)( + sections: Modifier[HtmlElement]* + )(actions: Modifier[HtmlElement]*): HtmlElement = + L.form( + cls("space-y-8 divide-y divide-gray-200"), + mods, + sections, + div( + cls("pt-5"), + div(cls("flex justify-end"), actions) + ) + ) + + override def inlineForm( + mods: Modifier[HtmlElement]* + ): HtmlElement = + L.form(cls("flex space-x-4"), mods) + + override def searchField( + id: String, + placeholderText: Option[String] = None + )( + mods: Modifier[HtmlElement]* + ): HtmlElement = + div( + cls := "flex-1 min-w-0", + label(ctx.messages("forms.search.label"), Some(id))(cls("sr-only")), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + icons.`search-solid`() + ), + input( + tpe := "search", + name := "search", + idAttr := id, + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholderText + .orElse( + ctx.messages + .opt( + s"forms.search.${id}.placeholder", + s"form.search.placeholder" + ) + ) + .map(placeholder(_)), + mods + ) + ) + ) + + override def inputField( + id: String, + labelText: String, + placeholderText: Option[String] = None, + inputType: String = "text", + helpText: Option[String] = None + ): HtmlElement = + field( + id, + labelText, + input( + tpe(inputType), + cls( + "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:max-w-xs sm:text-sm sm:leading-6" + ), + placeholderText.map(placeholder(_)) + ), + helpText + ) + + override def renderLocalDateSelect( + id: String, + labelText: Option[String], + placeholderText: Option[String], + mods: LocalDateSelect => Modifier[HtmlElement] + ): HtmlElement = + div( + labelText, + input( + idAttr(id), + name(id), + autoComplete("date"), + tpe("date"), + cls( + "ml-2 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ), + placeholderText.map(placeholder(_)), + mods(localDateSelect) + ) + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/TailwindUIButtonComponentsModule.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/TailwindUIButtonComponentsModule.scala new file mode 100644 index 0000000..4c44157 --- /dev/null +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/tailwindui/TailwindUIButtonComponentsModule.scala @@ -0,0 +1,65 @@ +package works.iterative.ui.components.laminar +package tailwindui + +import com.raquo.laminar.api.L.{*, given} +import works.iterative.ui.components.tailwind.ComponentContext + +trait TailwindUIButtonComponentsModule(using ctx: ComponentContext) + extends ButtonComponentsModule: + + override val buttons = new ButtonComponents: + + private inline def srHelp(id: String): Modifier[HtmlElement] = + ctx.messages + .opt(s"form.button.${id}.screenReaderHelp") + .map(sr => span(cls := "sr-only", sr)) + + override def primaryButton( + id: String, + text: Modifier[HtmlElement], + icon: Option[SvgElement] = None, + buttonType: String = "submit" + )(mods: Modifier[HtmlElement]*): HtmlElement = + button( + tpe(buttonType), + cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + icon, + text, + mods + ) + + override def secondaryButton( + id: String, + text: Modifier[HtmlElement], + icon: Option[SvgElement] = None, + buttonType: String = "button" + )(mods: Modifier[HtmlElement]*): HtmlElement = + button( + tpe(buttonType), + cls( + "ml-2 bg-white inline-flex justify-center py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" + ), + icon, + text, + mods + ) + + override def inlineButton(id: String, icon: SvgElement)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + button( + tpe := "button", + cls := "ml-1 inline-flex h-4 w-4 flex-shrink-0 rounded-full p-1 text-gray-400 hover:bg-gray-200 hover:text-gray-500", + srHelp(id), + icon + ) + + override def iconButton(id: String, icon: SvgElement)( + mods: Modifier[HtmlElement]* + ): HtmlElement = + button( + tpe := "button", + cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", + icon, + srHelp(id) + )