Initial iris repository structure
Some checks failed
CI / build (push) Failing after 36s

WASM reactive UI framework for Go:
- reactive/ - Signal[T], Effect, Runtime
- ui/ - Button, Text, Input, View, Canvas, SVG components
- navigation/ - Router, guards, history management
- auth/ - OIDC client for WASM applications
- host/ - Static file server

Extracted from arcadia as open-source component.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-01-08 19:23:49 +01:00
commit 00d98879d3
36 changed files with 4181 additions and 0 deletions

208
ui/input.go Normal file
View File

@@ -0,0 +1,208 @@
package ui
import (
"strconv"
"git.flowmade.one/flowmade-one/iris/internal/element"
"git.flowmade.one/flowmade-one/iris/reactive"
)
// TextInput creates a reactive text input field
func TextInput(value *reactive.Signal[string], placeholder string) View {
input := element.NewElement("input")
input.Attr("type", "text")
input.Attr("placeholder", placeholder)
// Set initial value
input.Set("value", value.Get())
// Update input when signal changes
reactive.NewEffect(func() {
currentValue := value.Get()
if input.Get("value").String() != currentValue {
input.Set("value", currentValue)
}
})
// Update signal when user types
input.On("input", func() {
newValue := input.Get("value").String()
if value.Get() != newValue {
value.Set(newValue)
}
})
return View{input}
}
// TextArea creates a multi-line text input
func TextArea(value *reactive.Signal[string], placeholder string, rows int) View {
textarea := element.NewElement("textarea")
textarea.Attr("placeholder", placeholder)
textarea.Attr("rows", string(rune(rows)))
// Set initial value
textarea.Set("value", value.Get())
// Update textarea when signal changes
reactive.NewEffect(func() {
currentValue := value.Get()
if textarea.Get("value").String() != currentValue {
textarea.Set("value", currentValue)
}
})
// Update signal when user types
textarea.On("input", func() {
newValue := textarea.Get("value").String()
if value.Get() != newValue {
value.Set(newValue)
}
})
return View{textarea}
}
// Checkbox creates a reactive checkbox input
func Checkbox(checked *reactive.Signal[bool], label string) View {
container := element.NewElement("label")
container.Get("style").Call("setProperty", "display", "flex")
container.Get("style").Call("setProperty", "align-items", "center")
container.Get("style").Call("setProperty", "gap", "8px")
container.Get("style").Call("setProperty", "cursor", "pointer")
checkbox := element.NewElement("input")
checkbox.Attr("type", "checkbox")
// Set initial state
if checked.Get() {
checkbox.Set("checked", true)
}
// Update checkbox when signal changes
reactive.NewEffect(func() {
isChecked := checked.Get()
checkbox.Set("checked", isChecked)
})
// Update signal when user clicks
checkbox.On("change", func() {
newValue := checkbox.Get("checked").Bool()
checked.Set(newValue)
})
// Add label text
labelText := element.NewTextNode(label)
container.Child(checkbox)
container.Child(labelText)
return View{container}
}
// Select creates a dropdown selection component
func Select(selected *reactive.Signal[string], options []string, placeholder string) View {
selectElem := element.NewElement("select")
// Add placeholder option if provided
if placeholder != "" {
placeholderOption := element.NewElement("option")
placeholderOption.Set("value", "")
placeholderOption.Set("disabled", true)
placeholderOption.Set("selected", true)
placeholderOption.Set("textContent", placeholder)
selectElem.Child(placeholderOption)
}
// Add options
for _, option := range options {
optionElem := element.NewElement("option")
optionElem.Set("value", option)
optionElem.Set("textContent", option)
selectElem.Child(optionElem)
}
// Set initial value
selectElem.Set("value", selected.Get())
// Update select when signal changes
reactive.NewEffect(func() {
currentValue := selected.Get()
selectElem.Set("value", currentValue)
})
// Update signal when user selects
selectElem.On("change", func() {
newValue := selectElem.Get("value").String()
selected.Set(newValue)
})
return View{selectElem}
}
// Slider creates a range input for numeric values
func Slider(value *reactive.Signal[int], min, max int) View {
slider := element.NewElement("input")
slider.Attr("type", "range")
slider.Attr("min", strconv.Itoa(min))
slider.Attr("max", strconv.Itoa(max))
// Set initial value
slider.Set("value", strconv.Itoa(value.Get()))
// Update slider when signal changes
reactive.NewEffect(func() {
currentValue := value.Get()
slider.Set("value", strconv.Itoa(currentValue))
})
// Update signal when user drags
slider.On("input", func() {
newValueStr := slider.Get("value").String()
if newValue, err := strconv.Atoi(newValueStr); err == nil {
value.Set(newValue)
}
})
return View{slider}
}
// NumberInput creates a reactive numeric input field for float64 values
func NumberInput(value *reactive.Signal[float64], placeholder string) View {
input := element.NewElement("input")
input.Attr("type", "number")
input.Attr("step", "0.01")
input.Attr("min", "0")
input.Attr("placeholder", placeholder)
// Set initial value
if value.Get() != 0 {
input.Set("value", strconv.FormatFloat(value.Get(), 'f', -1, 64))
}
// Update input when signal changes
reactive.NewEffect(func() {
currentValue := value.Get()
var displayValue string
if currentValue == 0 {
displayValue = ""
} else {
displayValue = strconv.FormatFloat(currentValue, 'f', -1, 64)
}
if input.Get("value").String() != displayValue {
input.Set("value", displayValue)
}
})
// Update signal when user types
input.On("input", func() {
newValueStr := input.Get("value").String()
if newValueStr == "" {
value.Set(0.0)
} else if newValue, err := strconv.ParseFloat(newValueStr, 64); err == nil {
value.Set(newValue)
}
})
return View{input}
}