Current section
Files
Jump to
Current section
Files
src/ui/search_field.gleam
// MIT License
// Copyright (c) 2026 Koncreate
// See LICENSE for details
import gleam/option.{type Option, None, Some}
import gleam/string
import lustre/attribute.{type Attribute, attribute, class, name, type_}
import lustre/element.{type Element}
import lustre/element/html
import ui/css
import ui/keyboard.{
type Key, ArrowDown, ArrowUp, End, Enter, Escape, Home, decode_key,
}
pub type Variant {
Default
Muted
}
pub type Size {
Small
Medium
Large
}
/// Messages for search field keyboard navigation
pub type Msg {
Increment
Decrement
SetMin
SetMax
Clear
}
pub fn search_field(attributes: List(Attribute(a))) -> Element(a) {
html.div([class("relative w-full")], [
html.div(
[
class(
"absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground pointer-events-none",
),
],
[
html.div(
[
class(
[
"h-4 w-4",
"border-2 border-current border-r-0 rounded-l",
"translate-x-0.5",
]
|> string.join(" "),
),
],
[],
),
],
),
html.input([
class(
[
"flex h-10 w-full rounded-md border border-input bg-background pl-10 pr-4 py-2.5 text-sm",
"placeholder:text-muted-foreground",
css.focus_ring(),
css.disabled(),
]
|> string.join(" "),
),
name(""),
type_("search"),
attribute("role", "searchbox"),
..attributes
]),
])
}
pub fn variant(v: Variant) -> Attribute(a) {
case v {
Default -> class("border-input bg-background")
Muted -> class("border-input bg-muted")
}
}
pub fn size(s: Size) -> Attribute(a) {
case s {
Small -> class("h-8 pl-8 pr-2 text-xs")
Medium -> class("h-10 pl-10 pr-3 text-sm")
Large -> class("h-11 pl-11 pr-4 text-base")
}
}
pub fn required() -> Attribute(a) {
attribute("aria-required", "true")
}
// ============================================================================
// Keyboard Navigation
// ============================================================================
/// Keymap for search field keyboard navigation
///
/// ## Keyboard interactions:
/// - **ArrowUp**: Increment value (for numeric search)
/// - **ArrowDown**: Decrement value
/// - **Home**: Minimum value
/// - **End**: Maximum value
/// - **Escape**: Clear value
///
/// Follows WAI-ARIA [Searchbox](https://www.w3.org/WAI/ARIA/apg/patterns/searchbox/) pattern.
pub fn keymap(key_event: keyboard.KeyEvent) -> Option(Msg) {
let key = decode_key(key_event.key)
case key {
ArrowUp -> Some(Increment)
ArrowDown -> Some(Decrement)
Home -> Some(SetMin)
End -> Some(SetMax)
Escape -> Some(Clear)
_ -> None
}
}