feat: add stdlib and browser examples
- stdlib/html.lux: Type-safe HTML construction - stdlib/browser.lux: Browser utilities - examples/web/: Counter app with DOM manipulation - examples/counter.lux: Simple counter example Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
173
examples/counter.lux
Normal file
173
examples/counter.lux
Normal file
@@ -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<M> =
|
||||
| Element(String, List<Attr<M>>, List<Html<M>>)
|
||||
| Text(String)
|
||||
| Empty
|
||||
|
||||
type Attr<M> =
|
||||
| Class(String)
|
||||
| Id(String)
|
||||
| OnClick(M)
|
||||
|
||||
// Html builder helpers
|
||||
fn div<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("div", attrs, children)
|
||||
|
||||
fn span<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("span", attrs, children)
|
||||
|
||||
fn h1<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("h1", attrs, children)
|
||||
|
||||
fn button<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("button", attrs, children)
|
||||
|
||||
fn text<M>(content: String): Html<M> =
|
||||
Text(content)
|
||||
|
||||
fn class<M>(name: String): Attr<M> =
|
||||
Class(name)
|
||||
|
||||
fn onClick<M>(msg: M): Attr<M> =
|
||||
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<Msg> = {
|
||||
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<Msg> = viewCounter(getCount(model))
|
||||
|
||||
// ============================================================================
|
||||
// Debug: Print Html structure
|
||||
// ============================================================================
|
||||
|
||||
fn showAttr(attr: Attr<Msg>): 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<Attr<Msg>>): 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<Html<Msg>>, 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<Msg>, 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 + "</" + tag + ">"
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 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 {}
|
||||
154
examples/web/README.md
Normal file
154
examples/web/README.md
Normal file
@@ -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 = {
|
||||
"<div>" + toString(getCount(model)) + "</div>"
|
||||
}
|
||||
```
|
||||
|
||||
### 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 <file>.lux --target js -o <file>.js`
|
||||
3. Refresh the browser
|
||||
144
examples/web/counter.js
Normal file
144
examples/web/counter.js
Normal file
@@ -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 (((((((((("<div class=\"counter\">" + "<h1>Lux Counter</h1>") + "<div class=\"display\">") + String(count_4)) + "</div>") + "<div class=\"buttons\">") + "<button onclick=\"dispatch('Decrement')\">-</button>") + "<button onclick=\"dispatch('Reset')\">Reset</button>") + "<button onclick=\"dispatch('Increment')\">+</button>") + "</div>") + "</div>");
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
62
examples/web/counter.lux
Normal file
62
examples/web/counter.lux
Normal file
@@ -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)
|
||||
"<div class=\"counter\">" +
|
||||
"<h1>Lux Counter</h1>" +
|
||||
"<div class=\"display\">" + toString(count) + "</div>" +
|
||||
"<div class=\"buttons\">" +
|
||||
"<button onclick=\"dispatch('Decrement')\">-</button>" +
|
||||
"<button onclick=\"dispatch('Reset')\">Reset</button>" +
|
||||
"<button onclick=\"dispatch('Increment')\">+</button>" +
|
||||
"</div>" +
|
||||
"</div>"
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 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)
|
||||
82
examples/web/counter_html.lux
Normal file
82
examples/web/counter_html.lux
Normal file
@@ -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)
|
||||
133
examples/web/index.html
Normal file
133
examples/web/index.html
Normal file
@@ -0,0 +1,133 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Lux Counter Example</title>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.counter {
|
||||
background: white;
|
||||
padding: 2rem 3rem;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #333;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.display {
|
||||
font-size: 4rem;
|
||||
font-weight: bold;
|
||||
color: #667eea;
|
||||
padding: 1rem;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
justify-content: center;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
button {
|
||||
font-size: 1.5rem;
|
||||
padding: 0.5rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.1s, box-shadow 0.1s;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
button:nth-child(1) {
|
||||
background: #ff6b6b;
|
||||
color: white;
|
||||
}
|
||||
|
||||
button:nth-child(2) {
|
||||
background: #868e96;
|
||||
color: white;
|
||||
}
|
||||
|
||||
button:nth-child(3) {
|
||||
background: #51cf66;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: fixed;
|
||||
bottom: 1rem;
|
||||
color: white;
|
||||
opacity: 0.8;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">Loading...</div>
|
||||
|
||||
<div class="footer">
|
||||
Built with <strong>Lux</strong> - A functional language with first-class effects
|
||||
</div>
|
||||
|
||||
<!-- Load compiled Lux code -->
|
||||
<script src="counter.js"></script>
|
||||
|
||||
<!-- Minimal TEA runtime -->
|
||||
<script>
|
||||
// Global state
|
||||
let model = luxInit_lux();
|
||||
|
||||
// Dispatch function called by button onclick handlers
|
||||
function dispatch(msgName) {
|
||||
model = luxUpdate_lux(model, msgName);
|
||||
render();
|
||||
}
|
||||
|
||||
// Render the view
|
||||
function render() {
|
||||
const html = luxView_lux(model);
|
||||
document.getElementById('app').innerHTML = html;
|
||||
}
|
||||
|
||||
// Initial render
|
||||
render();
|
||||
|
||||
console.log('Lux Counter loaded!');
|
||||
console.log('Initial model:', model);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
26
examples/web/serve.sh
Executable file
26
examples/web/serve.sh
Executable file
@@ -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
|
||||
89
stdlib/browser.lux
Normal file
89
stdlib/browser.lux
Normal file
@@ -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<Element>
|
||||
fn querySelector(selector: String): Option<Element>
|
||||
fn querySelectorAll(selector: String): List<Element>
|
||||
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<String>
|
||||
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<M> =
|
||||
| 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<M>(toMsg: fn(Float): M): Sub<M> =
|
||||
OnAnimationFrame(toMsg)
|
||||
|
||||
fn onResize<M>(toMsg: fn(Int, Int): M): Sub<M> =
|
||||
OnResize(toMsg)
|
||||
|
||||
fn onKeyPress<M>(toMsg: fn(String): M): Sub<M> =
|
||||
OnKeyPress(toMsg)
|
||||
|
||||
fn every<M>(ms: Int, toMsg: fn(Float): M): Sub<M> =
|
||||
Every(ms, toMsg)
|
||||
|
||||
fn noSub<M>(): Sub<M> =
|
||||
NoSub
|
||||
|
||||
// Combine multiple subscriptions
|
||||
fn batch<M>(subs: List<Sub<M>>): List<Sub<M>> =
|
||||
subs
|
||||
298
stdlib/html.lux
Normal file
298
stdlib/html.lux
Normal file
@@ -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<M> =
|
||||
| Element(String, List<Attr<M>>, List<Html<M>>)
|
||||
| Text(String)
|
||||
| Empty
|
||||
|
||||
// Attributes that can be applied to elements
|
||||
type Attr<M> =
|
||||
| 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<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("div", attrs, children)
|
||||
|
||||
fn span<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("span", attrs, children)
|
||||
|
||||
fn section<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("section", attrs, children)
|
||||
|
||||
fn article<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("article", attrs, children)
|
||||
|
||||
fn header<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("header", attrs, children)
|
||||
|
||||
fn footer<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("footer", attrs, children)
|
||||
|
||||
fn nav<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("nav", attrs, children)
|
||||
|
||||
fn main<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("main", attrs, children)
|
||||
|
||||
fn aside<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("aside", attrs, children)
|
||||
|
||||
// ============================================================================
|
||||
// Element builders - Text elements
|
||||
// ============================================================================
|
||||
|
||||
fn h1<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("h1", attrs, children)
|
||||
|
||||
fn h2<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("h2", attrs, children)
|
||||
|
||||
fn h3<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("h3", attrs, children)
|
||||
|
||||
fn h4<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("h4", attrs, children)
|
||||
|
||||
fn h5<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("h5", attrs, children)
|
||||
|
||||
fn h6<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("h6", attrs, children)
|
||||
|
||||
fn p<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("p", attrs, children)
|
||||
|
||||
fn pre<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("pre", attrs, children)
|
||||
|
||||
fn code<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("code", attrs, children)
|
||||
|
||||
fn blockquote<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("blockquote", attrs, children)
|
||||
|
||||
// ============================================================================
|
||||
// Element builders - Inline elements
|
||||
// ============================================================================
|
||||
|
||||
fn a<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("a", attrs, children)
|
||||
|
||||
fn strong<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("strong", attrs, children)
|
||||
|
||||
fn em<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("em", attrs, children)
|
||||
|
||||
fn small<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("small", attrs, children)
|
||||
|
||||
fn br<M>(): Html<M> =
|
||||
Element("br", [], [])
|
||||
|
||||
fn hr<M>(): Html<M> =
|
||||
Element("hr", [], [])
|
||||
|
||||
// ============================================================================
|
||||
// Element builders - Lists
|
||||
// ============================================================================
|
||||
|
||||
fn ul<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("ul", attrs, children)
|
||||
|
||||
fn ol<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("ol", attrs, children)
|
||||
|
||||
fn li<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("li", attrs, children)
|
||||
|
||||
// ============================================================================
|
||||
// Element builders - Forms
|
||||
// ============================================================================
|
||||
|
||||
fn form<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("form", attrs, children)
|
||||
|
||||
fn input<M>(attrs: List<Attr<M>>): Html<M> =
|
||||
Element("input", attrs, [])
|
||||
|
||||
fn textarea<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("textarea", attrs, children)
|
||||
|
||||
fn button<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("button", attrs, children)
|
||||
|
||||
fn label<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("label", attrs, children)
|
||||
|
||||
fn select<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("select", attrs, children)
|
||||
|
||||
fn option<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("option", attrs, children)
|
||||
|
||||
// ============================================================================
|
||||
// Element builders - Media
|
||||
// ============================================================================
|
||||
|
||||
fn img<M>(attrs: List<Attr<M>>): Html<M> =
|
||||
Element("img", attrs, [])
|
||||
|
||||
fn video<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("video", attrs, children)
|
||||
|
||||
fn audio<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("audio", attrs, children)
|
||||
|
||||
// ============================================================================
|
||||
// Element builders - Tables
|
||||
// ============================================================================
|
||||
|
||||
fn table<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("table", attrs, children)
|
||||
|
||||
fn thead<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("thead", attrs, children)
|
||||
|
||||
fn tbody<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("tbody", attrs, children)
|
||||
|
||||
fn tr<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("tr", attrs, children)
|
||||
|
||||
fn th<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("th", attrs, children)
|
||||
|
||||
fn td<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("td", attrs, children)
|
||||
|
||||
// ============================================================================
|
||||
// Text and empty nodes
|
||||
// ============================================================================
|
||||
|
||||
fn text<M>(content: String): Html<M> =
|
||||
Text(content)
|
||||
|
||||
fn empty<M>(): Html<M> =
|
||||
Empty
|
||||
|
||||
// ============================================================================
|
||||
// Attribute helpers
|
||||
// ============================================================================
|
||||
|
||||
fn class<M>(name: String): Attr<M> =
|
||||
Class(name)
|
||||
|
||||
fn id<M>(name: String): Attr<M> =
|
||||
Id(name)
|
||||
|
||||
fn style<M>(property: String, value: String): Attr<M> =
|
||||
Style(property, value)
|
||||
|
||||
fn href<M>(url: String): Attr<M> =
|
||||
Href(url)
|
||||
|
||||
fn src<M>(url: String): Attr<M> =
|
||||
Src(url)
|
||||
|
||||
fn alt<M>(description: String): Attr<M> =
|
||||
Alt(description)
|
||||
|
||||
fn inputType<M>(t: String): Attr<M> =
|
||||
Type(t)
|
||||
|
||||
fn value<M>(v: String): Attr<M> =
|
||||
Value(v)
|
||||
|
||||
fn placeholder<M>(p: String): Attr<M> =
|
||||
Placeholder(p)
|
||||
|
||||
fn disabled<M>(d: Bool): Attr<M> =
|
||||
Disabled(d)
|
||||
|
||||
fn checked<M>(c: Bool): Attr<M> =
|
||||
Checked(c)
|
||||
|
||||
fn name<M>(n: String): Attr<M> =
|
||||
Name(n)
|
||||
|
||||
fn onClick<M>(msg: M): Attr<M> =
|
||||
OnClick(msg)
|
||||
|
||||
fn onInput<M>(h: fn(String): M): Attr<M> =
|
||||
OnInput(h)
|
||||
|
||||
fn onSubmit<M>(msg: M): Attr<M> =
|
||||
OnSubmit(msg)
|
||||
|
||||
fn onChange<M>(h: fn(String): M): Attr<M> =
|
||||
OnChange(h)
|
||||
|
||||
fn onMouseEnter<M>(msg: M): Attr<M> =
|
||||
OnMouseEnter(msg)
|
||||
|
||||
fn onMouseLeave<M>(msg: M): Attr<M> =
|
||||
OnMouseLeave(msg)
|
||||
|
||||
fn onFocus<M>(msg: M): Attr<M> =
|
||||
OnFocus(msg)
|
||||
|
||||
fn onBlur<M>(msg: M): Attr<M> =
|
||||
OnBlur(msg)
|
||||
|
||||
fn onKeyDown<M>(h: fn(String): M): Attr<M> =
|
||||
OnKeyDown(h)
|
||||
|
||||
fn onKeyUp<M>(h: fn(String): M): Attr<M> =
|
||||
OnKeyUp(h)
|
||||
|
||||
fn data<M>(name: String, value: String): Attr<M> =
|
||||
DataAttr(name, value)
|
||||
|
||||
// ============================================================================
|
||||
// Utility functions
|
||||
// ============================================================================
|
||||
|
||||
// Conditionally include an element
|
||||
fn when<M>(condition: Bool, element: Html<M>): Html<M> =
|
||||
if condition then element else Empty
|
||||
|
||||
// Conditionally apply attributes
|
||||
fn attrIf<M>(condition: Bool, attr: Attr<M>): List<Attr<M>> =
|
||||
if condition then [attr] else []
|
||||
8
stdlib/lib.lux
Normal file
8
stdlib/lib.lux
Normal file
@@ -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
|
||||
8
stdlib/lux.toml
Normal file
8
stdlib/lux.toml
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user