Howdy

Button group

Button groups: related buttons joined into one control.

componentimport howdy/ui/button_groupSince Howdy 2.0
gleam run -m howdy/ui add button_group

Button group

ui.button_group(button_group.Horizontal, [attribute.aria_label("Pages")], [
  ui.button(Outline, [], [text("Previous")]),
  ui.button(Outline, [], [text("Today")]),
  ui.button(Outline, [], [text("Next")]),
])
Full source of howdy/ui/button_group
//// Button groups: related buttons joined into one control.
////
//// ```gleam
//// button_group.group(button_group.Horizontal, [attribute.aria_label("Pages")], [
////   button.button(Outline, [], [text("Previous")]),
////   button.button(Outline, [], [text("Next")]),
//// ])
//// ```
////
//// The buttons keep their own variants; the group squares off the corners
//// where they meet and lets their borders overlap.

import howdy/ui/style.{class}
import lustre/attribute.{type Attribute}
import lustre/element.{type Element}
import lustre/element/html
import sketch/css.{type Class}

pub type Orientation {
  Horizontal
  Vertical
}

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

/// Every class this module uses, for `howdy/ui/export`.
pub fn classes() -> List(Class) {
  [group_class(Horizontal), group_class(Vertical)]
}

pub fn group_class(orientation: Orientation) -> Class {
  let #(direction, joined_start, joined_end, overlap) = case orientation {
    Horizontal -> #(
      "row",
      ["border-start-start-radius", "border-end-start-radius"],
      ["border-start-end-radius", "border-end-end-radius"],
      "margin-inline-start",
    )
    Vertical -> #(
      "column",
      ["border-start-start-radius", "border-start-end-radius"],
      ["border-end-start-radius", "border-end-end-radius"],
      "margin-block-start",
    )
  }
  let square = fn(corners: List(String)) {
    case corners {
      [a, b] -> [css.property(a, "0"), css.property(b, "0")]
      _ -> []
    }
  }
  css.class([
    css.display("inline-flex"),
    css.flex_direction(direction),
    css.selector(" > :not(:first-child)", [
      css.property(overlap, "-1px"),
      ..square(joined_start)
    ]),
    css.selector(" > :not(:last-child)", square(joined_end)),
    // Borders overlap, so a hovered button draws its highlighted border,
    // and a focused one its ring, above its neighbours.
    css.selector(" > :hover", [css.position("relative"), css.z_index(1)]),
    css.selector(" > :focus-visible", [
      css.position("relative"),
      css.z_index(2),
    ]),
  ])
}