Packages

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

Current section

Files

Jump to
glizzy src ui split_panel.gleam
Raw

src/ui/split_panel.gleam

// MIT License
// Copyright (c) 2026 Koncreate
// See LICENSE for details
import gleam/int
import gleam/option.{type Option, None, Some}
import gleam/string
import lustre/attribute.{type Attribute, attribute, class, role}
import lustre/element.{type Element}
import lustre/element/html.{button, div, text}
import ui/css
import ui/keyboard.{
type Key, ArrowDown, ArrowLeft, ArrowRight, ArrowUp, End, Home, decode_key,
}
import ui/size.{type Size, Large, Medium, Small}
pub type Orientation {
Horizontal
Vertical
}
/// Split panel container with adjustable divider
pub fn split_panel(
attributes: List(Attribute(a)),
children: List(Element(a)),
) -> Element(a) {
div(
[
class("flex overflow-hidden rounded-md border border-input"),
role("region"),
attribute("aria-label", "Split panel"),
..attributes
],
children,
)
}
/// Left or top pane with percentage-based size
pub fn pane_a(
attributes: List(Attribute(a)),
size_percent: Int,
children: List(Element(a)),
) -> Element(a) {
div(
[
class("overflow-auto"),
attribute("style", "flex: " <> int.to_string(size_percent) <> " 0 0;"),
..attributes
],
children,
)
}
/// Right or bottom pane (fills remaining space)
pub fn pane_b(
attributes: List(Attribute(a)),
children: List(Element(a)),
) -> Element(a) {
div([class("overflow-auto flex-1"), ..attributes], children)
}
/// Adjustable divider with click controls
/// Pass on_click handlers for the decrease/increase buttons via attributes
pub fn adjustable_divider(
attributes: List(Attribute(a)),
position: Int,
) -> Element(a) {
div(
[
class(
[
"relative flex items-center justify-center w-4",
"bg-muted/50 hover:bg-primary/20",
"transition-colors cursor-col-resize",
]
|> string.join(" "),
),
attribute("role", "separator"),
attribute("aria-label", "Adjustable divider"),
attribute("aria-valuenow", int.to_string(position)),
attribute("aria-valuemin", "10"),
attribute("aria-valuemax", "90"),
attribute("tabindex", "0"),
..attributes
],
[
// Decrease button (make left panel smaller) - arrow pointing left
button(
[
class(
[
"absolute left-0 -translate-x-1/2 h-6 w-6 rounded-full bg-background border border-border",
"flex items-center justify-center",
"hover:bg-primary hover:text-primary-foreground",
css.focus_ring(),
"shadow-sm",
"text-xs font-bold",
]
|> string.join(" "),
),
attribute("type", "button"),
attribute("aria-label", "Decrease left panel size"),
attribute("data-action", "decrease"),
attribute("data-split-action", "decrease"),
attribute("tabindex", "-1"),
],
[text("◀")],
),
// Position indicator
div(
[
class("text-[10px] text-muted-foreground font-mono select-none"),
],
[text(int.to_string(position) <> "%")],
),
// Increase button (make left panel larger) - arrow pointing right
button(
[
class(
[
"absolute right-0 translate-x-1/2 h-6 w-6 rounded-full bg-background border border-border",
"flex items-center justify-center",
"hover:bg-primary hover:text-primary-foreground",
css.focus_ring(),
"shadow-sm",
"text-xs font-bold",
]
|> string.join(" "),
),
attribute("type", "button"),
attribute("aria-label", "Increase left panel size"),
attribute("data-action", "increase"),
attribute("data-split-action", "increase"),
attribute("tabindex", "-1"),
],
[text("▶")],
),
],
)
}
/// Helper to create click handler for split panel adjustment
/// Use with event delegation on the parent container
pub fn on_split_click(current_position: Int, step: Int) -> fn(String) -> Int {
fn(action: String) -> Int {
case action {
"decrease" | "ArrowLeft" -> {
let new_pos = current_position - step
case new_pos < 10 {
True -> 10
False -> new_pos
}
}
"increase" | "ArrowRight" -> {
let new_pos = current_position + step
case new_pos > 90 {
True -> 90
False -> new_pos
}
}
_ -> current_position
}
}
}
/// Simple divider handle (visual only, for future drag implementation)
pub fn divider_handle(attributes: List(Attribute(a))) -> Element(a) {
div(
[
class(
[
"w-1 h-full cursor-col-resize",
"bg-border hover:bg-primary/50",
"transition-colors",
]
|> string.join(" "),
),
attribute("role", "separator"),
..attributes
],
[],
)
}
/// Orientation attribute
pub fn orientation(o: Orientation) -> Attribute(a) {
case o {
Horizontal -> attribute("data-orientation", "horizontal")
Vertical -> attribute("data-orientation", "vertical")
}
}
/// Size attribute (height for horizontal splits)
pub fn size(s: Size) -> Attribute(a) {
case s {
Small -> class("h-48")
Medium -> class("h-64")
Large -> class("h-96")
}
}
/// Keyboard handler helper - returns message for key events
/// Use in your app's keydown handler
pub fn handle_key_position(key: String, current_position: Int, step: Int) -> Int {
let min_pos = 10
let max_pos = 90
case key {
"ArrowLeft" | "ArrowUp" -> {
let new_pos = current_position - step
case new_pos < min_pos {
True -> min_pos
False -> new_pos
}
}
"ArrowRight" | "ArrowDown" -> {
let new_pos = current_position + step
case new_pos > max_pos {
True -> max_pos
False -> new_pos
}
}
_ -> current_position
}
}
// ============================================================================
// Keyboard Navigation
// ============================================================================
/// Messages for split panel keyboard navigation
pub type Msg {
Decrease
Increase
Reset
SetMin
SetMax
}
/// Keymap for split panel keyboard navigation
///
/// ## Keyboard interactions:
/// - **ArrowLeft/ArrowUp**: Decrease panel size (horizontal/vertical split)
/// - **ArrowRight/ArrowDown**: Increase panel size
/// - **Home**: Reset to default size (50%)
/// - **End**: Maximize current panel (90% or 10%)
///
/// Follows WAI-ARIA [Separator](https://www.w3.org/WAI/ARIA/apg/patterns/separator/) pattern.
pub fn keymap(
key_event: keyboard.KeyEvent,
orientation: Orientation,
) -> Option(Msg) {
let key = decode_key(key_event.key)
case orientation {
Horizontal -> {
case key {
ArrowLeft -> Some(Decrease)
ArrowRight -> Some(Increase)
Home -> Some(Reset)
End -> Some(SetMax)
_ -> None
}
}
Vertical -> {
case key {
ArrowUp -> Some(Decrease)
ArrowDown -> Some(Increase)
Home -> Some(Reset)
End -> Some(SetMax)
_ -> None
}
}
}
}