Current section
Files
Jump to
Current section
Files
src/ui/color_picker.gleam
// MIT License
// Copyright (c) 2026 Koncreate
// See LICENSE for details
import gleam/option.{type Option, None, Some}
import lustre/attribute.{type Attribute, attribute, class, role, type_}
import lustre/element.{type Element}
import lustre/element/html
import ui/keyboard.{
type Key, ArrowDown, ArrowLeft, ArrowRight, ArrowUp, End, Home, PageDown,
PageUp, decode_key,
}
import ui/size.{type Size, Large, Medium, Small}
// ============================================================================
// Types
// ============================================================================
pub type Variant {
Default
Muted
}
/// Messages for color picker keyboard navigation
pub type Msg {
HueIncrease
HueDecrease
SaturationIncrease
SaturationDecrease
LightnessIncrease
LightnessDecrease
HueMin
HueMax
SaturationMin
SaturationMax
LightnessMin
LightnessMax
}
/// Color mode for keyboard navigation
pub type ColorMode {
Hue
Saturation
Lightness
}
// ============================================================================
// Color Picker Component (Presentational - controlled by parent)
// ============================================================================
pub fn color_picker(
attributes: List(Attribute(a)),
children: List(Element(a)),
) -> Element(a) {
html.div(
[
class("grid gap-3 rounded-md border border-input bg-background p-3"),
role("group"),
attribute("aria-label", "Color picker"),
..attributes
],
children,
)
}
pub fn variant(v: Variant) -> Attribute(a) {
case v {
Default -> class("bg-background text-foreground")
Muted -> class("bg-muted text-muted-foreground")
}
}
pub fn size(s: Size) -> Attribute(a) {
case s {
Small -> class("max-w-xs")
Medium -> class("max-w-sm")
Large -> class("max-w-md")
}
}
pub fn value(val: String) -> Attribute(a) {
attribute("value", val)
}
pub fn label(lbl: String) -> Attribute(a) {
attribute("aria-label", lbl)
}
// ============================================================================
// Helper Components
// ============================================================================
pub fn swatch(color: String, attributes: List(Attribute(a))) -> Element(a) {
html.div(
[
class("h-10 w-10 rounded-md border border-border cursor-pointer"),
attribute("style", "background-color: " <> color),
..attributes
],
[],
)
}
pub fn input(attributes: List(Attribute(a))) -> Element(a) {
html.input([
class(
"flex h-10 flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring",
),
type_("text"),
..attributes
])
}
pub fn hint(text: String) -> Element(a) {
html.p([class("text-xs text-muted-foreground")], [html.text(text)])
}
// ============================================================================
// Keyboard Navigation
// ============================================================================
/// Keymap for color picker keyboard navigation
///
/// ## Keyboard interactions:
/// - **Arrow Right/Up**: Increase current color value
/// - **Arrow Left/Down**: Decrease current color value
/// - **Home**: Minimum value (0)
/// - **End**: Maximum value (360 for hue, 100 for saturation/lightness)
/// - **PageUp**: Large increment (+10)
/// - **PageDown**: Large decrement (-10)
///
/// Follows WAI-ARIA [Slider](https://www.w3.org/WAI/ARIA/apg/patterns/slider/) pattern.
pub fn keymap(key_event: keyboard.KeyEvent, mode: ColorMode) -> Option(Msg) {
let key = decode_key(key_event.key)
case mode {
Hue -> {
case key {
ArrowRight | ArrowUp -> Some(HueIncrease)
ArrowLeft | ArrowDown -> Some(HueDecrease)
Home -> Some(HueMin)
End -> Some(HueMax)
PageUp -> Some(HueIncrease)
PageDown -> Some(HueDecrease)
_ -> None
}
}
Saturation -> {
case key {
ArrowRight | ArrowUp -> Some(SaturationIncrease)
ArrowLeft | ArrowDown -> Some(SaturationDecrease)
Home -> Some(SaturationMin)
End -> Some(SaturationMax)
PageUp -> Some(SaturationIncrease)
PageDown -> Some(SaturationDecrease)
_ -> None
}
}
Lightness -> {
case key {
ArrowRight | ArrowUp -> Some(LightnessIncrease)
ArrowLeft | ArrowDown -> Some(LightnessDecrease)
Home -> Some(LightnessMin)
End -> Some(LightnessMax)
PageUp -> Some(LightnessIncrease)
PageDown -> Some(LightnessDecrease)
_ -> None
}
}
}
}