Howdy

Input group

Input groups: a text input with text, icons or buttons inside its border.

componentimport howdy/ui/input_groupSince Howdy 2.0
gleam run -m howdy/ui add input_group

Input group

https://
ui.input_group([], [
  ui.input_group_addon([text("https://")]),
  ui.input_group_input([
    attribute.aria_label("Website"),
    attribute.placeholder("example.com"),
  ]),
  ui.input_group_addon([ui.sized_button(Ghost, Small, [], [text("Check")])]),
])
Full source of howdy/ui/input_group
//// Input groups: a text input with text, icons or buttons inside its
//// border.
////
//// ```gleam
//// input_group.group([], [
////   input_group.addon([text("https://")]),
////   input_group.input([attribute.aria_label("Website"), attribute.placeholder("example.com")]),
////   input_group.addon([button.sized(Ghost, Small, [], [text("Check")])]),
//// ])
//// ```
////
//// The group draws the border and the focus ring, so the whole thing reads
//// as one field. Use `input` for the text field inside it: the plain
//// `howdy/ui/input` brings its own border.

import howdy/ui/style.{class}
import howdy/ui/theme/tokens
import lustre/attribute.{type Attribute}
import lustre/element.{type Element}
import lustre/element/html
import sketch/css.{type Class}
import sketch/css/length.{percent, rem}

pub fn group(
  attributes: List(Attribute(msg)),
  children: List(Element(msg)),
) -> Element(msg) {
  html.div(
    [class(group_class()), attribute.role("group"), ..attributes],
    children,
  )
}

/// The text field inside a group.
pub fn input(attributes: List(Attribute(msg))) -> Element(msg) {
  html.input([class(input_class()), ..attributes])
}

/// A multiline text field inside a group, such as a message composer.
pub fn textarea(
  attributes: List(Attribute(msg)),
  content: String,
) -> Element(msg) {
  html.textarea([class(textarea_class()), ..attributes], content)
}

/// Text, an icon or a button at either end of the field.
pub fn addon(children: List(Element(msg))) -> Element(msg) {
  html.div([class(addon_class())], children)
}

/// A row above the field, across its whole width, such as a label or a
/// toolbar.
pub fn block_start(children: List(Element(msg))) -> Element(msg) {
  html.div([class(block_class()), class(block_start_class())], children)
}

/// A row below the field, across its whole width, such as a character
/// count and a send button.
pub fn block_end(children: List(Element(msg))) -> Element(msg) {
  html.div([class(block_class()), class(block_end_class())], children)
}

/// Every class this module uses, for `howdy/ui/export`.
pub fn classes() -> List(Class) {
  [
    group_class(),
    input_class(),
    textarea_class(),
    addon_class(),
    block_class(),
    block_start_class(),
    block_end_class(),
  ]
}

pub fn group_class() -> Class {
  css.class([
    css.display("flex"),
    css.flex_wrap("wrap"),
    css.align_items("center"),
    css.width(percent(100)),
    css.background(tokens.surface),
    style.bordered(),
    style.radius(tokens.radius_medium),
    css.focus_within([
      style.focus_outline(),
      css.property("outline-offset", "2px"),
    ]),
    css.selector(":has([aria-invalid=\"true\"])", [
      css.property("border-color", tokens.danger),
    ]),
  ])
}

pub fn input_class() -> Class {
  css.class([
    css.property("flex", "1"),
    css.property("min-width", "0"),
    css.padding_(tokens.space_2 <> " " <> tokens.space_3),
    css.border("0"),
    css.background("transparent"),
    css.color(tokens.text),
    css.font_family(tokens.font_body),
    css.font_size(rem(1.0)),
    css.outline("none"),
    css.placeholder([css.color(tokens.text_muted)]),
  ])
}

pub fn textarea_class() -> Class {
  css.class([
    css.property("flex", "1"),
    css.property("min-width", "0"),
    css.property("min-height", "4.5rem"),
    css.padding_(tokens.space_2 <> " " <> tokens.space_3),
    css.border("0"),
    css.background("transparent"),
    css.color(tokens.text),
    css.font_family(tokens.font_body),
    css.font_size(rem(1.0)),
    css.line_height("1.5"),
    css.property("resize", "vertical"),
    css.outline("none"),
    css.placeholder([css.color(tokens.text_muted)]),
  ])
}

pub fn block_class() -> Class {
  css.class([
    css.display("flex"),
    css.flex_wrap("wrap"),
    css.align_items("center"),
    css.gap(rem(0.5)),
    css.property("flex-basis", "100%"),
    css.padding_(tokens.space_2 <> " " <> tokens.space_3),
    css.color(tokens.text_muted),
    css.font_size(rem(0.875)),
  ])
}

pub fn block_start_class() -> Class {
  css.class([
    css.property("order", "-1"),
    css.property("padding-bottom", "0"),
  ])
}

pub fn block_end_class() -> Class {
  css.class([css.property("order", "1"), css.property("padding-top", "0")])
}

pub fn addon_class() -> Class {
  css.class([
    css.display("flex"),
    css.align_items("center"),
    css.gap(rem(0.25)),
    css.padding_("0 " <> tokens.space_2),
    css.color(tokens.text_muted),
    css.font_size(rem(0.875)),
    css.white_space("nowrap"),
  ])
}