diff --git a/web/.gitignore b/web/.gitignore
new file mode 100644
index 0000000..cf66596
--- /dev/null
+++ b/web/.gitignore
@@ -0,0 +1 @@
+dist/main.js
diff --git a/web/README.md b/web/README.md
new file mode 100644
index 0000000..ce1a014
--- /dev/null
+++ b/web/README.md
@@ -0,0 +1,12 @@
+
+
+# Web framework experiment
+
+Work in progress of a DOM-patching web framework, like Elm.
+
+In the root, you can do:
+
+```sh
+newt src/Todo.newt -o dist/main.js
+esbuild dist/main.js --bundle --servedir=web --outdir=web --alias:fs=./dist/empty.js --watch
+```
diff --git a/web/dist/empty.js b/web/dist/empty.js
new file mode 100644
index 0000000..b1c6ea4
--- /dev/null
+++ b/web/dist/empty.js
@@ -0,0 +1 @@
+export default {}
diff --git a/web/dist/index.html b/web/dist/index.html
new file mode 100644
index 0000000..bafd1bc
--- /dev/null
+++ b/web/dist/index.html
@@ -0,0 +1,11 @@
+
+
+
+ Counter
+
+
+
+
+
+
+
diff --git a/web/dist/spruce.ts b/web/dist/spruce.ts
new file mode 100644
index 0000000..211ccce
--- /dev/null
+++ b/web/dist/spruce.ts
@@ -0,0 +1,126 @@
+// We're going to do the VDom thing, directly off of newt data
+
+type Attrib =
+ | { tag: 0; h1: string; h2: string }
+ | { tag: 1; h1: string; h2: Msg }
+ | { tag: 2; h1: string; h2: (_: string) => Msg };
+
+type List = { tag: 0 } | { tag: 1; h1: A; h2: List };
+
+type VNode =
+ | { tag: 0; h1: string } // TNode
+ | { tag: 1; h1: string; h2: List>; h3: List> };
+
+type FancyElement = Element & { events?: Record > };
+
+// the pfunc will have two dummy arguments in front, and I dunno no node?
+export function runapp(
+ node: Node,
+ init: Model,
+ update: (_: Msg) => (_: Model) => Model,
+ view: (_: Model) => VNode,
+) {
+ function replace(parent: Node, node: Node | undefined, child: Node) {
+ if (node) {
+ parent.insertBefore(node, child);
+ parent.removeChild(node);
+ } else {
+ parent.appendChild(child);
+ }
+ return child;
+ }
+ // patch node, possibly returning a new node
+ function patch(
+ parentNode: Node,
+ node: Node | undefined,
+ vnode: VNode,
+ ): Node {
+ if (vnode.tag == 0) {
+ if (node && node.nodeType == 3) {
+ node.nodeValue = vnode.h1;
+ return node;
+ }
+ return replace(parentNode, node, document.createTextNode(vnode.h1));
+ }
+ let el: FancyElement;
+ if (node instanceof Element && node.tagName.toLowerCase() == vnode.h1) {
+ el = node;
+ } else {
+ el = document.createElement(vnode.h1);
+ }
+ // update node here
+ let has: Record = {};
+ for (let attrs = vnode.h2; attrs.tag == 1; attrs = attrs.h2) {
+ let attr = attrs.h1;
+ if (attr.tag == 0) {
+ let key = attr.h1;
+ has[key] = true;
+ if (key in el)
+ (el as any)[key] = attr.h2
+ else
+ el.setAttribute(key, attr.h2);
+ } else {
+ // onBlah
+ let key = attr.h1.slice(2).toLowerCase();
+ has[key] = true;
+ let events = el.events || (el.events = {});
+ if (!events[key]) el.addEventListener(key, listener);
+ events[key] = attr;
+ }
+ }
+ // remove attrs, not efficient..
+ for (let i = 0; i < el.attributes.length; ) {
+ let attr = el.attributes[i];
+ if (!has[attr.name]) {
+ el.removeAttribute(attr.name);
+ } else {
+ i++;
+ }
+ }
+ if (el.events) {
+ for (let key of Object.keys(el.events)) {
+ if (!has[key]) delete el.events[key];
+ }
+ }
+ let i = 0;
+ for (let kids = vnode.h3; kids.tag == 1; kids = kids.h2) {
+ patch(el, el.childNodes[i++], kids.h1);
+ }
+ while (el.childNodes[i]) el.removeChild(el.childNodes[i]);
+
+ return node == el ? node : replace(parentNode, node, el);
+ }
+ let model = init;
+ let vdom = view(model);
+ let listener = (ev: Event) => {
+ let target = ev.target as FancyElement;
+ if (!target.events) return;
+ const attr = target.events[ev.type]
+ let action
+ if (attr.tag === 2) {
+ // probably need to pass back the event
+ // we'll want to deal with keypress, change, ...
+ let action = attr.h2(ev.target!.value)
+ model = update(action)(model);
+ } else if (attr.tag === 1) {
+ model = update(attr.h2)(model);
+ }
+ vdom = view(model);
+ node = patch(node.parentNode!, node, vdom);
+ };
+ node = patch(node.parentNode!, node, vdom);
+}
+[{
+ "resource": "/Users/dunham/prj/newt/src/Web/spruce.ts",
+ "owner": "typescript",
+ "code": "2349",
+ "severity": 8,
+ "message": "This expression is not callable.\n Not all constituents of type '((_: string) => Msg) | (Msg & Function)' are callable.\n Type 'Msg & Function' has no call signatures.",
+ "source": "ts",
+ "startLineNumber": 101,
+ "startColumn": 16,
+ "endLineNumber": 101,
+ "endColumn": 19,
+ "modelVersionId": 289,
+ "origin": "extHost1"
+}]
diff --git a/web/dist/style.css b/web/dist/style.css
new file mode 100644
index 0000000..4b53a64
--- /dev/null
+++ b/web/dist/style.css
@@ -0,0 +1,13 @@
+.item {
+ display: flex;
+}
+.item > * {
+ vertical-align: middle;
+ padding-right: 10px;
+}
+.item > div {
+ display: inline;
+ width: 100px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
diff --git a/web/src/Prelude.newt b/web/src/Prelude.newt
new file mode 120000
index 0000000..2d44821
--- /dev/null
+++ b/web/src/Prelude.newt
@@ -0,0 +1 @@
+../../src/Prelude.newt
\ No newline at end of file
diff --git a/web/src/Spruce.newt b/web/src/Spruce.newt
new file mode 100644
index 0000000..a3a8134
--- /dev/null
+++ b/web/src/Spruce.newt
@@ -0,0 +1,31 @@
+module Spruce
+
+-- Spruce is not Elm
+
+import Prelude
+
+ptype Element
+pfunc getElementById : String → Element := `(id) => document.getElementById(id)`
+
+-- Make this align with spruce.ts
+data Attr msg = SAttr String String | MAttr String msg | VAttr String (String → msg)
+data VNode msg = TNode String | ENode String (List $ Attr msg) (List $ VNode msg)
+
+text : ∀ msg. String → VNode msg
+text = TNode
+
+tag : ∀ msg. String → List (Attr msg)→ List (VNode msg) → VNode msg
+tag = ENode
+
+tag_ : ∀ msg. String → List (VNode msg)→ VNode msg
+tag_ tag es = ENode tag Nil es
+
+-- TODO better story for import..
+-- also, we'll probably want to expose something with subscriptions / dispatch / ...
+pfunc runApp : ∀ msg model. Element → model → (update : msg → model → model) → (view : model → VNode msg) → Unit :=
+ `(_msg, _model, el, init, update, view) => {
+ require('./spruce').runapp(el,init,update,view);
+ return 0;
+ }`
+
+-- App
diff --git a/web/src/Todo.newt b/web/src/Todo.newt
new file mode 100644
index 0000000..d7dc7ce
--- /dev/null
+++ b/web/src/Todo.newt
@@ -0,0 +1,84 @@
+module Todo
+
+import Prelude
+import Spruce
+
+data Msg
+ = Toggle Nat
+ | Remove Nat
+ | Change String
+
+record Item where
+ checked : Bool
+ text : String
+
+record Model where
+ items : List Item
+ edit : String
+
+ElCon : U
+ElCon = List (Attr Msg) → List (VNode Msg) → VNode Msg
+
+-- Attributes
+
+onChange : Msg → Attr Msg
+onChange = MAttr "onChange"
+
+onClick : Msg → Attr Msg
+onClick action = MAttr "OnClick" action
+
+className : String → Attr Msg
+className v = SAttr "class" v
+
+checkbox : Bool → Nat → VNode Msg
+checkbox checked ix =
+ tag "input" [ SAttr "checked" (ite checked "true" ""), onChange (Toggle ix), SAttr "type" "checkbox" ] []
+
+-- Elements
+
+div : ElCon
+div = tag "div"
+
+button : String → Msg → VNode Msg
+button label action = tag "button" [ onClick action ] [ text label ]
+
+
+itemView : Nat × Item → VNode Msg
+itemView (ix , item) =
+ div [ className "item" ]
+ [ checkbox item.checked ix
+ , div [] [text item.text]
+ , button "x" (Remove ix)
+ ]
+
+view : Model → VNode Msg
+view model =
+ div []
+ [ div [] (map itemView $ enumerate model.items)
+ , tag "input" [ SAttr "value" model.edit, VAttr "onChange" Change ] []
+ ]
+
+update : Msg → Model → Model
+update (Toggle ix) model = { items $= toggle ix } model
+ where
+ toggleItem : Item → Item
+ toggleItem item = { checked := not item.checked } item
+
+ toggle : Nat → List Item → List Item
+ toggle _ Nil = Nil
+ toggle Z (item :: rest) = toggleItem item :: rest
+ toggle (S k) (item :: rest) = item :: toggle k rest
+
+update (Remove ix) model = {items $= delete ix } model
+ where
+ delete : Nat → List Item → List Item
+ delete _ Nil = Nil
+ delete Z (item :: rest) = rest
+ delete (S k) (item :: rest) = item :: delete k rest
+
+update (Change text) model = { items := (snoc model.items $ MkItem False text) } model
+
+main : IO Unit
+main = pure $ runApp (getElementById "main") (MkModel [] "") update view
+
+