diff --git a/examples/counter.lux b/examples/counter.lux new file mode 100644 index 0000000..201325e --- /dev/null +++ b/examples/counter.lux @@ -0,0 +1,173 @@ +// Counter Example - A simple interactive counter using TEA pattern +// +// This example demonstrates: +// - Model-View-Update architecture (TEA) +// - Html DSL for describing UI (inline version) +// - Message-based state updates + +// ============================================================================ +// Html Types (subset of stdlib/html) +// ============================================================================ + +type Html = + | Element(String, List>, List>) + | Text(String) + | Empty + +type Attr = + | Class(String) + | Id(String) + | OnClick(M) + +// Html builder helpers +fn div(attrs: List>, children: List>): Html = + Element("div", attrs, children) + +fn span(attrs: List>, children: List>): Html = + Element("span", attrs, children) + +fn h1(attrs: List>, children: List>): Html = + Element("h1", attrs, children) + +fn button(attrs: List>, children: List>): Html = + Element("button", attrs, children) + +fn text(content: String): Html = + Text(content) + +fn class(name: String): Attr = + Class(name) + +fn onClick(msg: M): Attr = + OnClick(msg) + +// ============================================================================ +// Model - The application state (using ADT wrapper) +// ============================================================================ + +type Model = + | Counter(Int) + +fn getCount(model: Model): Int = + match model { + Counter(n) => n + } + +fn init(): Model = Counter(0) + +// ============================================================================ +// Messages - Events that can occur +// ============================================================================ + +type Msg = + | Increment + | Decrement + | Reset + +// ============================================================================ +// Update - State transitions +// ============================================================================ + +fn update(model: Model, msg: Msg): Model = + match msg { + Increment => Counter(getCount(model) + 1), + Decrement => Counter(getCount(model) - 1), + Reset => Counter(0) + } + +// ============================================================================ +// View - Render the UI +// ============================================================================ + +fn viewCounter(count: Int): Html = { + let countText = text(toString(count)) + let countSpan = span([class("count")], [countText]) + let displayDiv = div([class("counter-display")], [countSpan]) + + let minusBtn = button([onClick(Decrement), class("btn")], [text("-")]) + let resetBtn = button([onClick(Reset), class("btn btn-reset")], [text("Reset")]) + let plusBtn = button([onClick(Increment), class("btn")], [text("+")]) + let buttonsDiv = div([class("counter-buttons")], [minusBtn, resetBtn, plusBtn]) + + let title = h1([], [text("Counter")]) + div([class("counter-app")], [title, displayDiv, buttonsDiv]) +} + +fn view(model: Model): Html = viewCounter(getCount(model)) + +// ============================================================================ +// Debug: Print Html structure +// ============================================================================ + +fn showAttr(attr: Attr): String = + match attr { + Class(s) => "class=\"" + s + "\"", + Id(s) => "id=\"" + s + "\"", + OnClick(msg) => match msg { + Increment => "onclick=\"Increment\"", + Decrement => "onclick=\"Decrement\"", + Reset => "onclick=\"Reset\"" + } + } + +fn showAttrs(attrs: List>): String = + match List.head(attrs) { + None => "", + Some(a) => match List.tail(attrs) { + None => showAttr(a), + Some(rest) => showAttr(a) + " " + showAttrs(rest) + } + } + +fn showChildren(children: List>, indent: Int): String = + match List.head(children) { + None => "", + Some(c) => match List.tail(children) { + None => showHtml(c, indent), + Some(rest) => showHtml(c, indent) + showChildren(rest, indent) + } + } + +fn showHtml(html: Html, indent: Int): String = + match html { + Empty => "", + Text(s) => s, + Element(tag, attrs, children) => { + let attrStr = showAttrs(attrs) + let attrPart = if String.length(attrStr) > 0 then " " + attrStr else "" + let childStr = showChildren(children, indent + 2) + "<" + tag + attrPart + ">" + childStr + "" + } + } + +// ============================================================================ +// Entry point +// ============================================================================ + +fn main(): Unit with {Console} = { + let model = init() + Console.print("=== Counter App (TEA Pattern) ===") + Console.print("") + Console.print("Initial count: " + toString(getCount(model))) + Console.print("") + + let m1 = update(model, Increment) + Console.print("After Increment: " + toString(getCount(m1))) + + let m2 = update(m1, Increment) + Console.print("After Increment: " + toString(getCount(m2))) + + let m3 = update(m2, Increment) + Console.print("After Increment: " + toString(getCount(m3))) + + let m4 = update(m3, Decrement) + Console.print("After Decrement: " + toString(getCount(m4))) + + let m5 = update(m4, Reset) + Console.print("After Reset: " + toString(getCount(m5))) + + Console.print("") + Console.print("=== View (HTML Structure) ===") + Console.print(showHtml(view(m2), 0)) +} +let output = run main() with {} diff --git a/examples/web/README.md b/examples/web/README.md new file mode 100644 index 0000000..54d1723 --- /dev/null +++ b/examples/web/README.md @@ -0,0 +1,154 @@ +# Lux Web Examples + +Interactive browser examples demonstrating Lux compiled to JavaScript. + +## Quick Start + +```bash +# 1. Compile the counter example +lux compile examples/web/counter.lux --target js -o examples/web/counter.js + +# 2. Start a local server +cd examples/web +./serve.sh + +# 3. Open in browser +# http://localhost:8080/index.html +``` + +## Examples + +### Counter (String-based) + +A simple counter demonstrating the TEA (The Elm Architecture) pattern using string concatenation for HTML: + +```lux +fn view(model: Model): String = { + "
" + toString(getCount(model)) + "
" +} +``` + +### Counter with Html Module (Type-safe) + +The same counter using the type-safe Html module: + +```lux +fn view(model: Model): String = { + let count = getCount(model) + let display = Html.div([Html.class("display")], [Html.text(toString(count))]) + Html.render(display) +} +``` + +Compile with: +```bash +lux compile examples/web/counter_html.lux --target js -o examples/web/counter_html.js +``` + +## The Elm Architecture (TEA) + +Both examples follow the TEA pattern: + +- **Model**: Application state (`Counter(Int)`) +- **Msg**: Events (`Increment`, `Decrement`, `Reset`) +- **Update**: State transitions `(Model, Msg) -> Model` +- **View**: HTML rendering `Model -> Html` or `Model -> String` + +## Available Modules + +### Html Module + +Type-safe HTML construction: + +```lux +// Elements +Html.div(attrs, children) +Html.span(attrs, children) +Html.button(attrs, children) +Html.input(attrs, children) +Html.h1(attrs, children) +// ... 30+ elements + +// Attributes +Html.class("name") +Html.id("id") +Html.href("url") +Html.style("...") +Html.attr("name", "value") + +// Events +Html.onClick(handler) +Html.onInput(handler) +Html.on("event", handler) + +// Text +Html.text("content") + +// Rendering +Html.render(node) // -> String (for innerHTML) +Html.renderToDom(node) // -> Element (for appendChild) +``` + +### Dom Effect + +Direct DOM manipulation: + +```lux +fn main(): Unit with {Dom} = { + match Dom.querySelector("#app") { + Some(el) => Dom.setTextContent(el, "Hello!"), + None => () + } +} +``` + +Available operations: +- `querySelector`, `querySelectorAll`, `getElementById` +- `createElement`, `createTextNode`, `appendChild` +- `setTextContent`, `getTextContent`, `setInnerHtml` +- `setAttribute`, `getAttribute`, `addClass`, `removeClass` +- `addEventListener`, `focus`, `blur` +- `getValue`, `setValue` (for inputs) +- `scrollTo`, `getWindowSize`, `getBoundingClientRect` + +## How It Works + +1. Lux code is compiled to JavaScript using `lux compile --target js` +2. The HTML page loads the compiled JS +3. A minimal runtime handles the TEA loop: + - `luxInit_lux()` creates initial state + - `luxUpdate_lux(model, msg)` handles state updates + - `luxView_lux(model)` renders HTML + - `dispatch(msg)` triggers updates and re-renders + +## TEA Runtime + +For applications using the Html module with event handlers, you can use the built-in TEA runtime: + +```javascript +Lux.app({ + init: luxInit_lux(), + update: (model, msg) => luxUpdate_lux(model, msg), + view: (model, dispatch) => luxView_lux(model), + root: "#app" +}); +``` + +Or for string-based views: + +```javascript +Lux.simpleApp({ + init: luxInit_lux(), + update: (model, msg) => luxUpdate_lux(model, msg), + view: (model) => luxView_lux(model), + root: "#app" +}); +``` + +## Development + +To modify an example: + +1. Edit the `.lux` file +2. Recompile: `lux compile .lux --target js -o .js` +3. Refresh the browser diff --git a/examples/web/counter.js b/examples/web/counter.js new file mode 100644 index 0000000..d7ed125 --- /dev/null +++ b/examples/web/counter.js @@ -0,0 +1,144 @@ +// Lux Runtime +const Lux = { + Some: (value) => ({ tag: "Some", value }), + None: () => ({ tag: "None" }), + + Ok: (value) => ({ tag: "Ok", value }), + Err: (error) => ({ tag: "Err", error }), + + Cons: (head, tail) => [head, ...tail], + Nil: () => [], + + defaultHandlers: { + Console: { + print: (msg) => console.log(msg), + readLine: () => { + if (typeof require !== 'undefined') { + const readline = require('readline'); + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); + return new Promise(resolve => rl.question('', answer => { rl.close(); resolve(answer); })); + } + return prompt('') || ''; + }, + readInt: () => parseInt(Lux.defaultHandlers.Console.readLine(), 10) + }, + Random: { + int: (min, max) => Math.floor(Math.random() * (max - min + 1)) + min, + bool: () => Math.random() < 0.5, + float: () => Math.random() + }, + Time: { + now: () => Date.now(), + sleep: (ms) => new Promise(resolve => setTimeout(resolve, ms)) + }, + Http: { + get: async (url) => { + try { + const response = await fetch(url); + const body = await response.text(); + const headers = []; + response.headers.forEach((v, k) => headers.push([k, v])); + return Lux.Ok({ status: response.status, body, headers }); + } catch (e) { + return Lux.Err(e.message); + } + }, + post: async (url, body) => { + try { + const response = await fetch(url, { method: 'POST', body }); + const respBody = await response.text(); + const headers = []; + response.headers.forEach((v, k) => headers.push([k, v])); + return Lux.Ok({ status: response.status, body: respBody, headers }); + } catch (e) { + return Lux.Err(e.message); + } + }, + postJson: async (url, json) => { + try { + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(json) + }); + const body = await response.text(); + const headers = []; + response.headers.forEach((v, k) => headers.push([k, v])); + return Lux.Ok({ status: response.status, body, headers }); + } catch (e) { + return Lux.Err(e.message); + } + } + } + } +}; + +// Model constructors +function Counter_lux(field0) { return { tag: "Counter", field0: field0 }; } + +// Msg constructors +const Increment_lux = { tag: "Increment" }; +const Decrement_lux = { tag: "Decrement" }; +const Reset_lux = { tag: "Reset" }; + +function getCount_lux(m) { + const _match_0 = m; + let _result_1; + if (_match_0.tag === "Counter") { + const n = _match_0.field0; + _result_1 = n; + } else { + throw new Error('Non-exhaustive match on ' + JSON.stringify(_match_0)); + } + return _result_1; +} + +function init_lux() { + return Counter_lux(0); +} + +function update_lux(model, msg) { + const _match_2 = msg; + let _result_3; + if (_match_2.tag === "Increment") { + _result_3 = Counter_lux((getCount_lux(model) + 1)); + } else if (_match_2.tag === "Decrement") { + _result_3 = Counter_lux((getCount_lux(model) - 1)); + } else if (_match_2.tag === "Reset") { + _result_3 = Counter_lux(0); + } else { + throw new Error('Non-exhaustive match on ' + JSON.stringify(_match_2)); + } + return _result_3; +} + +function view_lux(model) { + const count_4 = getCount_lux(model); + return (((((((((("
" + "

Lux Counter

") + "
") + String(count_4)) + "
") + "
") + "") + "") + "") + "
") + "
"); +} + +function luxInit_lux() { + return init_lux(); +} + +function luxUpdate_lux(model, msgName) { + const _match_5 = msgName; + let _result_6; + if (_match_5 === "Increment") { + _result_6 = update_lux(model, Increment_lux); + } else if (_match_5 === "Decrement") { + _result_6 = update_lux(model, Decrement_lux); + } else if (_match_5 === "Reset") { + _result_6 = update_lux(model, Reset_lux); + } else if (true) { + _result_6 = model; + } else { + throw new Error('Non-exhaustive match on ' + JSON.stringify(_match_5)); + } + return _result_6; +} + +function luxView_lux(model) { + return view_lux(model); +} + diff --git a/examples/web/counter.lux b/examples/web/counter.lux new file mode 100644 index 0000000..9687fb9 --- /dev/null +++ b/examples/web/counter.lux @@ -0,0 +1,62 @@ +// Simple Counter for Browser +// Compile with: lux compile examples/web/counter.lux --target js -o examples/web/counter.js + +// ============================================================================ +// Model +// ============================================================================ + +type Model = | Counter(Int) + +fn getCount(m: Model): Int = match m { Counter(n) => n } + +fn init(): Model = Counter(0) + +// ============================================================================ +// Messages +// ============================================================================ + +type Msg = | Increment | Decrement | Reset + +// ============================================================================ +// Update +// ============================================================================ + +fn update(model: Model, msg: Msg): Model = + match msg { + Increment => Counter(getCount(model) + 1), + Decrement => Counter(getCount(model) - 1), + Reset => Counter(0) + } + +// ============================================================================ +// View - Returns HTML string for simplicity +// ============================================================================ + +fn view(model: Model): String = { + let count = getCount(model) + "
" + + "

Lux Counter

" + + "
" + toString(count) + "
" + + "
" + + "" + + "" + + "" + + "
" + + "
" +} + +// ============================================================================ +// Export for browser runtime +// ============================================================================ + +fn luxInit(): Model = init() + +fn luxUpdate(model: Model, msgName: String): Model = + match msgName { + "Increment" => update(model, Increment), + "Decrement" => update(model, Decrement), + "Reset" => update(model, Reset), + _ => model + } + +fn luxView(model: Model): String = view(model) diff --git a/examples/web/counter_html.lux b/examples/web/counter_html.lux new file mode 100644 index 0000000..9f61bde --- /dev/null +++ b/examples/web/counter_html.lux @@ -0,0 +1,82 @@ +// Counter with Html Module (Type-safe HTML) +// Compile with: lux compile examples/web/counter_html.lux --target js -o examples/web/counter_html.js +// +// This version uses the Html module for type-safe HTML construction +// instead of string concatenation. The Html module provides: +// - Type-safe element constructors (div, button, etc.) +// - Type-safe attribute constructors (class, onClick, etc.) +// - Automatic HTML escaping + +// ============================================================================ +// Model +// ============================================================================ + +type Model = | Counter(Int) + +fn getCount(m: Model): Int = match m { Counter(n) => n } + +fn init(): Model = Counter(0) + +// ============================================================================ +// Messages +// ============================================================================ + +type Msg = | Increment | Decrement | Reset + +// ============================================================================ +// Update +// ============================================================================ + +fn update(model: Model, msg: Msg): Model = + match msg { + Increment => Counter(getCount(model) + 1), + Decrement => Counter(getCount(model) - 1), + Reset => Counter(0) + } + +// ============================================================================ +// View - Type-safe HTML using Html module +// ============================================================================ + +fn view(model: Model): String = { + let count = getCount(model) + + // Build HTML tree using Html module + let title = Html.h1([], [Html.text("Lux Counter")]) + let display = Html.div([Html.class("display")], [Html.text(toString(count))]) + + let decBtn = Html.button( + [Html.attr("onclick", "dispatch('Decrement')")], + [Html.text("-")] + ) + let resetBtn = Html.button( + [Html.attr("onclick", "dispatch('Reset')")], + [Html.text("Reset")] + ) + let incBtn = Html.button( + [Html.attr("onclick", "dispatch('Increment')")], + [Html.text("+")] + ) + let buttons = Html.div([Html.class("buttons")], [decBtn, resetBtn, incBtn]) + + let container = Html.div([Html.class("counter")], [title, display, buttons]) + + // Render to HTML string + Html.render(container) +} + +// ============================================================================ +// Export for browser runtime +// ============================================================================ + +fn luxInit(): Model = init() + +fn luxUpdate(model: Model, msgName: String): Model = + match msgName { + "Increment" => update(model, Increment), + "Decrement" => update(model, Decrement), + "Reset" => update(model, Reset), + _ => model + } + +fn luxView(model: Model): String = view(model) diff --git a/examples/web/index.html b/examples/web/index.html new file mode 100644 index 0000000..e7dab10 --- /dev/null +++ b/examples/web/index.html @@ -0,0 +1,133 @@ + + + + + + Lux Counter Example + + + +
Loading...
+ + + + + + + + + + diff --git a/examples/web/serve.sh b/examples/web/serve.sh new file mode 100755 index 0000000..5257d7c --- /dev/null +++ b/examples/web/serve.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# Simple HTTP server for testing Lux web examples +# Usage: ./serve.sh [port] + +PORT=${1:-8080} +DIR="$(dirname "$0")" + +echo "Serving Lux web examples at http://localhost:$PORT" +echo "Open http://localhost:$PORT/index.html in your browser" +echo "Press Ctrl+C to stop" +echo "" + +cd "$DIR" + +# Try python3 first, then python, then node +if command -v python3 &> /dev/null; then + python3 -m http.server $PORT +elif command -v python &> /dev/null; then + python -m SimpleHTTPServer $PORT +elif command -v npx &> /dev/null; then + npx serve -p $PORT . +else + echo "Error: No suitable HTTP server found." + echo "Install Python or Node.js to serve files." + exit 1 +fi diff --git a/stdlib/browser.lux b/stdlib/browser.lux new file mode 100644 index 0000000..2a3b30c --- /dev/null +++ b/stdlib/browser.lux @@ -0,0 +1,89 @@ +// Browser Module - Effects for browser/DOM interaction +// +// This module provides effects for interacting with the browser DOM, +// local storage, and other browser APIs. + +// Opaque Element type (represents a DOM element) +type Element = + | DomElement(Int) + +// DOM manipulation effect +effect Dom { + fn getElementById(id: String): Option + fn querySelector(selector: String): Option + fn querySelectorAll(selector: String): List + fn createElement(tag: String): Element + fn createTextNode(text: String): Element + fn appendChild(parent: Element, child: Element): Unit + fn removeChild(parent: Element, child: Element): Unit + fn setAttribute(element: Element, name: String, value: String): Unit + fn removeAttribute(element: Element, name: String): Unit + fn setProperty(element: Element, name: String, value: String): Unit + fn setTextContent(element: Element, text: String): Unit + fn getBody(): Element + fn focus(element: Element): Unit + fn blur(element: Element): Unit +} + +// Browser storage effect +effect Storage { + fn getItem(key: String): Option + fn setItem(key: String, value: String): Unit + fn removeItem(key: String): Unit + fn clear(): Unit +} + +// Browser navigation effect +effect Navigation { + fn pushState(url: String): Unit + fn replaceState(url: String): Unit + fn back(): Unit + fn forward(): Unit + fn getLocation(): String + fn getPathname(): String +} + +// Browser window effect +effect Window { + fn alert(message: String): Unit + fn confirm(message: String): Bool + fn scrollTo(x: Int, y: Int): Unit + fn getInnerWidth(): Int + fn getInnerHeight(): Int +} + +// ============================================================================ +// Subscription types for listening to external events +// ============================================================================ + +type Sub = + | OnAnimationFrame(fn(Float): M) + | OnResize(fn(Int, Int): M) + | OnKeyPress(fn(String): M) + | OnKeyDown(fn(String): M) + | OnKeyUp(fn(String): M) + | OnMouseMove(fn(Int, Int): M) + | OnClick(fn(Int, Int): M) + | OnUrlChange(fn(String): M) + | Every(Int, fn(Float): M) + | NoSub + +// Subscription constructors +fn onAnimationFrame(toMsg: fn(Float): M): Sub = + OnAnimationFrame(toMsg) + +fn onResize(toMsg: fn(Int, Int): M): Sub = + OnResize(toMsg) + +fn onKeyPress(toMsg: fn(String): M): Sub = + OnKeyPress(toMsg) + +fn every(ms: Int, toMsg: fn(Float): M): Sub = + Every(ms, toMsg) + +fn noSub(): Sub = + NoSub + +// Combine multiple subscriptions +fn batch(subs: List>): List> = + subs diff --git a/stdlib/html.lux b/stdlib/html.lux new file mode 100644 index 0000000..cff6088 --- /dev/null +++ b/stdlib/html.lux @@ -0,0 +1,298 @@ +// Html Module - DSL for building HTML/DOM structures +// +// This module provides a type-safe way to describe HTML elements that can be +// compiled to efficient JavaScript for browser rendering. +// +// Example usage: +// let myView = div([class("container")], [ +// h1([id("title")], [text("Hello!")]), +// button([onClick(Increment)], [text("+")]) +// ]) + +// Html type represents a DOM structure +// Parameterized by Msg - the type of messages emitted by event handlers +type Html = + | Element(String, List>, List>) + | Text(String) + | Empty + +// Attributes that can be applied to elements +type Attr = + | Class(String) + | Id(String) + | Style(String, String) + | Href(String) + | Src(String) + | Alt(String) + | Type(String) + | Value(String) + | Placeholder(String) + | Disabled(Bool) + | Checked(Bool) + | Name(String) + | OnClick(M) + | OnInput(fn(String): M) + | OnSubmit(M) + | OnChange(fn(String): M) + | OnMouseEnter(M) + | OnMouseLeave(M) + | OnFocus(M) + | OnBlur(M) + | OnKeyDown(fn(String): M) + | OnKeyUp(fn(String): M) + | DataAttr(String, String) + +// ============================================================================ +// Element builders - Container elements +// ============================================================================ + +fn div(attrs: List>, children: List>): Html = + Element("div", attrs, children) + +fn span(attrs: List>, children: List>): Html = + Element("span", attrs, children) + +fn section(attrs: List>, children: List>): Html = + Element("section", attrs, children) + +fn article(attrs: List>, children: List>): Html = + Element("article", attrs, children) + +fn header(attrs: List>, children: List>): Html = + Element("header", attrs, children) + +fn footer(attrs: List>, children: List>): Html = + Element("footer", attrs, children) + +fn nav(attrs: List>, children: List>): Html = + Element("nav", attrs, children) + +fn main(attrs: List>, children: List>): Html = + Element("main", attrs, children) + +fn aside(attrs: List>, children: List>): Html = + Element("aside", attrs, children) + +// ============================================================================ +// Element builders - Text elements +// ============================================================================ + +fn h1(attrs: List>, children: List>): Html = + Element("h1", attrs, children) + +fn h2(attrs: List>, children: List>): Html = + Element("h2", attrs, children) + +fn h3(attrs: List>, children: List>): Html = + Element("h3", attrs, children) + +fn h4(attrs: List>, children: List>): Html = + Element("h4", attrs, children) + +fn h5(attrs: List>, children: List>): Html = + Element("h5", attrs, children) + +fn h6(attrs: List>, children: List>): Html = + Element("h6", attrs, children) + +fn p(attrs: List>, children: List>): Html = + Element("p", attrs, children) + +fn pre(attrs: List>, children: List>): Html = + Element("pre", attrs, children) + +fn code(attrs: List>, children: List>): Html = + Element("code", attrs, children) + +fn blockquote(attrs: List>, children: List>): Html = + Element("blockquote", attrs, children) + +// ============================================================================ +// Element builders - Inline elements +// ============================================================================ + +fn a(attrs: List>, children: List>): Html = + Element("a", attrs, children) + +fn strong(attrs: List>, children: List>): Html = + Element("strong", attrs, children) + +fn em(attrs: List>, children: List>): Html = + Element("em", attrs, children) + +fn small(attrs: List>, children: List>): Html = + Element("small", attrs, children) + +fn br(): Html = + Element("br", [], []) + +fn hr(): Html = + Element("hr", [], []) + +// ============================================================================ +// Element builders - Lists +// ============================================================================ + +fn ul(attrs: List>, children: List>): Html = + Element("ul", attrs, children) + +fn ol(attrs: List>, children: List>): Html = + Element("ol", attrs, children) + +fn li(attrs: List>, children: List>): Html = + Element("li", attrs, children) + +// ============================================================================ +// Element builders - Forms +// ============================================================================ + +fn form(attrs: List>, children: List>): Html = + Element("form", attrs, children) + +fn input(attrs: List>): Html = + Element("input", attrs, []) + +fn textarea(attrs: List>, children: List>): Html = + Element("textarea", attrs, children) + +fn button(attrs: List>, children: List>): Html = + Element("button", attrs, children) + +fn label(attrs: List>, children: List>): Html = + Element("label", attrs, children) + +fn select(attrs: List>, children: List>): Html = + Element("select", attrs, children) + +fn option(attrs: List>, children: List>): Html = + Element("option", attrs, children) + +// ============================================================================ +// Element builders - Media +// ============================================================================ + +fn img(attrs: List>): Html = + Element("img", attrs, []) + +fn video(attrs: List>, children: List>): Html = + Element("video", attrs, children) + +fn audio(attrs: List>, children: List>): Html = + Element("audio", attrs, children) + +// ============================================================================ +// Element builders - Tables +// ============================================================================ + +fn table(attrs: List>, children: List>): Html = + Element("table", attrs, children) + +fn thead(attrs: List>, children: List>): Html = + Element("thead", attrs, children) + +fn tbody(attrs: List>, children: List>): Html = + Element("tbody", attrs, children) + +fn tr(attrs: List>, children: List>): Html = + Element("tr", attrs, children) + +fn th(attrs: List>, children: List>): Html = + Element("th", attrs, children) + +fn td(attrs: List>, children: List>): Html = + Element("td", attrs, children) + +// ============================================================================ +// Text and empty nodes +// ============================================================================ + +fn text(content: String): Html = + Text(content) + +fn empty(): Html = + Empty + +// ============================================================================ +// Attribute helpers +// ============================================================================ + +fn class(name: String): Attr = + Class(name) + +fn id(name: String): Attr = + Id(name) + +fn style(property: String, value: String): Attr = + Style(property, value) + +fn href(url: String): Attr = + Href(url) + +fn src(url: String): Attr = + Src(url) + +fn alt(description: String): Attr = + Alt(description) + +fn inputType(t: String): Attr = + Type(t) + +fn value(v: String): Attr = + Value(v) + +fn placeholder(p: String): Attr = + Placeholder(p) + +fn disabled(d: Bool): Attr = + Disabled(d) + +fn checked(c: Bool): Attr = + Checked(c) + +fn name(n: String): Attr = + Name(n) + +fn onClick(msg: M): Attr = + OnClick(msg) + +fn onInput(h: fn(String): M): Attr = + OnInput(h) + +fn onSubmit(msg: M): Attr = + OnSubmit(msg) + +fn onChange(h: fn(String): M): Attr = + OnChange(h) + +fn onMouseEnter(msg: M): Attr = + OnMouseEnter(msg) + +fn onMouseLeave(msg: M): Attr = + OnMouseLeave(msg) + +fn onFocus(msg: M): Attr = + OnFocus(msg) + +fn onBlur(msg: M): Attr = + OnBlur(msg) + +fn onKeyDown(h: fn(String): M): Attr = + OnKeyDown(h) + +fn onKeyUp(h: fn(String): M): Attr = + OnKeyUp(h) + +fn data(name: String, value: String): Attr = + DataAttr(name, value) + +// ============================================================================ +// Utility functions +// ============================================================================ + +// Conditionally include an element +fn when(condition: Bool, element: Html): Html = + if condition then element else Empty + +// Conditionally apply attributes +fn attrIf(condition: Bool, attr: Attr): List> = + if condition then [attr] else [] diff --git a/stdlib/lib.lux b/stdlib/lib.lux new file mode 100644 index 0000000..a985563 --- /dev/null +++ b/stdlib/lib.lux @@ -0,0 +1,8 @@ +// Lux Standard Library +// +// This module re-exports the core standard library modules. +// Import with: import stdlib + +// Re-export Html module +pub import html +pub import browser diff --git a/stdlib/lux.toml b/stdlib/lux.toml new file mode 100644 index 0000000..8542bc5 --- /dev/null +++ b/stdlib/lux.toml @@ -0,0 +1,8 @@ +[project] +name = "stdlib" +version = "0.1.0" +description = "Lux Standard Library - Core types and effects for browser development" +license = "MIT" + +[dependencies] +# No dependencies - this is the core library