Packages

A shadcn/ui-inspired UI library for Gleam and Lustre

Current section

Files

Jump to
glizzy src ui table.gleam
Raw

src/ui/table.gleam

// MIT License
// Copyright (c) 2026 Koncreate
// See LICENSE for details
import gleam/int
import gleam/option.{type Option, Some}
import lustre/attribute.{type Attribute, attribute, class, role}
import lustre/element.{type Element}
import lustre/element/html
import ui/keyboard
import ui/size as size_helpers
pub type Variant {
Default
Bordered
}
pub type Size {
Small
Medium
Large
}
pub type SelectionMode {
None
Single
Multiple
}
pub fn table(
attributes: List(Attribute(a)),
children: List(Element(a)),
) -> Element(a) {
html.table(
[class("w-full caption-bottom text-sm"), role("grid"), ..attributes],
children,
)
}
pub fn table_header(
attributes: List(Attribute(a)),
children: List(Element(a)),
) -> Element(a) {
html.thead([class("border-b bg-muted/50"), ..attributes], children)
}
pub fn table_header_row(
attributes: List(Attribute(a)),
children: List(Element(a)),
) -> Element(a) {
html.tr(
[
role("row"),
class(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
),
..attributes
],
children,
)
}
pub fn table_column_header(
attributes: List(Attribute(a)),
children: List(Element(a)),
) -> Element(a) {
html.th(
[
class(
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
),
role("columnheader"),
attribute("aria-sort", "none"),
..attributes
],
children,
)
}
pub fn table_body(
attributes: List(Attribute(a)),
children: List(Element(a)),
) -> Element(a) {
html.tbody([class(""), ..attributes], children)
}
pub fn table_row(
attributes: List(Attribute(a)),
children: List(Element(a)),
) -> Element(a) {
html.tr(
[
role("row"),
class(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
),
..attributes
],
children,
)
}
pub fn table_cell(
attributes: List(Attribute(a)),
children: List(Element(a)),
) -> Element(a) {
html.td(
[
class("p-2 align-middle [&:has([role=checkbox])]:pr-0"),
role("gridcell"),
..attributes
],
children,
)
}
pub fn table_head_cell(
attributes: List(Attribute(a)),
children: List(Element(a)),
) -> Element(a) {
html.td(
[
class("p-2 align-middle font-medium [&:has([role=checkbox])]:pr-0"),
role("gridcell"),
..attributes
],
children,
)
}
pub fn table_caption(
attributes: List(Attribute(a)),
children: List(Element(a)),
) -> Element(a) {
html.caption(
[class("mt-4 text-sm text-muted-foreground"), ..attributes],
children,
)
}
pub fn variant(v: Variant) -> Attribute(a) {
case v {
Default -> class("")
Bordered -> class("border")
}
}
pub fn size(s: Size) -> Attribute(a) {
case s {
Small -> class(size_helpers.text_class(size_helpers.Small))
Medium -> class(size_helpers.text_class(size_helpers.Medium))
Large -> class(size_helpers.text_class(size_helpers.Large))
}
}
pub fn selection_mode(m: SelectionMode) -> Attribute(a) {
case m {
None -> attribute("aria-multiselectable", "false")
Single -> attribute("aria-multiselectable", "false")
Multiple -> attribute("aria-multiselectable", "true")
}
}
pub fn aria_label(label: String) -> Attribute(a) {
attribute("aria-label", label)
}
// ============================================================================
// Keyboard Navigation
// ============================================================================
/// Keyboard messages for table component.
pub type Msg {
/// Move to next row (ArrowDown)
MoveDown
/// Move to previous row (ArrowUp)
MoveUp
/// Move to next cell (ArrowRight)
MoveRight
/// Move to previous cell (ArrowLeft)
MoveLeft
/// Move to first cell in row (Home)
MoveRowStart
/// Move to last cell in row (End)
MoveRowEnd
/// Move to first cell in table (Ctrl+Home)
MoveTableFirst
/// Move to last cell in table (Ctrl+End)
MoveTableLast
/// Select the currently focused row (Space)
SelectRow
/// Select the currently focused cell (Enter)
SelectCell
/// Toggle row selection (Ctrl+Space)
ToggleRowSelection
/// Extend selection with Shift+Arrow (multi-select mode)
ExtendSelectionDown
/// Extend selection with Shift+Arrow (multi-select mode)
ExtendSelectionUp
/// Enter cell editing mode (F2 or Enter for editable cells)
EditCell
}
/// Keymap for table keyboard navigation.
/// Follows WAI-ARIA grid pattern for tables:
/// - ArrowDown: Move to next row
/// - ArrowUp: Move to previous row
/// - ArrowRight: Move to next cell
/// - ArrowLeft: Move to previous cell
/// - Home: Move to first cell in row
/// - End: Move to last cell in row
/// - Ctrl+Home: Move to first cell in table
/// - Ctrl+End: Move to last cell in table
/// - Space: Select focused row
/// - Ctrl+Space: Toggle row selection
/// - Enter: Select cell or enter edit mode
/// - F2: Enter cell edit mode (for editable tables)
pub fn keymap(
key_event: keyboard.KeyEvent,
multi_select: Bool,
editable: Bool,
) -> Option(Msg) {
case keyboard.decode_key(key_event.key) {
keyboard.ArrowDown if key_event.shift && multi_select ->
Some(ExtendSelectionDown)
keyboard.ArrowUp if key_event.shift && multi_select ->
Some(ExtendSelectionUp)
keyboard.ArrowDown -> Some(MoveDown)
keyboard.ArrowUp -> Some(MoveUp)
keyboard.ArrowRight -> Some(MoveRight)
keyboard.ArrowLeft -> Some(MoveLeft)
keyboard.Home if key_event.ctrl -> Some(MoveTableFirst)
keyboard.End if key_event.ctrl -> Some(MoveTableLast)
keyboard.Home -> Some(MoveRowStart)
keyboard.End -> Some(MoveRowEnd)
keyboard.Space if key_event.ctrl -> Some(ToggleRowSelection)
keyboard.Space -> Some(SelectRow)
keyboard.Enter if editable -> Some(EditCell)
keyboard.Enter -> Some(SelectCell)
keyboard.F2 if editable -> Some(EditCell)
_ -> option.None
}
}
/// Get the element ID for a table cell at the given row and column.
pub fn table_cell_element_id(row: Int, col: Int) -> String {
"table-cell-" <> int.to_string(row) <> "-" <> int.to_string(col)
}
/// Get the element ID for a table row at the given index.
pub fn table_row_element_id(row: Int) -> String {
"table-row-" <> int.to_string(row)
}
/// Get the element ID for a table column header at the given index.
pub fn table_column_header_element_id(col: Int) -> String {
"table-column-" <> int.to_string(col)
}
/// Attribute to indicate the currently active descendant.
pub fn aria_activedescendant(id: String) -> Attribute(a) {
attribute("aria-activedescendant", id)
}
/// Attribute to indicate sort direction.
pub fn aria_sort(sort: String) -> Attribute(a) {
attribute("aria-sort", sort)
}