+
+---
+
+## Table of contents
+
+- [Features](#features)
+- [Example](#example)
+- [Philosophy](#philosophy)
+- [Installation](#installation)
+- [Where next](#where-next)
+- [Support](#support)
+
+## Features
+
+- A **declarative**, functional API for constructing HTML. No templates, no macros,
+ just Gleam.
+
+- An Erlang and Elm-inspired architecture for **managing state**.
+
+- **Managed side effects** for predictable, testable code.
+
+- Universal components. **Write once, run anywhere**. Elm meets Phoenix LiveView.
+
+- A **batteries-included CLI** that makes scaffolding and building apps a breeze.
+
+- **Server-side rendering** for static HTML templating.
+
+## Example
+
+```gleam
+import gleam/int
+import lustre
+import lustre/element.{text}
+import lustre/element/html.{div, button, p}
+import lustre/event.{on_click}
+
+pub fn main() {
+ let app = lustre.simple(init, update, view)
+ let assert Ok(_) = lustre.start(app, "#app", Nil)
+
+ Nil
+}
+
+fn init(_flags) {
+ 0
+}
+
+type Msg {
+ Incr
+ Decr
+}
+
+fn update(model, msg) {
+ case msg {
+ Incr -> model + 1
+ Decr -> model - 1
+ }
+}
+
+fn view(model) {
+ let count = int.to_string(model)
+
+ div([], [
+ button([on_click(Incr)], [text(" + ")]),
+ p([], [text(count)]),
+ button([on_click(Decr)], [text(" - ")])
+ ])
+}
+```
+
+## Philosophy
+
+Lustre is an _opinionated_ framework for building small-to-medium-sized Web
+applications. Modern frontend development is hard and complex. Some of that
+complexity is necessary, but a lot of it is accidental or comes from having far
+too many options. Lustre has the same design philosophy as Gleam: where possible,
+there should be only one way to do things.
+
+That means shipping with a single state management system out of the box, modelled
+after Elm and Erlang/OTP. Open any Lustre application and you should feel
+right at home.
+
+It also means we encourage simple approaches to constructing views over complex
+ones. Lustre _does_ have a way to create encapsulated stateful components (something
+we sorely missed in Elm) but it shouldn't be the default. Prefer simple functions
+to stateful components.
+
+Where components _are_ necessary, lean into the fact that Lustre components can
+run _anywhere_. Lustre gives you the tools to write components that can run inside
+an existing Lustre application, export them as a standalone Web Component, or run
+them on the server with a minimal runtime for patching the DOM. Lustre calls these
+**universal components** and they're written with Gleam's multiple targets in mind.
+
+## Installation
+
+Lustre is published on [Hex](https://hex.pm/packages/lustre)! You can add it to
+your Gleam projects from the command line:
+
+```sh
+gleam add lustre
+```
+
+Lustre also has a companion package containing development tooling that you might
+like to install:
+
+> **Note**: the lustre_dev_tools development server watches your filesystem for
+> changes to your gleam code and can automatically reload the browser. For linux
+> users this requires [inotify-tools]() be installed
+
+```sh
+gleam add --dev lustre_dev_tools
+```
+
+If you're using a different build tool, like Rebar3 or Mix, you can add Lustre
+to your `rebar.config` or `mix.exs` file respectively.
+
+```erlang
+{deps, [
+ {lustre, "4.0.0"}
+]}
+```
+
+```elixir
+defp deps do
+ [
+ {:lustre, "~> 4.0"}
+ ]
+end
+```
+
+## Where next
+
+To get up to speed with Lustre, check out the [quickstart guide](https://hexdocs.pm/lustre/guide/01-quickstart.html).
+If you prefer to see some code, the [examples](https://github.com/lustre-labs/lustre/tree/main/examples)
+directory contains a handful of small applications that demonstrate different
+aspects of the framework.
+
+You can also read through the documentation and API reference on
+[HexDocs](https://hexdocs.pm/lustre).
+
+## Support
+
+Lustre is mostly built by just me, [Hayleigh](https://github.com/hayleigh-dot-dev),
+around two jobs. If you'd like to support my work, you can [sponsor me on GitHub](https://github.com/sponsors/hayleigh-dot-dev).
+
+Contributions are also very welcome! If you've spotted a bug, or would like to
+suggest a feature, please open an issue or a pull request.
diff --git a/packages/lustre/birdie_snapshots/can_compute_a_diff_from_one_render_to_the_next.accepted b/packages/lustre/birdie_snapshots/can_compute_a_diff_from_one_render_to_the_next.accepted
new file mode 100644
index 0000000..4d6e333
--- /dev/null
+++ b/packages/lustre/birdie_snapshots/can_compute_a_diff_from_one_render_to_the_next.accepted
@@ -0,0 +1,5 @@
+---
+version: 1.1.0
+title: Can compute a diff from one render to the next
+---
+[[["0-0-0",{"content":"3"}]],[],[]]
\ No newline at end of file
diff --git a/packages/lustre/birdie_snapshots/can_compute_a_diff_from_one_render_to_the_next_with_fragments.accepted b/packages/lustre/birdie_snapshots/can_compute_a_diff_from_one_render_to_the_next_with_fragments.accepted
new file mode 100644
index 0000000..10e6efc
--- /dev/null
+++ b/packages/lustre/birdie_snapshots/can_compute_a_diff_from_one_render_to_the_next_with_fragments.accepted
@@ -0,0 +1,5 @@
+---
+version: 1.1.0
+title: Can compute a diff from one render to the next with fragments
+---
+[[["0-2-0",{"content":"3"}]],[],[]]
\ No newline at end of file
diff --git a/packages/lustre/birdie_snapshots/can_render_an_application's_initial_state.accepted b/packages/lustre/birdie_snapshots/can_render_an_application's_initial_state.accepted
new file mode 100644
index 0000000..4545fcb
--- /dev/null
+++ b/packages/lustre/birdie_snapshots/can_render_an_application's_initial_state.accepted
@@ -0,0 +1,5 @@
+---
+version: 1.0.1
+title: Can render an application's initial state.
+---
+
0
\ No newline at end of file
diff --git a/packages/lustre/birdie_snapshots/can_render_an_application's_initial_state_when_using_fragments.accepted b/packages/lustre/birdie_snapshots/can_render_an_application's_initial_state_when_using_fragments.accepted
new file mode 100644
index 0000000..d89dc7b
--- /dev/null
+++ b/packages/lustre/birdie_snapshots/can_render_an_application's_initial_state_when_using_fragments.accepted
@@ -0,0 +1,5 @@
+---
+version: 1.1.0
+title: Can render an application's initial state when using fragments
+---
+
start fragment
middle fragment
0
order check, last element
\ No newline at end of file
diff --git a/packages/lustre/birdie_snapshots/can_render_an_application's_state_after_some_updates.accepted b/packages/lustre/birdie_snapshots/can_render_an_application's_state_after_some_updates.accepted
new file mode 100644
index 0000000..0922d44
--- /dev/null
+++ b/packages/lustre/birdie_snapshots/can_render_an_application's_state_after_some_updates.accepted
@@ -0,0 +1,5 @@
+---
+version: 1.0.1
+title: Can render an application's state after some updates.
+---
+
3
\ No newline at end of file
diff --git a/packages/lustre/birdie_snapshots/can_render_static_html.accepted b/packages/lustre/birdie_snapshots/can_render_static_html.accepted
new file mode 100644
index 0000000..907c6a5
--- /dev/null
+++ b/packages/lustre/birdie_snapshots/can_render_static_html.accepted
@@ -0,0 +1,5 @@
+---
+version: 1.0.4
+title: Can render static HTML
+---
+Hello, World!
Hello, World!
\ No newline at end of file
diff --git a/packages/lustre/birdie_snapshots/can_render_static_html_with_unescaped_html.new b/packages/lustre/birdie_snapshots/can_render_static_html_with_unescaped_html.new
new file mode 100644
index 0000000..9ffc59d
--- /dev/null
+++ b/packages/lustre/birdie_snapshots/can_render_static_html_with_unescaped_html.new
@@ -0,0 +1,6 @@
+---
+version: 1.1.2
+title: Can render static HTML with unescaped HTML
+---
+
+
hello!
\ No newline at end of file
diff --git a/packages/lustre/birdie_snapshots/can_safely_escape_dangerous_symbols_in_attributes.accepted b/packages/lustre/birdie_snapshots/can_safely_escape_dangerous_symbols_in_attributes.accepted
new file mode 100644
index 0000000..cbb7f9d
--- /dev/null
+++ b/packages/lustre/birdie_snapshots/can_safely_escape_dangerous_symbols_in_attributes.accepted
@@ -0,0 +1,5 @@
+---
+version: 1.1.2
+title: Can safely escape dangerous symbols in attributes
+---
+
\ No newline at end of file
diff --git a/packages/lustre/examples/01-hello-world/README.md b/packages/lustre/examples/01-hello-world/README.md
new file mode 100644
index 0000000..71df563
--- /dev/null
+++ b/packages/lustre/examples/01-hello-world/README.md
@@ -0,0 +1,67 @@
+![](./header.png)
+
+# 01 Hello World
+
+This hello world example is a tiny example of what you need to put together to
+get a Lustre application running. In later examples we'll touch on server-side
+rendering and Lustre Universal Components but for these first examples we'll
+be looking at rendering on the client _only_.
+
+## Configuring the Gleam project
+
+It's important to remember to add `target = "javascript"` to your `gleam.toml`!
+If you forget to do this you might end up confused when it looks like your project
+is successfully building but you have no JavaScript output!
+
+## Creating a `lustre.element` application
+
+The simplest kind of Lustre application is the `element`. This sets up a static
+application that does not have its own update loop and cannot dynamically render
+any content. Instead, we provide a static Lustre `Element` to render once.
+
+### HTML attributes and inline styles
+
+In Lustre, HTML attributes are modelled as a `List` of attributes. This is a bit
+different from many other frameworks that use an object or record for attributes.
+Lustre takes the list-of-attributes approach for a couple of reasons:
+
+- Gleam doesn't have a way to construct an anonymous record: we'd have to have
+ an infinite number of types to cover every possible varation!
+
+- Working with lists makes it convenient to merge different sets of attributes
+ together (like an element that defines some local attributes and merges them
+ with any passed in as an argument).
+
+In a similar fashion, inline styles are lists of property/value tuples. In this
+example we're setting inline styles for the `width` and `height` properties.
+
+### Why `element.text`?
+
+In frameworks like React, it's enough to just return a `String` if you want to
+render some text. Gleam's type system works a little differently though, a string
+literal isn't compatible with Lustre's `Element` type on its own, so we need to
+wrap any text to render in `element.text`.
+
+You won't see us do it in any of the examples we share, but it's common for folks
+to import `text` and any html elements they're using unqualified to cut down on
+some of the noise:
+
+```gleam
+import lustre/element.{text}
+import lustre/element/html.{div, p}
+...
+```
+
+## Seeing the result
+
+Lustre has a companion package containing development tooling called
+[lustre_dev_tools](https://hexdocs.pm/lustre_dev_tools/). It's already included
+in this and all the other example. You can run `gleam run -m lustre/dev start`
+in any of these examples to start a development server and head over to
+`localhost:1234` to see what it produces.
+
+## Getting help
+
+If you're having trouble with Lustre or not sure what the right way to do
+something is, the best place to get help is the [Gleam Discord server](https://discord.gg/Fm8Pwmy).
+You could also open an issue on the [Lustre GitHub repository](https://github.com/lustre-labs/lustre/issues).
diff --git a/packages/lustre/examples/01-hello-world/gleam.toml b/packages/lustre/examples/01-hello-world/gleam.toml
new file mode 100644
index 0000000..6b9e4db
--- /dev/null
+++ b/packages/lustre/examples/01-hello-world/gleam.toml
@@ -0,0 +1,13 @@
+name = "app"
+version = "1.0.0"
+target = "javascript"
+
+[dependencies]
+gleam_json = "1.0.1"
+gleam_stdlib = "~> 0.36"
+lustre = "~> 4.0"
+lustre_ui = "~> 0.4"
+
+[dev-dependencies]
+gleeunit = "~> 1.0"
+lustre_dev_tools = "~> 1.0"
diff --git a/packages/lustre/examples/01-hello-world/header.png b/packages/lustre/examples/01-hello-world/header.png
new file mode 100644
index 0000000..dd76b40
Binary files /dev/null and b/packages/lustre/examples/01-hello-world/header.png differ
diff --git a/packages/lustre/examples/01-hello-world/index.html b/packages/lustre/examples/01-hello-world/index.html
new file mode 100644
index 0000000..36ddf10
--- /dev/null
+++ b/packages/lustre/examples/01-hello-world/index.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+ 🚧 app
+
+
+
+
+
+
+
+
+
diff --git a/packages/lustre/examples/01-hello-world/manifest.toml b/packages/lustre/examples/01-hello-world/manifest.toml
new file mode 100644
index 0000000..ee55acd
--- /dev/null
+++ b/packages/lustre/examples/01-hello-world/manifest.toml
@@ -0,0 +1,49 @@
+# This file was generated by Gleam
+# You typically do not need to edit this file
+
+packages = [
+ { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" },
+ { name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" },
+ { name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" },
+ { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" },
+ { name = "fs", version = "8.6.1", build_tools = ["rebar3"], requirements = [], otp_app = "fs", source = "hex", outer_checksum = "61EA2BDAEDAE4E2024D0D25C63E44DCCF65622D4402DB4A2DF12868D1546503F" },
+ { name = "gleam_community_ansi", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "FE79E08BF97009729259B6357EC058315B6FBB916FAD1C2FF9355115FEB0D3A4" },
+ { name = "gleam_community_colour", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "795964217EBEDB3DA656F5EB8F67D7AD22872EB95182042D3E7AFEF32D3FD2FE" },
+ { name = "gleam_crypto", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "ADD058DEDE8F0341F1ADE3AAC492A224F15700829D9A3A3F9ADF370F875C51B7" },
+ { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" },
+ { name = "gleam_http", version = "3.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" },
+ { name = "gleam_httpc", version = "2.2.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "CF76C71002DEECF6DC5D9CA83D962728FAE166B57926BE442D827004D3C7DF1B" },
+ { name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" },
+ { name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" },
+ { name = "gleam_package_interface", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_package_interface", source = "hex", outer_checksum = "52A721BCA972C8099BB881195D821AAA64B9F2655BECC102165D5A1097731F01" },
+ { name = "gleam_stdlib", version = "0.38.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "663CF11861179AF415A625307447775C09404E752FF99A24E2057C835319F1BE" },
+ { name = "glearray", version = "0.2.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glearray", source = "hex", outer_checksum = "9C207E05F38D724F464FA921378DB3ABC2B0A2F5821116D8BC8B2CACC68930D5" },
+ { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" },
+ { name = "glint", version = "0.18.1", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "5FB54D7732B4105E4AF4D89A7EE6D5E8CF33DA13A3575D0C6ECE470B97958454" },
+ { name = "glisten", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "glisten", source = "hex", outer_checksum = "CF3A9383E9BA4A8CBAF2F7B799716290D02F2AC34E7A77556B49376B662B9314" },
+ { name = "gramps", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "3CCAA6E081225180D95C79679D383BBF51C8D1FDC1B84DA1DA444F628C373793" },
+ { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" },
+ { name = "logging", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "A996064F04EF6E67F0668FD0ACFB309830B05D0EE3A0C11BBBD2D4464334F792" },
+ { name = "lustre", version = "4.2.6", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib"], otp_app = "lustre", source = "hex", outer_checksum = "9ABD71D63F4B8F362CB824DED2C4CA64895DEFACD8F22B0FF055BF15241B1AE2" },
+ { name = "lustre_dev_tools", version = "1.3.3", build_tools = ["gleam"], requirements = ["argv", "filepath", "fs", "gleam_community_ansi", "gleam_erlang", "gleam_http", "gleam_httpc", "gleam_json", "gleam_otp", "gleam_package_interface", "gleam_stdlib", "glint", "glisten", "mist", "simplifile", "spinner", "term_size", "tom", "wisp"], otp_app = "lustre_dev_tools", source = "hex", outer_checksum = "67B4E62DAD9B8323487AAA697A6F3FA72348B6DEA6674D65D4F7A1407CF377ED" },
+ { name = "lustre_ui", version = "0.6.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "lustre_ui", source = "hex", outer_checksum = "FA1F9E89D89CDD5DF376ED86ABA8A38441CB2E664CD4D402F22A49DA4D7BB56D" },
+ { name = "marceau", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "5188D643C181EE350D8A20A3BDBD63AF7B6C505DE333CFBE05EF642ADD88A59B" },
+ { name = "mist", version = "1.2.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "109B4D64E68C104CC23BB3CC5441ECD479DD7444889DA01113B75C6AF0F0E17B" },
+ { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" },
+ { name = "repeatedly", version = "2.1.1", build_tools = ["gleam"], requirements = [], otp_app = "repeatedly", source = "hex", outer_checksum = "38808C3EC382B0CD981336D5879C24ECB37FCB9C1D1BD128F7A80B0F74404D79" },
+ { name = "simplifile", version = "1.7.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "1D5DFA3A2F9319EC85825F6ED88B8E449F381B0D55A62F5E61424E748E7DDEB0" },
+ { name = "snag", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "54D32E16E33655346AA3E66CBA7E191DE0A8793D2C05284E3EFB90AD2CE92BCC" },
+ { name = "spinner", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_erlang", "gleam_stdlib", "glearray", "repeatedly"], otp_app = "spinner", source = "hex", outer_checksum = "200BA3D4A04D468898E63C0D316E23F526E02514BC46454091975CB5BAE41E8F" },
+ { name = "term_size", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "term_size", source = "hex", outer_checksum = "D00BD2BC8FB3EBB7E6AE076F3F1FF2AC9D5ED1805F004D0896C784D06C6645F1" },
+ { name = "thoas", version = "1.2.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A" },
+ { name = "tom", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "0831C73E45405A2153091226BF98FB485ED16376988602CC01A5FD086B82D577" },
+ { name = "wisp", version = "0.14.0", build_tools = ["gleam"], requirements = ["exception", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "9F5453AF1F9275E6F8707BC815D6A6A9DF41551921B16FBDBA52883773BAE684" },
+]
+
+[requirements]
+gleam_json = { version = "1.0.1" }
+gleam_stdlib = { version = "~> 0.36" }
+gleeunit = { version = "~> 1.0" }
+lustre = { version = "~> 4.0" }
+lustre_dev_tools = { version = "~> 1.0" }
+lustre_ui = { version = "~> 0.4" }
diff --git a/packages/lustre/examples/01-hello-world/src/app.gleam b/packages/lustre/examples/01-hello-world/src/app.gleam
new file mode 100644
index 0000000..57b9492
--- /dev/null
+++ b/packages/lustre/examples/01-hello-world/src/app.gleam
@@ -0,0 +1,20 @@
+import lustre
+import lustre/attribute
+import lustre/element
+import lustre/element/html
+import lustre/ui
+
+pub fn main() {
+ let styles = [#("width", "100vw"), #("height", "100vh"), #("padding", "1rem")]
+ let app =
+ lustre.element(ui.centre(
+ [attribute.style(styles)],
+ html.div([], [
+ html.h1([], [element.text("Hello, world.")]),
+ html.h2([], [element.text("Welcome to Lustre.")]),
+ ]),
+ ))
+ let assert Ok(_) = lustre.start(app, "#app", Nil)
+
+ Nil
+}
diff --git a/packages/lustre/examples/02-interactivity/README.md b/packages/lustre/examples/02-interactivity/README.md
new file mode 100644
index 0000000..37ebde7
--- /dev/null
+++ b/packages/lustre/examples/02-interactivity/README.md
@@ -0,0 +1,149 @@
+![](./header.png)
+
+# 02 Interactivity
+
+In this example we show the basic structure of all Lustre applications with a
+classic counter example.
+
+## The Model-View-Update architecture
+
+All Lustre applications are built around the Model-View-Update (MVU) architecture.
+This is a pattern that's been popularised by the Elm programming language and
+has since been adopted by many other frameworks and languages.
+
+MVU applications are built around three main concepts:
+
+- A `Model` and a function to initialise it.
+- A `Msg` type and a function to update the model based on messages.
+- A `View` function to render the model as a Lustre `Element`.
+
+These three pieces come together to form a self-contained update loop. You produce
+an initial model, render it as HTML, and convert any user interactions into
+messages to handle in the update function.
+
+```text
+ +--------+
+ | |
+ | update |
+ | |
+ +--------+
+ ^ |
+ | |
+ Msg | | Model
+ | |
+ | v
++------+ +------------------------+
+| | Model | |
+| init |------------------------>| Lustre Runtime |
+| | | |
++------+ +------------------------+
+ ^ |
+ | |
+ Msg | | Model
+ | |
+ | v
+ +--------+
+ | |
+ | view |
+ | |
+ +--------+
+```
+
+### Model
+
+The model represents the entire state of your application. For most Lustre
+applications this will be a record, but for this example we're aliasing `Int` to
+our `Model` type to keep things simple.
+
+We also need to write an `init` function that returns the initial state of our
+application. It takes one argument, known as "flags" which is provided when the
+application is first started.
+
+```gleam
+fn init(initial_count: Int) -> Model {
+ case initial_count < 0 {
+ True -> 0
+ False -> initial_count
+ }
+}
+```
+
+Our `init` function takes a starting count, but ensures it cannot be below `0`.
+
+### Update
+
+In many other frameworks, it's common to update state directly in an event handler.
+MVU applications take a different approach: instead of state updates being scattered
+around your codebase, they are handled in a single `update` function.
+
+To achieve this, we define a `Msg` type that represents all the different kinds of
+messages our application can receive. If you're familiar with Erlang this approach
+to state management will be familiar to you. If you're coming from a JavaScript
+background, this approach is most-similar to state management solutions like Redux
+or Vuex.
+
+```gleam
+pub opaque type Msg {
+ Incr
+ Decr
+}
+```
+
+This approach means it is easy to quickly get an idea of all the ways your app
+can change state, and makes it easy to add new state changes over time. By pattern
+matching on an incoming message in our `update` function, we can lean on Gleam's
+_exhaustiveness checking_ to ensure we handle all possible messages.
+
+### View
+
+Because state management is handled in our `update` function, our `view` becomes
+a simple function that takes a model and returns some HTML in the form of a
+Lustre `Element`.
+
+```gleam
+fn view(model: Model) -> Element(Msg) {
+ ...
+}
+```
+
+In Lustre we call _all_ functions that return an `Element` "view functions": there's
+nothing special about the `view` that takes your model.
+
+Folks coming from frameworks like React might notice the absence of components
+with local encapsulated state. Lustre _does_ have components like this, but unlike
+other frameworks these are a fairly advanced use of the library and are typically
+used for larger pieces of UI like an entire form or a table. We'll cover how
+components fit into Lustre in later examples, but for now resist the urge to think
+in terms of "components" and "state" and try to think of your UI as a composition
+of _view functions_.
+
+## Creating a dynamic Lustre application
+
+In the previous example we used the `lustre.element` function to construct a
+static Lustre app. To introduce the basic MVU loop, we can use `lustre.simple`
+instead. From now on we'll see that all the different ways to construct a Lustre
+application all take the same three `init`, `update`, and `view` functions.
+
+Starting a Lustre application with `lustre.start` requires three things:
+
+- A configured `Application` (that's what we used `lustre.element` for).
+
+- A [CSS selector](https://developer.mozilla.org/en-US/docs/Web/API/Document_object_model/Locating_DOM_elements_using_selectors)
+ to locate the DOM node to mount the application on to. As in other frameworks,
+ it's common to use an element with the id "app": for that you'd write the
+ selector as `#app`.
+
+- Some initial data to pass to the application's `init` function. Because applications
+ constructed with `lustre.element` are not dynamic there's nothing meaningful
+ to pass in here, so we just use `Nil`.
+
+Starting an application could fail for a number of reasons, so this function
+returns a `Result`. The `Ok` value is a function you can use to send messages to
+your running application from the outside world: we'll see more of that in later
+examples!
+
+## Getting help
+
+If you're having trouble with Lustre or not sure what the right way to do
+something is, the best place to get help is the [Gleam Discord server](https://discord.gg/Fm8Pwmy).
+You could also open an issue on the [Lustre GitHub repository](https://github.com/lustre-labs/lustre/issues).
diff --git a/packages/lustre/examples/02-interactivity/gleam.toml b/packages/lustre/examples/02-interactivity/gleam.toml
new file mode 100644
index 0000000..6b9e4db
--- /dev/null
+++ b/packages/lustre/examples/02-interactivity/gleam.toml
@@ -0,0 +1,13 @@
+name = "app"
+version = "1.0.0"
+target = "javascript"
+
+[dependencies]
+gleam_json = "1.0.1"
+gleam_stdlib = "~> 0.36"
+lustre = "~> 4.0"
+lustre_ui = "~> 0.4"
+
+[dev-dependencies]
+gleeunit = "~> 1.0"
+lustre_dev_tools = "~> 1.0"
diff --git a/packages/lustre/examples/02-interactivity/header.png b/packages/lustre/examples/02-interactivity/header.png
new file mode 100644
index 0000000..6cca8a2
Binary files /dev/null and b/packages/lustre/examples/02-interactivity/header.png differ
diff --git a/packages/lustre/examples/02-interactivity/index.html b/packages/lustre/examples/02-interactivity/index.html
new file mode 100644
index 0000000..36ddf10
--- /dev/null
+++ b/packages/lustre/examples/02-interactivity/index.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+ 🚧 app
+
+
+
+
+
+
+
+
+
diff --git a/packages/lustre/examples/02-interactivity/manifest.toml b/packages/lustre/examples/02-interactivity/manifest.toml
new file mode 100644
index 0000000..ee55acd
--- /dev/null
+++ b/packages/lustre/examples/02-interactivity/manifest.toml
@@ -0,0 +1,49 @@
+# This file was generated by Gleam
+# You typically do not need to edit this file
+
+packages = [
+ { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" },
+ { name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" },
+ { name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" },
+ { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" },
+ { name = "fs", version = "8.6.1", build_tools = ["rebar3"], requirements = [], otp_app = "fs", source = "hex", outer_checksum = "61EA2BDAEDAE4E2024D0D25C63E44DCCF65622D4402DB4A2DF12868D1546503F" },
+ { name = "gleam_community_ansi", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "FE79E08BF97009729259B6357EC058315B6FBB916FAD1C2FF9355115FEB0D3A4" },
+ { name = "gleam_community_colour", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "795964217EBEDB3DA656F5EB8F67D7AD22872EB95182042D3E7AFEF32D3FD2FE" },
+ { name = "gleam_crypto", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "ADD058DEDE8F0341F1ADE3AAC492A224F15700829D9A3A3F9ADF370F875C51B7" },
+ { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" },
+ { name = "gleam_http", version = "3.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" },
+ { name = "gleam_httpc", version = "2.2.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "CF76C71002DEECF6DC5D9CA83D962728FAE166B57926BE442D827004D3C7DF1B" },
+ { name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" },
+ { name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" },
+ { name = "gleam_package_interface", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_package_interface", source = "hex", outer_checksum = "52A721BCA972C8099BB881195D821AAA64B9F2655BECC102165D5A1097731F01" },
+ { name = "gleam_stdlib", version = "0.38.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "663CF11861179AF415A625307447775C09404E752FF99A24E2057C835319F1BE" },
+ { name = "glearray", version = "0.2.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glearray", source = "hex", outer_checksum = "9C207E05F38D724F464FA921378DB3ABC2B0A2F5821116D8BC8B2CACC68930D5" },
+ { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" },
+ { name = "glint", version = "0.18.1", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "5FB54D7732B4105E4AF4D89A7EE6D5E8CF33DA13A3575D0C6ECE470B97958454" },
+ { name = "glisten", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "glisten", source = "hex", outer_checksum = "CF3A9383E9BA4A8CBAF2F7B799716290D02F2AC34E7A77556B49376B662B9314" },
+ { name = "gramps", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "3CCAA6E081225180D95C79679D383BBF51C8D1FDC1B84DA1DA444F628C373793" },
+ { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" },
+ { name = "logging", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "A996064F04EF6E67F0668FD0ACFB309830B05D0EE3A0C11BBBD2D4464334F792" },
+ { name = "lustre", version = "4.2.6", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib"], otp_app = "lustre", source = "hex", outer_checksum = "9ABD71D63F4B8F362CB824DED2C4CA64895DEFACD8F22B0FF055BF15241B1AE2" },
+ { name = "lustre_dev_tools", version = "1.3.3", build_tools = ["gleam"], requirements = ["argv", "filepath", "fs", "gleam_community_ansi", "gleam_erlang", "gleam_http", "gleam_httpc", "gleam_json", "gleam_otp", "gleam_package_interface", "gleam_stdlib", "glint", "glisten", "mist", "simplifile", "spinner", "term_size", "tom", "wisp"], otp_app = "lustre_dev_tools", source = "hex", outer_checksum = "67B4E62DAD9B8323487AAA697A6F3FA72348B6DEA6674D65D4F7A1407CF377ED" },
+ { name = "lustre_ui", version = "0.6.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "lustre_ui", source = "hex", outer_checksum = "FA1F9E89D89CDD5DF376ED86ABA8A38441CB2E664CD4D402F22A49DA4D7BB56D" },
+ { name = "marceau", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "5188D643C181EE350D8A20A3BDBD63AF7B6C505DE333CFBE05EF642ADD88A59B" },
+ { name = "mist", version = "1.2.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "109B4D64E68C104CC23BB3CC5441ECD479DD7444889DA01113B75C6AF0F0E17B" },
+ { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" },
+ { name = "repeatedly", version = "2.1.1", build_tools = ["gleam"], requirements = [], otp_app = "repeatedly", source = "hex", outer_checksum = "38808C3EC382B0CD981336D5879C24ECB37FCB9C1D1BD128F7A80B0F74404D79" },
+ { name = "simplifile", version = "1.7.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "1D5DFA3A2F9319EC85825F6ED88B8E449F381B0D55A62F5E61424E748E7DDEB0" },
+ { name = "snag", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "54D32E16E33655346AA3E66CBA7E191DE0A8793D2C05284E3EFB90AD2CE92BCC" },
+ { name = "spinner", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_erlang", "gleam_stdlib", "glearray", "repeatedly"], otp_app = "spinner", source = "hex", outer_checksum = "200BA3D4A04D468898E63C0D316E23F526E02514BC46454091975CB5BAE41E8F" },
+ { name = "term_size", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "term_size", source = "hex", outer_checksum = "D00BD2BC8FB3EBB7E6AE076F3F1FF2AC9D5ED1805F004D0896C784D06C6645F1" },
+ { name = "thoas", version = "1.2.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A" },
+ { name = "tom", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "0831C73E45405A2153091226BF98FB485ED16376988602CC01A5FD086B82D577" },
+ { name = "wisp", version = "0.14.0", build_tools = ["gleam"], requirements = ["exception", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "9F5453AF1F9275E6F8707BC815D6A6A9DF41551921B16FBDBA52883773BAE684" },
+]
+
+[requirements]
+gleam_json = { version = "1.0.1" }
+gleam_stdlib = { version = "~> 0.36" }
+gleeunit = { version = "~> 1.0" }
+lustre = { version = "~> 4.0" }
+lustre_dev_tools = { version = "~> 1.0" }
+lustre_ui = { version = "~> 0.4" }
diff --git a/packages/lustre/examples/02-interactivity/src/app.gleam b/packages/lustre/examples/02-interactivity/src/app.gleam
new file mode 100644
index 0000000..6e69048
--- /dev/null
+++ b/packages/lustre/examples/02-interactivity/src/app.gleam
@@ -0,0 +1,60 @@
+import gleam/int
+import lustre
+import lustre/attribute
+import lustre/element.{type Element}
+import lustre/element/html
+import lustre/event
+import lustre/ui
+
+// MAIN ------------------------------------------------------------------------
+
+pub fn main() {
+ let app = lustre.simple(init, update, view)
+ let assert Ok(_) = lustre.start(app, "#app", 0)
+
+ Nil
+}
+
+// MODEL -----------------------------------------------------------------------
+
+type Model =
+ Int
+
+fn init(initial_count: Int) -> Model {
+ case initial_count < 0 {
+ True -> 0
+ False -> initial_count
+ }
+}
+
+// UPDATE ----------------------------------------------------------------------
+
+pub opaque type Msg {
+ Incr
+ Decr
+}
+
+fn update(model: Model, msg: Msg) -> Model {
+ case msg {
+ Incr -> model + 1
+ Decr -> model - 1
+ }
+}
+
+// VIEW ------------------------------------------------------------------------
+
+fn view(model: Model) -> Element(Msg) {
+ let styles = [#("width", "100vw"), #("height", "100vh"), #("padding", "1rem")]
+ let count = int.to_string(model)
+
+ ui.centre(
+ [attribute.style(styles)],
+ ui.stack([], [
+ ui.button([event.on_click(Incr)], [element.text("+")]),
+ html.p([attribute.style([#("text-align", "center")])], [
+ element.text(count),
+ ]),
+ ui.button([event.on_click(Decr)], [element.text("-")]),
+ ]),
+ )
+}
diff --git a/packages/lustre/examples/03-controlled-inputs/README.md b/packages/lustre/examples/03-controlled-inputs/README.md
new file mode 100644
index 0000000..95ca620
--- /dev/null
+++ b/packages/lustre/examples/03-controlled-inputs/README.md
@@ -0,0 +1,70 @@
+![](./header.png)
+
+# 03 Controlled Inputs
+
+The most common way to handle inputs and other state-holding elements is in a
+_controlled_ way. This means your app's model is the source of truth for that
+element's state, and you update that state based on user input or other events.
+
+This example shows what that means in practice. For any controlled input we need
+two things:
+
+- A field in our model (or a function to derive a value from the model) to use
+ as the input's `value` attribute.
+
+- A message variant to handle input events and update the model.
+
+```gleam
+ui.input([
+ // Input's value is fixed to the model's `value` field
+ attribute.value(model.value),
+ // Whenever the input changes, we send a `UserUpdatedMessage` message with the
+ // new value
+ event.on_input(UserUpdatedMessage)
+])
+```
+
+## Why is this beneficial?
+
+Central to Lustre's architecture is the idea that your model is the single source
+of truth for your application's UI. This opens up the door to things like serialising
+program state to load in the future, time-travel debugging, and rehydrating your
+app's state from a server.
+
+It also gives you tighter control of when and how to update your UI in response
+to user input. In this example, we only update the model when the new input
+value is less than 10 characters long.
+
+```gleam
+case msg {
+ UserUpdatedMessage(value) -> {
+ let length = string.length(value)
+
+ case length <= model.max {
+ True -> Model(..model, value: value, length: length)
+ False -> model
+ }
+ }
+
+ ...
+```
+
+## A note on message naming
+
+In our [state management guide](https://hexdocs.pm/lustre/guide/02-state-management.html)
+we touch on the idea of "messages not actions." We think the best way to name your
+messages is following a "Subject Verb Object" pattern: `UserUpdatedMessage` not
+`SetMessage` and so on.
+
+This approach to message naming can feel a cumbersome at first, especially for
+small examples like this. One of Lustre's super powers is that as your app grows
+in size, your `Msg` type becomes a very helpful overview of all the different
+events your app can handle. When they take the form of `Subject Verb Object` it
+gives you an immediate sense of the different things that speak to your app: how
+much is coming from your backend, how much is user input, and so on.
+
+## Getting help
+
+If you're having trouble with Lustre or not sure what the right way to do
+something is, the best place to get help is the [Gleam Discord server](https://discord.gg/Fm8Pwmy).
+You could also open an issue on the [Lustre GitHub repository](https://github.com/lustre-labs/lustre/issues).
diff --git a/packages/lustre/examples/03-controlled-inputs/gleam.toml b/packages/lustre/examples/03-controlled-inputs/gleam.toml
new file mode 100644
index 0000000..6b9e4db
--- /dev/null
+++ b/packages/lustre/examples/03-controlled-inputs/gleam.toml
@@ -0,0 +1,13 @@
+name = "app"
+version = "1.0.0"
+target = "javascript"
+
+[dependencies]
+gleam_json = "1.0.1"
+gleam_stdlib = "~> 0.36"
+lustre = "~> 4.0"
+lustre_ui = "~> 0.4"
+
+[dev-dependencies]
+gleeunit = "~> 1.0"
+lustre_dev_tools = "~> 1.0"
diff --git a/packages/lustre/examples/03-controlled-inputs/header.png b/packages/lustre/examples/03-controlled-inputs/header.png
new file mode 100644
index 0000000..336fb18
Binary files /dev/null and b/packages/lustre/examples/03-controlled-inputs/header.png differ
diff --git a/packages/lustre/examples/03-controlled-inputs/index.html b/packages/lustre/examples/03-controlled-inputs/index.html
new file mode 100644
index 0000000..36ddf10
--- /dev/null
+++ b/packages/lustre/examples/03-controlled-inputs/index.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+ 🚧 app
+
+
+
+
+
+
+
+
+
diff --git a/packages/lustre/examples/03-controlled-inputs/manifest.toml b/packages/lustre/examples/03-controlled-inputs/manifest.toml
new file mode 100644
index 0000000..ee55acd
--- /dev/null
+++ b/packages/lustre/examples/03-controlled-inputs/manifest.toml
@@ -0,0 +1,49 @@
+# This file was generated by Gleam
+# You typically do not need to edit this file
+
+packages = [
+ { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" },
+ { name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" },
+ { name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" },
+ { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" },
+ { name = "fs", version = "8.6.1", build_tools = ["rebar3"], requirements = [], otp_app = "fs", source = "hex", outer_checksum = "61EA2BDAEDAE4E2024D0D25C63E44DCCF65622D4402DB4A2DF12868D1546503F" },
+ { name = "gleam_community_ansi", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "FE79E08BF97009729259B6357EC058315B6FBB916FAD1C2FF9355115FEB0D3A4" },
+ { name = "gleam_community_colour", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "795964217EBEDB3DA656F5EB8F67D7AD22872EB95182042D3E7AFEF32D3FD2FE" },
+ { name = "gleam_crypto", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "ADD058DEDE8F0341F1ADE3AAC492A224F15700829D9A3A3F9ADF370F875C51B7" },
+ { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" },
+ { name = "gleam_http", version = "3.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" },
+ { name = "gleam_httpc", version = "2.2.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "CF76C71002DEECF6DC5D9CA83D962728FAE166B57926BE442D827004D3C7DF1B" },
+ { name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" },
+ { name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" },
+ { name = "gleam_package_interface", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_package_interface", source = "hex", outer_checksum = "52A721BCA972C8099BB881195D821AAA64B9F2655BECC102165D5A1097731F01" },
+ { name = "gleam_stdlib", version = "0.38.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "663CF11861179AF415A625307447775C09404E752FF99A24E2057C835319F1BE" },
+ { name = "glearray", version = "0.2.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glearray", source = "hex", outer_checksum = "9C207E05F38D724F464FA921378DB3ABC2B0A2F5821116D8BC8B2CACC68930D5" },
+ { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" },
+ { name = "glint", version = "0.18.1", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "5FB54D7732B4105E4AF4D89A7EE6D5E8CF33DA13A3575D0C6ECE470B97958454" },
+ { name = "glisten", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "glisten", source = "hex", outer_checksum = "CF3A9383E9BA4A8CBAF2F7B799716290D02F2AC34E7A77556B49376B662B9314" },
+ { name = "gramps", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "3CCAA6E081225180D95C79679D383BBF51C8D1FDC1B84DA1DA444F628C373793" },
+ { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" },
+ { name = "logging", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "A996064F04EF6E67F0668FD0ACFB309830B05D0EE3A0C11BBBD2D4464334F792" },
+ { name = "lustre", version = "4.2.6", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib"], otp_app = "lustre", source = "hex", outer_checksum = "9ABD71D63F4B8F362CB824DED2C4CA64895DEFACD8F22B0FF055BF15241B1AE2" },
+ { name = "lustre_dev_tools", version = "1.3.3", build_tools = ["gleam"], requirements = ["argv", "filepath", "fs", "gleam_community_ansi", "gleam_erlang", "gleam_http", "gleam_httpc", "gleam_json", "gleam_otp", "gleam_package_interface", "gleam_stdlib", "glint", "glisten", "mist", "simplifile", "spinner", "term_size", "tom", "wisp"], otp_app = "lustre_dev_tools", source = "hex", outer_checksum = "67B4E62DAD9B8323487AAA697A6F3FA72348B6DEA6674D65D4F7A1407CF377ED" },
+ { name = "lustre_ui", version = "0.6.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "lustre_ui", source = "hex", outer_checksum = "FA1F9E89D89CDD5DF376ED86ABA8A38441CB2E664CD4D402F22A49DA4D7BB56D" },
+ { name = "marceau", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "5188D643C181EE350D8A20A3BDBD63AF7B6C505DE333CFBE05EF642ADD88A59B" },
+ { name = "mist", version = "1.2.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "109B4D64E68C104CC23BB3CC5441ECD479DD7444889DA01113B75C6AF0F0E17B" },
+ { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" },
+ { name = "repeatedly", version = "2.1.1", build_tools = ["gleam"], requirements = [], otp_app = "repeatedly", source = "hex", outer_checksum = "38808C3EC382B0CD981336D5879C24ECB37FCB9C1D1BD128F7A80B0F74404D79" },
+ { name = "simplifile", version = "1.7.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "1D5DFA3A2F9319EC85825F6ED88B8E449F381B0D55A62F5E61424E748E7DDEB0" },
+ { name = "snag", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "54D32E16E33655346AA3E66CBA7E191DE0A8793D2C05284E3EFB90AD2CE92BCC" },
+ { name = "spinner", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_erlang", "gleam_stdlib", "glearray", "repeatedly"], otp_app = "spinner", source = "hex", outer_checksum = "200BA3D4A04D468898E63C0D316E23F526E02514BC46454091975CB5BAE41E8F" },
+ { name = "term_size", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "term_size", source = "hex", outer_checksum = "D00BD2BC8FB3EBB7E6AE076F3F1FF2AC9D5ED1805F004D0896C784D06C6645F1" },
+ { name = "thoas", version = "1.2.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A" },
+ { name = "tom", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "0831C73E45405A2153091226BF98FB485ED16376988602CC01A5FD086B82D577" },
+ { name = "wisp", version = "0.14.0", build_tools = ["gleam"], requirements = ["exception", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "9F5453AF1F9275E6F8707BC815D6A6A9DF41551921B16FBDBA52883773BAE684" },
+]
+
+[requirements]
+gleam_json = { version = "1.0.1" }
+gleam_stdlib = { version = "~> 0.36" }
+gleeunit = { version = "~> 1.0" }
+lustre = { version = "~> 4.0" }
+lustre_dev_tools = { version = "~> 1.0" }
+lustre_ui = { version = "~> 0.4" }
diff --git a/packages/lustre/examples/03-controlled-inputs/src/app.gleam b/packages/lustre/examples/03-controlled-inputs/src/app.gleam
new file mode 100644
index 0000000..d44e1ae
--- /dev/null
+++ b/packages/lustre/examples/03-controlled-inputs/src/app.gleam
@@ -0,0 +1,71 @@
+import gleam/int
+import gleam/string
+import lustre
+import lustre/attribute
+import lustre/element.{type Element}
+import lustre/event
+import lustre/ui
+import lustre/ui/layout/aside
+
+// MAIN ------------------------------------------------------------------------
+
+pub fn main() {
+ let app = lustre.simple(init, update, view)
+ let assert Ok(_) = lustre.start(app, "#app", Nil)
+}
+
+// MODEL -----------------------------------------------------------------------
+
+type Model {
+ Model(value: String, length: Int, max: Int)
+}
+
+fn init(_flags) -> Model {
+ Model(value: "", length: 0, max: 10)
+}
+
+// UPDATE ----------------------------------------------------------------------
+
+pub opaque type Msg {
+ UserUpdatedMessage(value: String)
+ UserResetMessage
+}
+
+fn update(model: Model, msg: Msg) -> Model {
+ case msg {
+ UserUpdatedMessage(value) -> {
+ let length = string.length(value)
+
+ case length <= model.max {
+ True -> Model(..model, value: value, length: length)
+ False -> model
+ }
+ }
+ UserResetMessage -> Model(..model, value: "", length: 0)
+ }
+}
+
+// VIEW ------------------------------------------------------------------------
+
+fn view(model: Model) -> Element(Msg) {
+ let styles = [#("width", "100vw"), #("height", "100vh"), #("padding", "1rem")]
+ let length = int.to_string(model.length)
+ let max = int.to_string(model.max)
+
+ ui.centre(
+ [attribute.style(styles)],
+ ui.aside(
+ [aside.content_first(), aside.align_centre()],
+ ui.field(
+ [],
+ [element.text("Write a message:")],
+ ui.input([
+ attribute.value(model.value),
+ event.on_input(UserUpdatedMessage),
+ ]),
+ [element.text(length <> "/" <> max)],
+ ),
+ ui.button([event.on_click(UserResetMessage)], [element.text("Reset")]),
+ ),
+ )
+}
diff --git a/packages/lustre/examples/04-custom-event-handlers/README.md b/packages/lustre/examples/04-custom-event-handlers/README.md
new file mode 100644
index 0000000..434725a
--- /dev/null
+++ b/packages/lustre/examples/04-custom-event-handlers/README.md
@@ -0,0 +1,76 @@
+![](./header.png)
+
+# 04 Custom Event Handlers
+
+While Lustre's built-in event handlers can cover most of your basic needs, in practice you will often need to provide more advanced functionality. For this, we can reach for the `event.on("eventname", handler)` function to generate attributes that can provide custom event handling.
+
+But first, let's take a look under the hood to see what event handlers actually _do_.
+
+## Decoding Dynamic Data
+
+Lustre is a type-safe framework, but the DOM allows HTML elements to generate events containing values of any arbitrary type and structure. In Gleam, such data is referred to as _dynamic_, and is handled by the `gleam/dynamic` library. `gleam/dynamic` is used for decoding everything from unpredictable JSON input to Lustre's DOM events.
+
+If you peek at [the gleam\dynamic documentation](https://hexdocs.pm/gleam_stdlib/0.17.1/gleam/dynamic/#module-types), you'll quickly see it exports four types:
+
+```gleam
+ pub external type Dynamic
+ // data for which we don't know the type
+
+ pub type DecodeError { ... }
+ // the error returned when unexpected data is encountered
+
+ pub type DecodeErrors = List(DecodeError)
+
+ pub type Decoder(t) = fn(Dynamic) -> Result(t, DecodeErrors)
+ // any function that accepts dynamic data and returns a Result(t, DecodeErrors)
+```
+
+In Lustre, all DOM event values are converted to `Dynamic` values before being passed to their respective handlers. Event handlers accept those `Dynamic` values and return a `Result` of either `Ok(Msg)`, or `DecodeErrors` - the `DecodeError` list.
+
+Therefore, Lustre event handlers are simply an implementation of the `Decoder` function type.
+
+## Writing A Custom Input Handler
+
+In javascript, input event handlers often look something like this:
+
+```js
+function onInput(event) {
+ const input = event.target.value;
+ // do your stuff!
+}
+```
+
+This is very convenient! But it's not type-safe. From the function's perspective, there is no guarantee that _`event`_ is an object with a property named _`target`_ which itself has a property named _`value`_. In a more complex app, we might even pass it a numeric or boolean value on accident. The failure to handle such error conditions leads to many `Uncaught TypeError` crashes.
+
+Here's how we can extract the event's dynamic value in a type-safe way in Lustre:
+
+```gleam
+ let on_input = fn(event: dynamic.Dynamic) -> Result(Msg, dynamic.DecodeErrors) {
+ use target <- result.try(dynamic.field("target", dynamic.dynamic)(event))
+ use value <- result.try(dynamic.field("value", dynamic.string)(target))
+ // do your stuff!
+ Ok(GotInput(value))
+ }
+```
+
+First we extract the `target` field from our `event`, which is expected to be of the type `dynamic.dynamic`. Because the target is itself dynamic, we can again use the dynamic library to extract its `value` field, which is expected to be of type `dynamic.string`. If either of those expectations are not met, the function will return an error, and nothing more will happen.
+
+This is such a common use case that Lustre's `event` module has a helper function for it. Here is a far less verbose version that provides the exact same type-safe guarantees:
+
+```gleam
+ let on_input = fn(event) {
+ use value <- result.try(event.value(event))
+ // do your stuff!
+ Ok(GotInput(value))
+ }
+```
+
+## Make it Loud
+
+In this [example code](./src/app.gleam#L63), we define a custom input handler called `make_it_loud`, which calls `string.uppercase` to make sure all our input is LOUD. Then in our [view function](./src/app.gleam#L79), instead of calling `event.on_input(GotInput)` like we did in the last example, we can just call `event.on("input", make_it_loud)`.
+
+## Getting help
+
+If you're having trouble with Lustre or not sure what the right way to do
+something is, the best place to get help is the [Gleam Discord server](https://discord.gg/Fm8Pwmy).
+You could also open an issue on the [Lustre GitHub repository](https://github.com/lustre-labs/lustre/issues).
diff --git a/packages/lustre/examples/04-custom-event-handlers/gleam.toml b/packages/lustre/examples/04-custom-event-handlers/gleam.toml
new file mode 100644
index 0000000..6b9e4db
--- /dev/null
+++ b/packages/lustre/examples/04-custom-event-handlers/gleam.toml
@@ -0,0 +1,13 @@
+name = "app"
+version = "1.0.0"
+target = "javascript"
+
+[dependencies]
+gleam_json = "1.0.1"
+gleam_stdlib = "~> 0.36"
+lustre = "~> 4.0"
+lustre_ui = "~> 0.4"
+
+[dev-dependencies]
+gleeunit = "~> 1.0"
+lustre_dev_tools = "~> 1.0"
diff --git a/packages/lustre/examples/04-custom-event-handlers/header.png b/packages/lustre/examples/04-custom-event-handlers/header.png
new file mode 100644
index 0000000..e75e2b5
Binary files /dev/null and b/packages/lustre/examples/04-custom-event-handlers/header.png differ
diff --git a/packages/lustre/examples/04-custom-event-handlers/index.html b/packages/lustre/examples/04-custom-event-handlers/index.html
new file mode 100644
index 0000000..36ddf10
--- /dev/null
+++ b/packages/lustre/examples/04-custom-event-handlers/index.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+ 🚧 app
+
+
+
+
+
+
+
+
+
diff --git a/packages/lustre/examples/04-custom-event-handlers/manifest.toml b/packages/lustre/examples/04-custom-event-handlers/manifest.toml
new file mode 100644
index 0000000..ee55acd
--- /dev/null
+++ b/packages/lustre/examples/04-custom-event-handlers/manifest.toml
@@ -0,0 +1,49 @@
+# This file was generated by Gleam
+# You typically do not need to edit this file
+
+packages = [
+ { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" },
+ { name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" },
+ { name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" },
+ { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" },
+ { name = "fs", version = "8.6.1", build_tools = ["rebar3"], requirements = [], otp_app = "fs", source = "hex", outer_checksum = "61EA2BDAEDAE4E2024D0D25C63E44DCCF65622D4402DB4A2DF12868D1546503F" },
+ { name = "gleam_community_ansi", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "FE79E08BF97009729259B6357EC058315B6FBB916FAD1C2FF9355115FEB0D3A4" },
+ { name = "gleam_community_colour", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "795964217EBEDB3DA656F5EB8F67D7AD22872EB95182042D3E7AFEF32D3FD2FE" },
+ { name = "gleam_crypto", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "ADD058DEDE8F0341F1ADE3AAC492A224F15700829D9A3A3F9ADF370F875C51B7" },
+ { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" },
+ { name = "gleam_http", version = "3.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" },
+ { name = "gleam_httpc", version = "2.2.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "CF76C71002DEECF6DC5D9CA83D962728FAE166B57926BE442D827004D3C7DF1B" },
+ { name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" },
+ { name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" },
+ { name = "gleam_package_interface", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_package_interface", source = "hex", outer_checksum = "52A721BCA972C8099BB881195D821AAA64B9F2655BECC102165D5A1097731F01" },
+ { name = "gleam_stdlib", version = "0.38.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "663CF11861179AF415A625307447775C09404E752FF99A24E2057C835319F1BE" },
+ { name = "glearray", version = "0.2.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glearray", source = "hex", outer_checksum = "9C207E05F38D724F464FA921378DB3ABC2B0A2F5821116D8BC8B2CACC68930D5" },
+ { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" },
+ { name = "glint", version = "0.18.1", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "5FB54D7732B4105E4AF4D89A7EE6D5E8CF33DA13A3575D0C6ECE470B97958454" },
+ { name = "glisten", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "glisten", source = "hex", outer_checksum = "CF3A9383E9BA4A8CBAF2F7B799716290D02F2AC34E7A77556B49376B662B9314" },
+ { name = "gramps", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "3CCAA6E081225180D95C79679D383BBF51C8D1FDC1B84DA1DA444F628C373793" },
+ { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" },
+ { name = "logging", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "A996064F04EF6E67F0668FD0ACFB309830B05D0EE3A0C11BBBD2D4464334F792" },
+ { name = "lustre", version = "4.2.6", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib"], otp_app = "lustre", source = "hex", outer_checksum = "9ABD71D63F4B8F362CB824DED2C4CA64895DEFACD8F22B0FF055BF15241B1AE2" },
+ { name = "lustre_dev_tools", version = "1.3.3", build_tools = ["gleam"], requirements = ["argv", "filepath", "fs", "gleam_community_ansi", "gleam_erlang", "gleam_http", "gleam_httpc", "gleam_json", "gleam_otp", "gleam_package_interface", "gleam_stdlib", "glint", "glisten", "mist", "simplifile", "spinner", "term_size", "tom", "wisp"], otp_app = "lustre_dev_tools", source = "hex", outer_checksum = "67B4E62DAD9B8323487AAA697A6F3FA72348B6DEA6674D65D4F7A1407CF377ED" },
+ { name = "lustre_ui", version = "0.6.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "lustre_ui", source = "hex", outer_checksum = "FA1F9E89D89CDD5DF376ED86ABA8A38441CB2E664CD4D402F22A49DA4D7BB56D" },
+ { name = "marceau", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "5188D643C181EE350D8A20A3BDBD63AF7B6C505DE333CFBE05EF642ADD88A59B" },
+ { name = "mist", version = "1.2.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "109B4D64E68C104CC23BB3CC5441ECD479DD7444889DA01113B75C6AF0F0E17B" },
+ { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" },
+ { name = "repeatedly", version = "2.1.1", build_tools = ["gleam"], requirements = [], otp_app = "repeatedly", source = "hex", outer_checksum = "38808C3EC382B0CD981336D5879C24ECB37FCB9C1D1BD128F7A80B0F74404D79" },
+ { name = "simplifile", version = "1.7.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "1D5DFA3A2F9319EC85825F6ED88B8E449F381B0D55A62F5E61424E748E7DDEB0" },
+ { name = "snag", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "54D32E16E33655346AA3E66CBA7E191DE0A8793D2C05284E3EFB90AD2CE92BCC" },
+ { name = "spinner", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_erlang", "gleam_stdlib", "glearray", "repeatedly"], otp_app = "spinner", source = "hex", outer_checksum = "200BA3D4A04D468898E63C0D316E23F526E02514BC46454091975CB5BAE41E8F" },
+ { name = "term_size", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "term_size", source = "hex", outer_checksum = "D00BD2BC8FB3EBB7E6AE076F3F1FF2AC9D5ED1805F004D0896C784D06C6645F1" },
+ { name = "thoas", version = "1.2.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A" },
+ { name = "tom", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "0831C73E45405A2153091226BF98FB485ED16376988602CC01A5FD086B82D577" },
+ { name = "wisp", version = "0.14.0", build_tools = ["gleam"], requirements = ["exception", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "9F5453AF1F9275E6F8707BC815D6A6A9DF41551921B16FBDBA52883773BAE684" },
+]
+
+[requirements]
+gleam_json = { version = "1.0.1" }
+gleam_stdlib = { version = "~> 0.36" }
+gleeunit = { version = "~> 1.0" }
+lustre = { version = "~> 4.0" }
+lustre_dev_tools = { version = "~> 1.0" }
+lustre_ui = { version = "~> 0.4" }
diff --git a/packages/lustre/examples/04-custom-event-handlers/src/app.gleam b/packages/lustre/examples/04-custom-event-handlers/src/app.gleam
new file mode 100644
index 0000000..725b04b
--- /dev/null
+++ b/packages/lustre/examples/04-custom-event-handlers/src/app.gleam
@@ -0,0 +1,77 @@
+import gleam/dynamic
+import gleam/int
+import gleam/result
+import gleam/string
+import lustre
+import lustre/attribute
+import lustre/element.{type Element}
+import lustre/event
+import lustre/ui
+import lustre/ui/layout/aside
+
+// MAIN ------------------------------------------------------------------------
+
+pub fn main() {
+ let app = lustre.simple(init, update, view)
+ let assert Ok(_) = lustre.start(app, "#app", Nil)
+}
+
+// MODEL -----------------------------------------------------------------------
+
+type Model {
+ Model(value: String, length: Int, max: Int)
+}
+
+fn init(_flags) -> Model {
+ Model(value: "", length: 0, max: 10)
+}
+
+// UPDATE ----------------------------------------------------------------------
+
+pub opaque type Msg {
+ UserUpdatedMessage(value: String)
+ UserResetMessage
+}
+
+fn update(model: Model, msg: Msg) -> Model {
+ case msg {
+ UserUpdatedMessage(value) -> {
+ let length = string.length(value)
+ case length <= model.max {
+ True -> Model(..model, value: value, length: length)
+ False -> model
+ }
+ }
+ UserResetMessage -> Model(..model, value: "", length: 0)
+ }
+}
+
+// VIEW ------------------------------------------------------------------------
+
+fn view(model: Model) -> Element(Msg) {
+ let styles = [#("width", "100vw"), #("height", "100vh"), #("padding", "1rem")]
+ let length = int.to_string(model.length)
+ let max = int.to_string(model.max)
+ let make_it_loud = fn(event) -> Result(Msg, List(dynamic.DecodeError)) {
+ use target <- result.try(dynamic.field("target", dynamic.dynamic)(event))
+ use value <- result.try(dynamic.field("value", dynamic.string)(target))
+
+ let loud = string.uppercase(value)
+
+ Ok(UserUpdatedMessage(loud))
+ }
+
+ ui.centre(
+ [attribute.style(styles)],
+ ui.aside(
+ [aside.content_first(), aside.align_centre()],
+ ui.field(
+ [],
+ [element.text("Write a LOUD message:")],
+ ui.input([attribute.value(model.value), event.on("input", make_it_loud)]),
+ [element.text(length <> "/" <> max)],
+ ),
+ ui.button([event.on_click(UserResetMessage)], [element.text("Reset")]),
+ ),
+ )
+}
diff --git a/packages/lustre/examples/05-http-requests/README.md b/packages/lustre/examples/05-http-requests/README.md
new file mode 100644
index 0000000..5fb95eb
--- /dev/null
+++ b/packages/lustre/examples/05-http-requests/README.md
@@ -0,0 +1,137 @@
+![](./header.png)
+
+# 05 HTTP Requests
+
+In the previous examples, we've seen Lustre applications constructed with the
+[`lustre.simple`](https://hexdocs.pm/lustre/lustre.html#simple) constructor.
+These kinds of applications are great for introducing the Model-View-Update (MVU)
+pattern, but for most real-world applications we'll need a way to talk to the
+outside world.
+
+Lustre's runtime includes _managed effects_, which allow us to perform side effects
+like HTTP requests and communicate the results back to our application's `update`
+function. To learn more about Lustre's effect system and why it's useful, check
+out the [side effects guide](https://hexdocs.pm/lustre/guide/03-side-effects.html),
+or the docs for the [`lustre/effect` module](https://hexdocs.pm/lustre/lustre/effect.html).
+
+This example is a practical look at what effects mean in Lustre, and we'll look
+at how to send HTTP requests in a Lustre application: a pretty important thing to
+know!
+
+## Moving on from `lustre.simple`
+
+From this example onwards, we will use a new application constructor:
+[`lustre.application`](https://hexdocs.pm/lustre/lustre.html#application). Full Lustre
+applications have the ability to communicate to the runtime. Let's compare the type
+of both the `simple` and `application` functions:
+
+```gleam
+pub fn simple(
+ init: fn(flags) -> model,
+ update: fn(model, msg) -> model,
+ view: fn(model) -> Element(msg),
+) -> App(flags, model, msg)
+
+pub fn application(
+ init: fn(flags) -> #(model, Effect(msg)),
+ update: fn(model, msg) -> #(model, Effect(msg)),
+ view: fn(model) -> Element(msg),
+) -> App(flags, model, msg)
+```
+
+All that's changed is the return type of our `init` and `update` functions. Instead
+of returning just a new model, they now return a tuple containing both a model and
+any side effects we want the runtime to perform.
+
+You'll notice that running a Lustre app with side effects _changes the signature_
+of our [`init`](src/app.gleam#L43) and [`update`](src/app.gleam#L54) functions.
+Instead of returning just a model, we return a tuple containing both a model an
+an `Effect(Msg)` value. The effect value specifies any further updates we might
+want the Lustre runtime to execute before the next invocation of the `view`
+function.
+
+> **Note**: notice how the type of `view` remains the same. In Lustre, your `view`
+> is always a [_pure function_](https://github.com/lustre-labs/lustre/blob/main/pages/hints/pure-functions.md)
+> that takes a model and returns the UI to be rendered: we never perform side effects
+> in the `view` function itself.
+
+## HTTP requests as side effects
+
+The community library [`lustre_http`](https://hexdocs.pm/lustre_http/) gives us
+a way to model HTTP requests as Lustre `Effect`s. Crucially, when we call
+`lustre_http.get` we are _not_ performing the request! We're constructing a
+description of the side effect that we can hand off to the Lustre runtime to
+perform.
+
+```gleam
+fn get_quote() -> Effect(Msg) {
+ let url = "https://api.quotable.io/random"
+ let decoder =
+ dynamic.decode2(
+ Quote,
+ dynamic.field("author", dynamic.string),
+ dynamic.field("content", dynamic.string),
+ )
+
+ lustre_http.get(url, lustre_http.expect_json(decoder, ApiUpdatedQuote))
+}
+```
+
+To construct HTTP requests, we need a few different things:
+
+- The `url` to send the request to.
+
+- A description of what we _expect_ the result to be. There are a few options:
+ `expect_anything`, `expect_text`, `expect_json`. In this example we say we're
+ expecting a JSON response and provide a decoder.
+
+- Along with what we expect the response to be, we also need to provide a way
+ to turn that response into a `Msg` value that our `update` function can handle.
+
+The same applies for post requests too, but there you also need to provide the
+JSON body of the request.
+
+## Tying it together
+
+We now have a function that can create an `Effect` for us, but we need to hand it
+to the runtime to be executed. The only way we can do that is by returning it from
+our `update` (or `init`) function! We attach an event listener on a button, and
+when the user clicks that button we'll return the `Effect` we want to perform as
+the second element of a tuple:
+
+```gleam
+fn view(model: Model) -> Element(Msg) {
+ ui.centre([],
+ ui.button([event.on_click(UserClickedRefresh)], [
+ element.text("New quote"),
+ ]),
+ )
+}
+
+fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
+ case msg {
+ UserClickedRefresh -> #(model, get_quote())
+ ...
+ }
+}
+```
+
+Of course, we need to handle responses from the quote API in our `update` function
+too. When there are no side effects we want the runtime to perform for us, we need
+to call `effect.none()`:
+
+```gleam
+fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
+ case msg {
+ ...
+ ApiUpdatedQuote(Ok(quote)) -> #(Model(quote: Some(quote)), effect.none())
+ ApiUpdatedQuote(Error(_)) -> #(model, effect.none())
+ }
+}
+```
+
+## Getting help
+
+If you're having trouble with Lustre or not sure what the right way to do
+something is, the best place to get help is the [Gleam Discord server](https://discord.gg/Fm8Pwmy).
+You could also open an issue on the [Lustre GitHub repository](https://github.com/lustre-labs/lustre/issues).
diff --git a/packages/lustre/examples/05-http-requests/gleam.toml b/packages/lustre/examples/05-http-requests/gleam.toml
new file mode 100644
index 0000000..518d327
--- /dev/null
+++ b/packages/lustre/examples/05-http-requests/gleam.toml
@@ -0,0 +1,14 @@
+name = "app"
+version = "1.0.0"
+target = "javascript"
+
+[dependencies]
+gleam_json = "1.0.1"
+gleam_stdlib = "~> 0.36"
+lustre = "~> 4.0"
+lustre_ui = "~> 0.4"
+lustre_http = "~> 0.5.2"
+
+[dev-dependencies]
+gleeunit = "~> 1.0"
+lustre_dev_tools = "~> 1.0"
diff --git a/packages/lustre/examples/05-http-requests/header.png b/packages/lustre/examples/05-http-requests/header.png
new file mode 100644
index 0000000..f08fb17
Binary files /dev/null and b/packages/lustre/examples/05-http-requests/header.png differ
diff --git a/packages/lustre/examples/05-http-requests/index.html b/packages/lustre/examples/05-http-requests/index.html
new file mode 100644
index 0000000..36ddf10
--- /dev/null
+++ b/packages/lustre/examples/05-http-requests/index.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+ 🚧 app
+
+
+
+
+
+
+
+
+
diff --git a/packages/lustre/examples/05-http-requests/manifest.toml b/packages/lustre/examples/05-http-requests/manifest.toml
new file mode 100644
index 0000000..68abb49
--- /dev/null
+++ b/packages/lustre/examples/05-http-requests/manifest.toml
@@ -0,0 +1,53 @@
+# This file was generated by Gleam
+# You typically do not need to edit this file
+
+packages = [
+ { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" },
+ { name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" },
+ { name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" },
+ { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" },
+ { name = "fs", version = "8.6.1", build_tools = ["rebar3"], requirements = [], otp_app = "fs", source = "hex", outer_checksum = "61EA2BDAEDAE4E2024D0D25C63E44DCCF65622D4402DB4A2DF12868D1546503F" },
+ { name = "gleam_community_ansi", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "FE79E08BF97009729259B6357EC058315B6FBB916FAD1C2FF9355115FEB0D3A4" },
+ { name = "gleam_community_colour", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "795964217EBEDB3DA656F5EB8F67D7AD22872EB95182042D3E7AFEF32D3FD2FE" },
+ { name = "gleam_crypto", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "ADD058DEDE8F0341F1ADE3AAC492A224F15700829D9A3A3F9ADF370F875C51B7" },
+ { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" },
+ { name = "gleam_fetch", version = "0.4.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_javascript", "gleam_stdlib"], otp_app = "gleam_fetch", source = "hex", outer_checksum = "7446410A44A1D1328F5BC1FF4FC9CBD1570479EA69349237B3F82E34521CCC10" },
+ { name = "gleam_http", version = "3.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" },
+ { name = "gleam_httpc", version = "2.2.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "CF76C71002DEECF6DC5D9CA83D962728FAE166B57926BE442D827004D3C7DF1B" },
+ { name = "gleam_javascript", version = "0.8.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "14D5B7E1A70681E0776BF0A0357F575B822167960C844D3D3FA114D3A75F05A8" },
+ { name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" },
+ { name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" },
+ { name = "gleam_package_interface", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_package_interface", source = "hex", outer_checksum = "52A721BCA972C8099BB881195D821AAA64B9F2655BECC102165D5A1097731F01" },
+ { name = "gleam_stdlib", version = "0.38.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "663CF11861179AF415A625307447775C09404E752FF99A24E2057C835319F1BE" },
+ { name = "glearray", version = "0.2.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glearray", source = "hex", outer_checksum = "9C207E05F38D724F464FA921378DB3ABC2B0A2F5821116D8BC8B2CACC68930D5" },
+ { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" },
+ { name = "glint", version = "0.18.1", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "5FB54D7732B4105E4AF4D89A7EE6D5E8CF33DA13A3575D0C6ECE470B97958454" },
+ { name = "glisten", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "glisten", source = "hex", outer_checksum = "CF3A9383E9BA4A8CBAF2F7B799716290D02F2AC34E7A77556B49376B662B9314" },
+ { name = "gramps", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "3CCAA6E081225180D95C79679D383BBF51C8D1FDC1B84DA1DA444F628C373793" },
+ { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" },
+ { name = "logging", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "A996064F04EF6E67F0668FD0ACFB309830B05D0EE3A0C11BBBD2D4464334F792" },
+ { name = "lustre", version = "4.2.6", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib"], otp_app = "lustre", source = "hex", outer_checksum = "9ABD71D63F4B8F362CB824DED2C4CA64895DEFACD8F22B0FF055BF15241B1AE2" },
+ { name = "lustre_dev_tools", version = "1.3.3", build_tools = ["gleam"], requirements = ["argv", "filepath", "fs", "gleam_community_ansi", "gleam_erlang", "gleam_http", "gleam_httpc", "gleam_json", "gleam_otp", "gleam_package_interface", "gleam_stdlib", "glint", "glisten", "mist", "simplifile", "spinner", "term_size", "tom", "wisp"], otp_app = "lustre_dev_tools", source = "hex", outer_checksum = "67B4E62DAD9B8323487AAA697A6F3FA72348B6DEA6674D65D4F7A1407CF377ED" },
+ { name = "lustre_http", version = "0.5.2", build_tools = ["gleam"], requirements = ["gleam_fetch", "gleam_http", "gleam_javascript", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "lustre_http", source = "hex", outer_checksum = "FB0478CBFA6B16DBE8ECA326DAE2EC15645E04900595EF2C4F039ABFA0512ABA" },
+ { name = "lustre_ui", version = "0.6.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "lustre_ui", source = "hex", outer_checksum = "FA1F9E89D89CDD5DF376ED86ABA8A38441CB2E664CD4D402F22A49DA4D7BB56D" },
+ { name = "marceau", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "5188D643C181EE350D8A20A3BDBD63AF7B6C505DE333CFBE05EF642ADD88A59B" },
+ { name = "mist", version = "1.2.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "109B4D64E68C104CC23BB3CC5441ECD479DD7444889DA01113B75C6AF0F0E17B" },
+ { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" },
+ { name = "repeatedly", version = "2.1.1", build_tools = ["gleam"], requirements = [], otp_app = "repeatedly", source = "hex", outer_checksum = "38808C3EC382B0CD981336D5879C24ECB37FCB9C1D1BD128F7A80B0F74404D79" },
+ { name = "simplifile", version = "1.7.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "1D5DFA3A2F9319EC85825F6ED88B8E449F381B0D55A62F5E61424E748E7DDEB0" },
+ { name = "snag", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "54D32E16E33655346AA3E66CBA7E191DE0A8793D2C05284E3EFB90AD2CE92BCC" },
+ { name = "spinner", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_erlang", "gleam_stdlib", "glearray", "repeatedly"], otp_app = "spinner", source = "hex", outer_checksum = "200BA3D4A04D468898E63C0D316E23F526E02514BC46454091975CB5BAE41E8F" },
+ { name = "term_size", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "term_size", source = "hex", outer_checksum = "D00BD2BC8FB3EBB7E6AE076F3F1FF2AC9D5ED1805F004D0896C784D06C6645F1" },
+ { name = "thoas", version = "1.2.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A" },
+ { name = "tom", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "0831C73E45405A2153091226BF98FB485ED16376988602CC01A5FD086B82D577" },
+ { name = "wisp", version = "0.14.0", build_tools = ["gleam"], requirements = ["exception", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "9F5453AF1F9275E6F8707BC815D6A6A9DF41551921B16FBDBA52883773BAE684" },
+]
+
+[requirements]
+gleam_json = { version = "1.0.1" }
+gleam_stdlib = { version = "~> 0.36" }
+gleeunit = { version = "~> 1.0" }
+lustre = { version = "~> 4.0" }
+lustre_dev_tools = { version = "~> 1.0" }
+lustre_http = { version = "~> 0.5.2" }
+lustre_ui = { version = "~> 0.4" }
diff --git a/packages/lustre/examples/05-http-requests/src/app.gleam b/packages/lustre/examples/05-http-requests/src/app.gleam
new file mode 100644
index 0000000..acd5fcc
--- /dev/null
+++ b/packages/lustre/examples/05-http-requests/src/app.gleam
@@ -0,0 +1,93 @@
+import gleam/dynamic
+import gleam/option.{type Option, None, Some}
+import lustre
+import lustre/attribute
+import lustre/effect.{type Effect}
+import lustre/element.{type Element}
+import lustre/element/html
+import lustre/event
+
+// Lustre_http is a community package that provides a simple API for making
+// HTTP requests from your update function. You can find the docs for the package
+// here: https://hexdocs.pm/lustre_http/index.html
+import lustre/ui
+import lustre/ui/layout/aside
+import lustre_http.{type HttpError}
+
+// MAIN ------------------------------------------------------------------------
+
+pub fn main() {
+ let app = lustre.application(init, update, view)
+ let assert Ok(_) = lustre.start(app, "#app", Nil)
+}
+
+// MODEL -----------------------------------------------------------------------
+
+type Model {
+ Model(quote: Option(Quote))
+}
+
+type Quote {
+ Quote(author: String, content: String)
+}
+
+fn init(_flags) -> #(Model, Effect(Msg)) {
+ #(Model(quote: None), effect.none())
+}
+
+// UPDATE ----------------------------------------------------------------------
+
+pub opaque type Msg {
+ UserClickedRefresh
+ ApiUpdatedQuote(Result(Quote, HttpError))
+}
+
+fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
+ case msg {
+ UserClickedRefresh -> #(model, get_quote())
+ ApiUpdatedQuote(Ok(quote)) -> #(Model(quote: Some(quote)), effect.none())
+ ApiUpdatedQuote(Error(_)) -> #(model, effect.none())
+ }
+}
+
+fn get_quote() -> Effect(Msg) {
+ let url = "https://api.quotable.io/random"
+ let decoder =
+ dynamic.decode2(
+ Quote,
+ dynamic.field("author", dynamic.string),
+ dynamic.field("content", dynamic.string),
+ )
+
+ lustre_http.get(url, lustre_http.expect_json(decoder, ApiUpdatedQuote))
+}
+
+// VIEW ------------------------------------------------------------------------
+
+fn view(model: Model) -> Element(Msg) {
+ let styles = [#("width", "100vw"), #("height", "100vh"), #("padding", "1rem")]
+
+ ui.centre(
+ [attribute.style(styles)],
+ ui.aside(
+ [aside.min_width(70), attribute.style([#("width", "60ch")])],
+ view_quote(model.quote),
+ ui.button([event.on_click(UserClickedRefresh)], [
+ element.text("New quote"),
+ ]),
+ ),
+ )
+}
+
+fn view_quote(quote: Option(Quote)) -> Element(msg) {
+ case quote {
+ Some(quote) ->
+ ui.stack([], [
+ element.text(quote.author <> " once said..."),
+ html.p([attribute.style([#("font-style", "italic")])], [
+ element.text(quote.content),
+ ]),
+ ])
+ None -> html.p([], [element.text("Click the button to get a quote!")])
+ }
+}
diff --git a/packages/lustre/examples/06-custom-effects/README.md b/packages/lustre/examples/06-custom-effects/README.md
new file mode 100644
index 0000000..0af61bd
--- /dev/null
+++ b/packages/lustre/examples/06-custom-effects/README.md
@@ -0,0 +1,88 @@
+![](./header.png)
+
+# 06 Custom Effects
+
+In the last example, we showed how to use effects provided by `lustre_http`. In
+this example we'll see how to create effects of our own using Lustre's
+[`effect.from`](https://hexdocs.pm/lustre/lustre/effect.html#from)
+function.
+
+Since we use effects to communicate with _external_ systems (like the browser or
+the Erlang VM) you'll find creating custom effects usually involves Gleam's
+[external functions](https://tour.gleam.run/everything/#advanced-features-externals).
+So be sure to read up on that!
+
+> Gleam externals are part of its "foreign function interface", which is why
+> you'll typically see files with `ffi` in the name - like
+> [`app.ffi.mjs`](./src/app.ffi.mjs).
+
+## Accessing Browser Storage
+
+In this example, the external system we want to interact with is the browser's
+[local storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage).
+This way, we can write a message into the text input and it will still be there
+when we refresh the page. Handy!
+
+The `view`, `update` and `init` functions should look pretty familiar by now, so
+let's focus on the functions that generate our custom effects.
+
+```rust
+fn read_localstorage(key: String) -> Effect(Msg) {
+ effect.from(fn(dispatch) {
+ do_read_localstorage(key)
+ |> CacheUpdatedMessage
+ |> dispatch
+ })
+}
+```
+
+We use `effect.from` by passing it a custom function that describes the effect
+we want the Lustre runtime to perform. Our custom function will receive a
+`dispatch` function as its only parameter - we can use that to send new messages
+back to our `update` function if we want to.
+
+In this case, we read from local storage, pipe its value into the
+`CacheUpdatedMessage` constructor, and pipe that to the `dispatch` function so
+our `update` messsage can handle it.
+
+```rust
+fn write_localstorage(key: String, value: String) -> Effect(msg) {
+ effect.from(fn(_) {
+ do_write_localstorage(key, value)
+ })
+}
+```
+
+Here, our effect is simpler. We tell the gleam compiler we don't need to use the
+`dispatch` function by replacing it with a [discard
+pattern](https://tour.gleam.run/everything/#basics-discard-patterns). Then we
+write to local storage, and no more work needs to be done.
+
+You may be wondering, why bother using an effect if we aren't also going to update
+our program with the result? Why not just fire off `do_write_localstorage` directly
+from the `update` function or a custom event handler?
+
+Using effects has many benefits! It lets us implement our `update` and `view`
+functions as [pure functions](https://github.com/lustre-labs/lustre/blob/main/pages/hints/pure-functions.md).
+This makes them easier to test, allows for time-travel debugging, and even opens
+the door to easily porting them to [server components](https://hexdocs.pm/lustre/lustre/server_component.html).
+
+## Another note on message naming
+
+In our [controlled inputs
+example](https://github.com/lustre-labs/lustre/tree/main/examples/03-controlled-inputs)
+we touched on the idea of naming messages in a "Subject Verb Object" pattern.
+This example neatly shows the benefits of taking such an approach once different
+"things" start talking to your application.
+
+It would be easy to have a single `SetMessage` variant that both the user input
+and local storage lookup use to update the model, but doing so might encourage
+us to conceal the fact that the local storage lookup can fail and makes it
+harder to see what things our app deals with.
+
+## Getting help
+
+If you're having trouble with Lustre or not sure what the right way to do
+something is, the best place to get help is the [Gleam Discord
+server](https://discord.gg/Fm8Pwmy). You could also open an issue on the [Lustre
+GitHub repository](https://github.com/lustre-labs/lustre/issues).
diff --git a/packages/lustre/examples/06-custom-effects/gleam.toml b/packages/lustre/examples/06-custom-effects/gleam.toml
new file mode 100644
index 0000000..1066817
--- /dev/null
+++ b/packages/lustre/examples/06-custom-effects/gleam.toml
@@ -0,0 +1,14 @@
+name = "app"
+version = "1.0.0"
+target = "javascript"
+
+[dependencies]
+gleam_json = "1.0.1"
+gleam_stdlib = "~> 0.36"
+lustre = "~> 4.0"
+lustre_ui = "~> 0.4"
+lustre_http = "~> 0.5"
+
+[dev-dependencies]
+gleeunit = "~> 1.0"
+lustre_dev_tools = "~> 1.0"
diff --git a/packages/lustre/examples/06-custom-effects/header.png b/packages/lustre/examples/06-custom-effects/header.png
new file mode 100644
index 0000000..1dbbdb0
Binary files /dev/null and b/packages/lustre/examples/06-custom-effects/header.png differ
diff --git a/packages/lustre/examples/06-custom-effects/index.html b/packages/lustre/examples/06-custom-effects/index.html
new file mode 100644
index 0000000..36ddf10
--- /dev/null
+++ b/packages/lustre/examples/06-custom-effects/index.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+ 🚧 app
+
+
+
+
+
+
+
+
+
diff --git a/packages/lustre/examples/06-custom-effects/manifest.toml b/packages/lustre/examples/06-custom-effects/manifest.toml
new file mode 100644
index 0000000..69c2af4
--- /dev/null
+++ b/packages/lustre/examples/06-custom-effects/manifest.toml
@@ -0,0 +1,53 @@
+# This file was generated by Gleam
+# You typically do not need to edit this file
+
+packages = [
+ { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" },
+ { name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" },
+ { name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" },
+ { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" },
+ { name = "fs", version = "8.6.1", build_tools = ["rebar3"], requirements = [], otp_app = "fs", source = "hex", outer_checksum = "61EA2BDAEDAE4E2024D0D25C63E44DCCF65622D4402DB4A2DF12868D1546503F" },
+ { name = "gleam_community_ansi", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "FE79E08BF97009729259B6357EC058315B6FBB916FAD1C2FF9355115FEB0D3A4" },
+ { name = "gleam_community_colour", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "795964217EBEDB3DA656F5EB8F67D7AD22872EB95182042D3E7AFEF32D3FD2FE" },
+ { name = "gleam_crypto", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "ADD058DEDE8F0341F1ADE3AAC492A224F15700829D9A3A3F9ADF370F875C51B7" },
+ { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" },
+ { name = "gleam_fetch", version = "0.4.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_javascript", "gleam_stdlib"], otp_app = "gleam_fetch", source = "hex", outer_checksum = "7446410A44A1D1328F5BC1FF4FC9CBD1570479EA69349237B3F82E34521CCC10" },
+ { name = "gleam_http", version = "3.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" },
+ { name = "gleam_httpc", version = "2.2.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "CF76C71002DEECF6DC5D9CA83D962728FAE166B57926BE442D827004D3C7DF1B" },
+ { name = "gleam_javascript", version = "0.8.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "14D5B7E1A70681E0776BF0A0357F575B822167960C844D3D3FA114D3A75F05A8" },
+ { name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" },
+ { name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" },
+ { name = "gleam_package_interface", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_package_interface", source = "hex", outer_checksum = "52A721BCA972C8099BB881195D821AAA64B9F2655BECC102165D5A1097731F01" },
+ { name = "gleam_stdlib", version = "0.38.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "663CF11861179AF415A625307447775C09404E752FF99A24E2057C835319F1BE" },
+ { name = "glearray", version = "0.2.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glearray", source = "hex", outer_checksum = "9C207E05F38D724F464FA921378DB3ABC2B0A2F5821116D8BC8B2CACC68930D5" },
+ { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" },
+ { name = "glint", version = "0.18.1", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "5FB54D7732B4105E4AF4D89A7EE6D5E8CF33DA13A3575D0C6ECE470B97958454" },
+ { name = "glisten", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "glisten", source = "hex", outer_checksum = "CF3A9383E9BA4A8CBAF2F7B799716290D02F2AC34E7A77556B49376B662B9314" },
+ { name = "gramps", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "3CCAA6E081225180D95C79679D383BBF51C8D1FDC1B84DA1DA444F628C373793" },
+ { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" },
+ { name = "logging", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "A996064F04EF6E67F0668FD0ACFB309830B05D0EE3A0C11BBBD2D4464334F792" },
+ { name = "lustre", version = "4.2.6", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib"], otp_app = "lustre", source = "hex", outer_checksum = "9ABD71D63F4B8F362CB824DED2C4CA64895DEFACD8F22B0FF055BF15241B1AE2" },
+ { name = "lustre_dev_tools", version = "1.3.3", build_tools = ["gleam"], requirements = ["argv", "filepath", "fs", "gleam_community_ansi", "gleam_erlang", "gleam_http", "gleam_httpc", "gleam_json", "gleam_otp", "gleam_package_interface", "gleam_stdlib", "glint", "glisten", "mist", "simplifile", "spinner", "term_size", "tom", "wisp"], otp_app = "lustre_dev_tools", source = "hex", outer_checksum = "67B4E62DAD9B8323487AAA697A6F3FA72348B6DEA6674D65D4F7A1407CF377ED" },
+ { name = "lustre_http", version = "0.5.2", build_tools = ["gleam"], requirements = ["gleam_fetch", "gleam_http", "gleam_javascript", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "lustre_http", source = "hex", outer_checksum = "FB0478CBFA6B16DBE8ECA326DAE2EC15645E04900595EF2C4F039ABFA0512ABA" },
+ { name = "lustre_ui", version = "0.6.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "lustre_ui", source = "hex", outer_checksum = "FA1F9E89D89CDD5DF376ED86ABA8A38441CB2E664CD4D402F22A49DA4D7BB56D" },
+ { name = "marceau", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "5188D643C181EE350D8A20A3BDBD63AF7B6C505DE333CFBE05EF642ADD88A59B" },
+ { name = "mist", version = "1.2.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "109B4D64E68C104CC23BB3CC5441ECD479DD7444889DA01113B75C6AF0F0E17B" },
+ { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" },
+ { name = "repeatedly", version = "2.1.1", build_tools = ["gleam"], requirements = [], otp_app = "repeatedly", source = "hex", outer_checksum = "38808C3EC382B0CD981336D5879C24ECB37FCB9C1D1BD128F7A80B0F74404D79" },
+ { name = "simplifile", version = "1.7.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "1D5DFA3A2F9319EC85825F6ED88B8E449F381B0D55A62F5E61424E748E7DDEB0" },
+ { name = "snag", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "54D32E16E33655346AA3E66CBA7E191DE0A8793D2C05284E3EFB90AD2CE92BCC" },
+ { name = "spinner", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_erlang", "gleam_stdlib", "glearray", "repeatedly"], otp_app = "spinner", source = "hex", outer_checksum = "200BA3D4A04D468898E63C0D316E23F526E02514BC46454091975CB5BAE41E8F" },
+ { name = "term_size", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "term_size", source = "hex", outer_checksum = "D00BD2BC8FB3EBB7E6AE076F3F1FF2AC9D5ED1805F004D0896C784D06C6645F1" },
+ { name = "thoas", version = "1.2.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A" },
+ { name = "tom", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "0831C73E45405A2153091226BF98FB485ED16376988602CC01A5FD086B82D577" },
+ { name = "wisp", version = "0.14.0", build_tools = ["gleam"], requirements = ["exception", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "9F5453AF1F9275E6F8707BC815D6A6A9DF41551921B16FBDBA52883773BAE684" },
+]
+
+[requirements]
+gleam_json = { version = "1.0.1" }
+gleam_stdlib = { version = "~> 0.36" }
+gleeunit = { version = "~> 1.0" }
+lustre = { version = "~> 4.0" }
+lustre_dev_tools = { version = "~> 1.0" }
+lustre_http = { version = "~> 0.5" }
+lustre_ui = { version = "~> 0.4" }
diff --git a/packages/lustre/examples/06-custom-effects/src/app.ffi.mjs b/packages/lustre/examples/06-custom-effects/src/app.ffi.mjs
new file mode 100644
index 0000000..c401b99
--- /dev/null
+++ b/packages/lustre/examples/06-custom-effects/src/app.ffi.mjs
@@ -0,0 +1,11 @@
+import { Ok, Error } from "./gleam.mjs";
+
+export function read_localstorage(key) {
+ const value = window.localStorage.getItem(key);
+
+ return value ? new Ok(value) : new Error(undefined);
+}
+
+export function write_localstorage(key, value) {
+ window.localStorage.setItem(key, value);
+}
diff --git a/packages/lustre/examples/06-custom-effects/src/app.gleam b/packages/lustre/examples/06-custom-effects/src/app.gleam
new file mode 100644
index 0000000..a6c668e
--- /dev/null
+++ b/packages/lustre/examples/06-custom-effects/src/app.gleam
@@ -0,0 +1,84 @@
+import gleam/option.{type Option, None, Some}
+import lustre
+import lustre/attribute
+import lustre/effect.{type Effect}
+import lustre/element.{type Element}
+import lustre/event
+import lustre/ui
+
+// MAIN ------------------------------------------------------------------------
+
+pub fn main() {
+ let app = lustre.application(init, update, view)
+ let assert Ok(_) = lustre.start(app, "#app", Nil)
+}
+
+// MODEL -----------------------------------------------------------------------
+
+type Model {
+ Model(message: Option(String))
+}
+
+fn init(_flags) -> #(Model, Effect(Msg)) {
+ #(Model(message: None), read_localstorage("message"))
+}
+
+// UPDATE ----------------------------------------------------------------------
+
+pub opaque type Msg {
+ UserUpdatedMessage(String)
+ CacheUpdatedMessage(Result(String, Nil))
+}
+
+fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
+ case msg {
+ UserUpdatedMessage(input) -> #(
+ Model(message: Some(input)),
+ write_localstorage("message", input),
+ )
+ CacheUpdatedMessage(Ok(message)) -> #(
+ Model(message: Some(message)),
+ effect.none(),
+ )
+ CacheUpdatedMessage(Error(_)) -> #(model, effect.none())
+ }
+}
+
+fn read_localstorage(key: String) -> Effect(Msg) {
+ effect.from(fn(dispatch) {
+ do_read_localstorage(key)
+ |> CacheUpdatedMessage
+ |> dispatch
+ })
+}
+
+@external(javascript, "./app.ffi.mjs", "read_localstorage")
+fn do_read_localstorage(_key: String) -> Result(String, Nil) {
+ Error(Nil)
+}
+
+fn write_localstorage(key: String, value: String) -> Effect(msg) {
+ effect.from(fn(_) { do_write_localstorage(key, value) })
+}
+
+@external(javascript, "./app.ffi.mjs", "write_localstorage")
+fn do_write_localstorage(_key: String, _value: String) -> Nil {
+ Nil
+}
+
+// VIEW ------------------------------------------------------------------------
+
+fn view(model: Model) -> Element(Msg) {
+ let styles = [#("width", "100vw"), #("height", "100vh")]
+ let message = option.unwrap(model.message, "")
+
+ ui.centre(
+ [attribute.style(styles)],
+ ui.field(
+ [],
+ [],
+ ui.input([attribute.value(message), event.on_input(UserUpdatedMessage)]),
+ [element.text("Type a message and refresh the page")],
+ ),
+ )
+}
diff --git a/packages/lustre/examples/07-routing/README.md b/packages/lustre/examples/07-routing/README.md
new file mode 100644
index 0000000..217b674
--- /dev/null
+++ b/packages/lustre/examples/07-routing/README.md
@@ -0,0 +1,65 @@
+![](./header.png)
+
+# 07 Routing
+
+In this example, we demonstrate basic routing using the community library [modem](https://hexdocs.pm/modem/). Modem's quickstart docs should be all you should need to get up to speed, so that's the best place to start.
+
+Of course, it's not much fun routing without something to route _to_. This example lets users create new pages on the fly - a guest book for your next house party! Hospitality is very important, and guests will be sure to feel welcome when they see their name in the navigation with a special greeting page just for them.
+
+## Using Modem
+
+Modem uses [custom side effects](../06-custom-effects/) and external Javascript to translate browser click and navigation events into `update` messages for the Lustre runtime. All we need to use it is a [route change handler function](./src/app.gleam#L59) that we can pass to `modem.init` in our app's `init` function.
+
+> _Note:_ See [`modem.advanced`](https://hexdocs.pm/modem/modem.html#advanced) to configure more options.
+
+Inside our `on_route_change` function, we match URI path segment patterns to our routes:
+
+```gleam
+ fn on_route_change(uri: Uri) -> Msg {
+ case uri.path_segments(uri.path) {
+ ["welcome", guest] -> OnRouteChange(WelcomeGuest(guest))
+ _ -> OnRouteChange(Home)
+ }
+ }
+```
+
+In our `update` function, we assign the matched route to `model.current_route`:
+
+```gleam
+fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
+ case msg {
+ OnRouteChange(route) -> #(
+ Model(..model, current_route: route),
+ effect.none(),
+ )
+ ...
+ }
+```
+
+And in our `view` function, we use `model.current_route` to determine what to display to the user:
+
+```gleam
+fn view(model: Model) -> Element(Msg) {
+ let page = case model.current_route {
+ Home -> render_home(model)
+ WelcomeGuest(name) -> render_welcome(model, name)
+ }
+ ...
+}
+```
+
+## Views: They're just functions!
+
+Lustre doesn't provide a traditional HTML or JSX-style templating engine, and this is by design.
+
+Since the `view` portion of this example is a bit more involved than our previous ones have been, it should start to give you more of a feel for how views in Lustre are _just functions_. The layout is a function. Each page view is a function. The nav is a function, and each individual nav _item_ is a function too.
+
+This means we can build up our entire UI using Gleam's functional syntax, benefiting from features like exhaustive pattern matching based on our routes.
+
+Since our views are [pure functions](https://github.com/lustre-labs/lustre/blob/main/pages/hints/pure-functions.md), we know they'll always render reliably.
+
+## Getting help
+
+If you're having trouble with Lustre or are not sure what the right way to do
+something is, the best place to get help is the [Gleam Discord server](https://discord.gg/Fm8Pwmy).
+You could also open an issue on the [Lustre GitHub repository](https://github.com/lustre-labs/lustre/issues).
diff --git a/packages/lustre/examples/07-routing/gleam.toml b/packages/lustre/examples/07-routing/gleam.toml
new file mode 100644
index 0000000..58f37ee
--- /dev/null
+++ b/packages/lustre/examples/07-routing/gleam.toml
@@ -0,0 +1,15 @@
+name = "app"
+version = "1.0.0"
+target = "javascript"
+
+[dependencies]
+gleam_json = "1.0.1"
+gleam_stdlib = "~> 0.36"
+lustre = "~> 4.0"
+lustre_ui = "~> 0.4"
+lustre_http = "~> 0.5.2"
+modem = "~> 1.0"
+
+[dev-dependencies]
+gleeunit = "~> 1.0"
+lustre_dev_tools = "~> 1.1"
diff --git a/packages/lustre/examples/07-routing/header.png b/packages/lustre/examples/07-routing/header.png
new file mode 100644
index 0000000..f903c89
Binary files /dev/null and b/packages/lustre/examples/07-routing/header.png differ
diff --git a/packages/lustre/examples/07-routing/index.html b/packages/lustre/examples/07-routing/index.html
new file mode 100644
index 0000000..36ddf10
--- /dev/null
+++ b/packages/lustre/examples/07-routing/index.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+ 🚧 app
+
+
+
+
+
+
+
+
+
diff --git a/packages/lustre/examples/07-routing/manifest.toml b/packages/lustre/examples/07-routing/manifest.toml
new file mode 100644
index 0000000..a02e147
--- /dev/null
+++ b/packages/lustre/examples/07-routing/manifest.toml
@@ -0,0 +1,55 @@
+# This file was generated by Gleam
+# You typically do not need to edit this file
+
+packages = [
+ { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" },
+ { name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" },
+ { name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" },
+ { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" },
+ { name = "fs", version = "8.6.1", build_tools = ["rebar3"], requirements = [], otp_app = "fs", source = "hex", outer_checksum = "61EA2BDAEDAE4E2024D0D25C63E44DCCF65622D4402DB4A2DF12868D1546503F" },
+ { name = "gleam_community_ansi", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "FE79E08BF97009729259B6357EC058315B6FBB916FAD1C2FF9355115FEB0D3A4" },
+ { name = "gleam_community_colour", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "795964217EBEDB3DA656F5EB8F67D7AD22872EB95182042D3E7AFEF32D3FD2FE" },
+ { name = "gleam_crypto", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "ADD058DEDE8F0341F1ADE3AAC492A224F15700829D9A3A3F9ADF370F875C51B7" },
+ { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" },
+ { name = "gleam_fetch", version = "0.4.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_javascript", "gleam_stdlib"], otp_app = "gleam_fetch", source = "hex", outer_checksum = "7446410A44A1D1328F5BC1FF4FC9CBD1570479EA69349237B3F82E34521CCC10" },
+ { name = "gleam_http", version = "3.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" },
+ { name = "gleam_httpc", version = "2.2.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "CF76C71002DEECF6DC5D9CA83D962728FAE166B57926BE442D827004D3C7DF1B" },
+ { name = "gleam_javascript", version = "0.8.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "14D5B7E1A70681E0776BF0A0357F575B822167960C844D3D3FA114D3A75F05A8" },
+ { name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" },
+ { name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" },
+ { name = "gleam_package_interface", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_package_interface", source = "hex", outer_checksum = "52A721BCA972C8099BB881195D821AAA64B9F2655BECC102165D5A1097731F01" },
+ { name = "gleam_stdlib", version = "0.38.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "663CF11861179AF415A625307447775C09404E752FF99A24E2057C835319F1BE" },
+ { name = "glearray", version = "0.2.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glearray", source = "hex", outer_checksum = "9C207E05F38D724F464FA921378DB3ABC2B0A2F5821116D8BC8B2CACC68930D5" },
+ { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" },
+ { name = "glint", version = "0.18.1", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "5FB54D7732B4105E4AF4D89A7EE6D5E8CF33DA13A3575D0C6ECE470B97958454" },
+ { name = "glisten", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "glisten", source = "hex", outer_checksum = "CF3A9383E9BA4A8CBAF2F7B799716290D02F2AC34E7A77556B49376B662B9314" },
+ { name = "gramps", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "3CCAA6E081225180D95C79679D383BBF51C8D1FDC1B84DA1DA444F628C373793" },
+ { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" },
+ { name = "logging", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "A996064F04EF6E67F0668FD0ACFB309830B05D0EE3A0C11BBBD2D4464334F792" },
+ { name = "lustre", version = "4.2.6", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib"], otp_app = "lustre", source = "hex", outer_checksum = "9ABD71D63F4B8F362CB824DED2C4CA64895DEFACD8F22B0FF055BF15241B1AE2" },
+ { name = "lustre_dev_tools", version = "1.3.3", build_tools = ["gleam"], requirements = ["argv", "filepath", "fs", "gleam_community_ansi", "gleam_erlang", "gleam_http", "gleam_httpc", "gleam_json", "gleam_otp", "gleam_package_interface", "gleam_stdlib", "glint", "glisten", "mist", "simplifile", "spinner", "term_size", "tom", "wisp"], otp_app = "lustre_dev_tools", source = "hex", outer_checksum = "67B4E62DAD9B8323487AAA697A6F3FA72348B6DEA6674D65D4F7A1407CF377ED" },
+ { name = "lustre_http", version = "0.5.2", build_tools = ["gleam"], requirements = ["gleam_fetch", "gleam_http", "gleam_javascript", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "lustre_http", source = "hex", outer_checksum = "FB0478CBFA6B16DBE8ECA326DAE2EC15645E04900595EF2C4F039ABFA0512ABA" },
+ { name = "lustre_ui", version = "0.6.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "lustre_ui", source = "hex", outer_checksum = "FA1F9E89D89CDD5DF376ED86ABA8A38441CB2E664CD4D402F22A49DA4D7BB56D" },
+ { name = "marceau", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "5188D643C181EE350D8A20A3BDBD63AF7B6C505DE333CFBE05EF642ADD88A59B" },
+ { name = "mist", version = "1.2.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "109B4D64E68C104CC23BB3CC5441ECD479DD7444889DA01113B75C6AF0F0E17B" },
+ { name = "modem", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "lustre"], otp_app = "modem", source = "hex", outer_checksum = "4C6E448089B09A57C179455D44526A717E4E217D4000B91201617FD2D9F18E68" },
+ { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" },
+ { name = "repeatedly", version = "2.1.1", build_tools = ["gleam"], requirements = [], otp_app = "repeatedly", source = "hex", outer_checksum = "38808C3EC382B0CD981336D5879C24ECB37FCB9C1D1BD128F7A80B0F74404D79" },
+ { name = "simplifile", version = "1.7.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "1D5DFA3A2F9319EC85825F6ED88B8E449F381B0D55A62F5E61424E748E7DDEB0" },
+ { name = "snag", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "54D32E16E33655346AA3E66CBA7E191DE0A8793D2C05284E3EFB90AD2CE92BCC" },
+ { name = "spinner", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_erlang", "gleam_stdlib", "glearray", "repeatedly"], otp_app = "spinner", source = "hex", outer_checksum = "200BA3D4A04D468898E63C0D316E23F526E02514BC46454091975CB5BAE41E8F" },
+ { name = "term_size", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "term_size", source = "hex", outer_checksum = "D00BD2BC8FB3EBB7E6AE076F3F1FF2AC9D5ED1805F004D0896C784D06C6645F1" },
+ { name = "thoas", version = "1.2.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A" },
+ { name = "tom", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "0831C73E45405A2153091226BF98FB485ED16376988602CC01A5FD086B82D577" },
+ { name = "wisp", version = "0.14.0", build_tools = ["gleam"], requirements = ["exception", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "9F5453AF1F9275E6F8707BC815D6A6A9DF41551921B16FBDBA52883773BAE684" },
+]
+
+[requirements]
+gleam_json = { version = "1.0.1" }
+gleam_stdlib = { version = "~> 0.36" }
+gleeunit = { version = "~> 1.0" }
+lustre = { version = "~> 4.0" }
+lustre_dev_tools = { version = "~> 1.1" }
+lustre_http = { version = "~> 0.5.2" }
+lustre_ui = { version = "~> 0.4" }
+modem = { version = "~> 1.0" }
diff --git a/packages/lustre/examples/07-routing/src/app.gleam b/packages/lustre/examples/07-routing/src/app.gleam
new file mode 100644
index 0000000..5b65d51
--- /dev/null
+++ b/packages/lustre/examples/07-routing/src/app.gleam
@@ -0,0 +1,169 @@
+import gleam/dynamic
+import gleam/list
+import gleam/result
+import gleam/string
+import gleam/uri.{type Uri}
+import lustre
+import lustre/attribute
+import lustre/effect.{type Effect}
+import lustre/element.{type Element}
+import lustre/element/html
+import lustre/event
+import lustre/ui
+import lustre/ui/layout/cluster
+import lustre/ui/util/cn
+import modem
+
+// MAIN ------------------------------------------------------------------------
+
+pub fn main() {
+ let app = lustre.application(init, update, view)
+ let assert Ok(_) = lustre.start(app, "#app", Nil)
+}
+
+// MODEL -----------------------------------------------------------------------
+
+type Model {
+ Model(current_route: Route, guests: List(Guest), new_guest_name: String)
+}
+
+type Route {
+ Home
+ WelcomeGuest(value: String)
+}
+
+type Guest {
+ Guest(slug: String, name: String)
+}
+
+fn init(_flags) -> #(Model, Effect(Msg)) {
+ #(
+ Model(
+ current_route: Home,
+ guests: [
+ Guest(slug: "chihiro", name: "Chihiro"),
+ Guest(slug: "totoro", name: "Totoro"),
+ ],
+ new_guest_name: "",
+ ),
+ modem.init(on_route_change),
+ )
+}
+
+fn on_route_change(uri: Uri) -> Msg {
+ case uri.path_segments(uri.path) {
+ ["welcome", guest] -> OnRouteChange(WelcomeGuest(guest))
+ _ -> OnRouteChange(Home)
+ }
+}
+
+// UPDATE ----------------------------------------------------------------------
+
+pub opaque type Msg {
+ OnRouteChange(Route)
+ UserUpdatedNewGuestName(String)
+ UserAddedNewGuest(Guest)
+}
+
+fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
+ case msg {
+ OnRouteChange(route) -> #(
+ Model(..model, current_route: route),
+ effect.none(),
+ )
+ UserUpdatedNewGuestName(name) -> #(
+ Model(..model, new_guest_name: name),
+ effect.none(),
+ )
+ UserAddedNewGuest(guest) -> #(
+ Model(
+ ..model,
+ guests: list.append(model.guests, [guest]),
+ new_guest_name: "",
+ ),
+ effect.none(),
+ )
+ }
+}
+
+// VIEW ------------------------------------------------------------------------
+
+fn view(model: Model) -> Element(Msg) {
+ let styles = [#("margin", "15vh")]
+
+ let page = case model.current_route {
+ Home -> view_home(model)
+ WelcomeGuest(name) -> view_welcome(model, name)
+ }
+
+ ui.stack([attribute.style(styles)], [view_nav(model), page])
+}
+
+fn view_home(model: Model) {
+ let new_guest_input = fn(event) {
+ use key_code <- result.try(dynamic.field("key", dynamic.string)(event))
+ case key_code {
+ "Enter" -> {
+ let guest_slug =
+ model.new_guest_name
+ |> string.replace(" ", "-")
+ |> string.lowercase
+ Ok(
+ UserAddedNewGuest(Guest(name: model.new_guest_name, slug: guest_slug)),
+ )
+ }
+ _ -> {
+ use value <- result.try(event.value(event))
+ Ok(UserUpdatedNewGuestName(value))
+ }
+ }
+ }
+
+ view_body([
+ view_title("Welcome to the Party 🏡"),
+ html.p([], [element.text("Please sign the guest book:")]),
+ ui.input([
+ event.on("keyup", new_guest_input),
+ attribute.value(model.new_guest_name),
+ ]),
+ ])
+}
+
+fn view_welcome(model: Model, slug) -> Element(a) {
+ let guest =
+ model.guests
+ |> list.find(fn(guest: Guest) { guest.slug == slug })
+
+ let title = case guest {
+ Ok(guest) -> view_title("Hello, " <> guest.name <> "! 🎉")
+ _ -> view_title("Sorry ... didn't quite catch that.")
+ }
+
+ view_body([title])
+}
+
+fn view_nav(model: Model) -> Element(a) {
+ let item_styles = [#("text-decoration", "underline")]
+
+ let view_nav_item = fn(path, text) {
+ html.a([attribute.href("/" <> path), attribute.style(item_styles)], [
+ element.text(text),
+ ])
+ }
+
+ let guest_nav_items =
+ model.guests
+ |> list.map(fn(guest: Guest) {
+ view_nav_item("welcome/" <> guest.slug, guest.name)
+ })
+
+ cluster.of(html.nav, [], [view_nav_item("", "Home"), ..guest_nav_items])
+}
+
+fn view_body(children) {
+ ui.centre([cn.mt_xl()], ui.stack([], children))
+}
+
+fn view_title(text) {
+ html.h1([cn.text_xl()], [element.text(text)])
+}
diff --git a/packages/lustre/examples/99-full-stack-applications/README.md b/packages/lustre/examples/99-full-stack-applications/README.md
new file mode 100644
index 0000000..a45e7a9
--- /dev/null
+++ b/packages/lustre/examples/99-full-stack-applications/README.md
@@ -0,0 +1,25 @@
+# app
+
+[![Package Version](https://img.shields.io/hexpm/v/app)](https://hex.pm/packages/app)
+[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/app/)
+
+```sh
+gleam add app
+```
+```gleam
+import app
+
+pub fn main() {
+ // TODO: An example of the project in use
+}
+```
+
+Further documentation can be found at .
+
+## Development
+
+```sh
+gleam run # Run the project
+gleam test # Run the tests
+gleam shell # Run an Erlang shell
+```
diff --git a/packages/lustre/examples/99-full-stack-applications/client/gleam.toml b/packages/lustre/examples/99-full-stack-applications/client/gleam.toml
new file mode 100644
index 0000000..9025873
--- /dev/null
+++ b/packages/lustre/examples/99-full-stack-applications/client/gleam.toml
@@ -0,0 +1,24 @@
+name = "app"
+version = "1.0.0"
+
+# Fill out these fields if you intend to generate HTML documentation or publish
+# your project to the Hex package manager.
+#
+# description = ""
+# licences = ["Apache-2.0"]
+# repository = { type = "github", user = "username", repo = "project" }
+# links = [{ title = "Website", href = "https://gleam.run" }]
+#
+# For a full reference of all the available options, you can have a look at
+# https://gleam.run/writing-gleam/gleam-toml/.
+
+[dependencies]
+gleam_stdlib = ">= 0.34.0 and < 2.0.0"
+lustre = ">= 4.2.4 and < 5.0.0"
+lustre_http = ">= 0.5.2 and < 1.0.0"
+gleam_json = ">= 1.0.1 and < 2.0.0"
+decipher = ">= 1.2.0 and < 2.0.0"
+
+[dev-dependencies]
+gleeunit = ">= 1.0.0 and < 2.0.0"
+lustre_dev_tools = ">= 1.3.2 and < 2.0.0"
diff --git a/packages/lustre/examples/99-full-stack-applications/client/index.html b/packages/lustre/examples/99-full-stack-applications/client/index.html
new file mode 100644
index 0000000..da137b1
--- /dev/null
+++ b/packages/lustre/examples/99-full-stack-applications/client/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+ 🚧 app
+
+
+
+
+
+
+
+
diff --git a/packages/lustre/examples/99-full-stack-applications/client/manifest.toml b/packages/lustre/examples/99-full-stack-applications/client/manifest.toml
new file mode 100644
index 0000000..4d627ad
--- /dev/null
+++ b/packages/lustre/examples/99-full-stack-applications/client/manifest.toml
@@ -0,0 +1,54 @@
+# This file was generated by Gleam
+# You typically do not need to edit this file
+
+packages = [
+ { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" },
+ { name = "birl", version = "1.7.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "B1FA529E7BE3FF12CADF32814AB8EC7294E74CEDEE8CC734505707B929A98985" },
+ { name = "decipher", version = "1.2.0", build_tools = ["gleam"], requirements = ["birl", "gleam_json", "gleam_stdlib", "stoiridh_version"], otp_app = "decipher", source = "hex", outer_checksum = "9F1B5C6FF0D798046E4E0EF87D09DD729324CB72BD7F0D4152B797324D51223E" },
+ { name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" },
+ { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" },
+ { name = "fs", version = "8.6.1", build_tools = ["rebar3"], requirements = [], otp_app = "fs", source = "hex", outer_checksum = "61EA2BDAEDAE4E2024D0D25C63E44DCCF65622D4402DB4A2DF12868D1546503F" },
+ { name = "gleam_community_ansi", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "FE79E08BF97009729259B6357EC058315B6FBB916FAD1C2FF9355115FEB0D3A4" },
+ { name = "gleam_community_colour", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "795964217EBEDB3DA656F5EB8F67D7AD22872EB95182042D3E7AFEF32D3FD2FE" },
+ { name = "gleam_crypto", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "ADD058DEDE8F0341F1ADE3AAC492A224F15700829D9A3A3F9ADF370F875C51B7" },
+ { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" },
+ { name = "gleam_fetch", version = "0.4.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_javascript", "gleam_stdlib"], otp_app = "gleam_fetch", source = "hex", outer_checksum = "7446410A44A1D1328F5BC1FF4FC9CBD1570479EA69349237B3F82E34521CCC10" },
+ { name = "gleam_http", version = "3.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" },
+ { name = "gleam_httpc", version = "2.2.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "CF76C71002DEECF6DC5D9CA83D962728FAE166B57926BE442D827004D3C7DF1B" },
+ { name = "gleam_javascript", version = "0.8.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "14D5B7E1A70681E0776BF0A0357F575B822167960C844D3D3FA114D3A75F05A8" },
+ { name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" },
+ { name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" },
+ { name = "gleam_package_interface", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_package_interface", source = "hex", outer_checksum = "52A721BCA972C8099BB881195D821AAA64B9F2655BECC102165D5A1097731F01" },
+ { name = "gleam_stdlib", version = "0.38.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "663CF11861179AF415A625307447775C09404E752FF99A24E2057C835319F1BE" },
+ { name = "glearray", version = "0.2.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glearray", source = "hex", outer_checksum = "9C207E05F38D724F464FA921378DB3ABC2B0A2F5821116D8BC8B2CACC68930D5" },
+ { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" },
+ { name = "glint", version = "0.18.1", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "5FB54D7732B4105E4AF4D89A7EE6D5E8CF33DA13A3575D0C6ECE470B97958454" },
+ { name = "glisten", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "glisten", source = "hex", outer_checksum = "CF3A9383E9BA4A8CBAF2F7B799716290D02F2AC34E7A77556B49376B662B9314" },
+ { name = "gramps", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "3CCAA6E081225180D95C79679D383BBF51C8D1FDC1B84DA1DA444F628C373793" },
+ { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" },
+ { name = "logging", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "A996064F04EF6E67F0668FD0ACFB309830B05D0EE3A0C11BBBD2D4464334F792" },
+ { name = "lustre", version = "4.2.4", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib"], otp_app = "lustre", source = "hex", outer_checksum = "09B94E1380CBC400DCD594B36A845E5CB2E143DF89E95460B2CA59E44499CAC9" },
+ { name = "lustre_dev_tools", version = "1.3.2", build_tools = ["gleam"], requirements = ["argv", "filepath", "fs", "gleam_community_ansi", "gleam_erlang", "gleam_http", "gleam_httpc", "gleam_json", "gleam_otp", "gleam_package_interface", "gleam_stdlib", "glint", "glisten", "mist", "simplifile", "spinner", "term_size", "tom", "wisp"], otp_app = "lustre_dev_tools", source = "hex", outer_checksum = "CC8F46BCE51C1349862C5F6BA0075B0C68096B866ED1C520B60358FAAB398B60" },
+ { name = "lustre_http", version = "0.5.2", build_tools = ["gleam"], requirements = ["gleam_fetch", "gleam_http", "gleam_javascript", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "lustre_http", source = "hex", outer_checksum = "FB0478CBFA6B16DBE8ECA326DAE2EC15645E04900595EF2C4F039ABFA0512ABA" },
+ { name = "marceau", version = "1.1.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "1AAD727A30BE0F95562C3403BB9B27C823797AD90037714255EEBF617B1CDA81" },
+ { name = "mist", version = "1.2.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "109B4D64E68C104CC23BB3CC5441ECD479DD7444889DA01113B75C6AF0F0E17B" },
+ { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" },
+ { name = "repeatedly", version = "2.1.1", build_tools = ["gleam"], requirements = [], otp_app = "repeatedly", source = "hex", outer_checksum = "38808C3EC382B0CD981336D5879C24ECB37FCB9C1D1BD128F7A80B0F74404D79" },
+ { name = "simplifile", version = "1.7.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "1D5DFA3A2F9319EC85825F6ED88B8E449F381B0D55A62F5E61424E748E7DDEB0" },
+ { name = "snag", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "54D32E16E33655346AA3E66CBA7E191DE0A8793D2C05284E3EFB90AD2CE92BCC" },
+ { name = "spinner", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_erlang", "gleam_stdlib", "glearray", "repeatedly"], otp_app = "spinner", source = "hex", outer_checksum = "200BA3D4A04D468898E63C0D316E23F526E02514BC46454091975CB5BAE41E8F" },
+ { name = "stoiridh_version", version = "0.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "stoiridh_version", source = "hex", outer_checksum = "298ABEA44DF37764A34C2E9190A84BF2770BC59DD9397C6DC7708040E5A0142B" },
+ { name = "term_size", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "term_size", source = "hex", outer_checksum = "D00BD2BC8FB3EBB7E6AE076F3F1FF2AC9D5ED1805F004D0896C784D06C6645F1" },
+ { name = "thoas", version = "1.2.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A" },
+ { name = "tom", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "0831C73E45405A2153091226BF98FB485ED16376988602CC01A5FD086B82D577" },
+ { name = "wisp", version = "0.14.0", build_tools = ["gleam"], requirements = ["exception", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "9F5453AF1F9275E6F8707BC815D6A6A9DF41551921B16FBDBA52883773BAE684" },
+]
+
+[requirements]
+decipher = { version = ">= 1.2.0 and < 2.0.0"}
+gleam_json = { version = ">= 1.0.1 and < 2.0.0" }
+gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" }
+gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
+lustre = { version = ">= 4.2.4 and < 5.0.0" }
+lustre_dev_tools = { version = ">= 1.3.2 and < 2.0.0" }
+lustre_http = { version = ">= 0.5.2 and < 1.0.0" }
diff --git a/packages/lustre/examples/99-full-stack-applications/client/src/app.gleam b/packages/lustre/examples/99-full-stack-applications/client/src/app.gleam
new file mode 100644
index 0000000..aae4530
--- /dev/null
+++ b/packages/lustre/examples/99-full-stack-applications/client/src/app.gleam
@@ -0,0 +1,127 @@
+// IMPORTS ---------------------------------------------------------------------
+
+import decipher
+import gleam/dynamic.{dynamic}
+import gleam/int
+import gleam/list
+import gleam/result
+import lustre
+import lustre/attribute
+import lustre/effect.{type Effect}
+import lustre/element.{type Element}
+import lustre/element/html
+import lustre/event
+
+// MAIN ------------------------------------------------------------------------
+
+pub fn main() {
+ let app = lustre.application(init, update, view)
+ let assert Ok(_) = lustre.start(app, "#app", Nil)
+
+ Nil
+}
+
+// MODEL -----------------------------------------------------------------------
+
+type Model =
+ List(#(String, Int))
+
+fn init(_) -> #(Model, Effect(Msg)) {
+ let model = []
+ let effect = effect.none()
+
+ #(model, effect)
+}
+
+// UPDATE ----------------------------------------------------------------------
+
+type Msg {
+ ServerSavedList(Result(Nil, String))
+ UserAddedProduct(name: String)
+ UserSavedList
+ UserUpdatedQuantity(name: String, amount: Int)
+}
+
+fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
+ case msg {
+ ServerSavedList(_) -> #(model, effect.none())
+ UserAddedProduct(name) -> #([#(name, 1), ..model], effect.none())
+ UserSavedList -> #(model, effect.none())
+ UserUpdatedQuantity(name, quantity) -> {
+ let model =
+ list.map(model, fn(item) {
+ case item.0 == name {
+ True -> #(name, quantity)
+ False -> item
+ }
+ })
+
+ #(model, effect.none())
+ }
+ }
+}
+
+// VIEW ------------------------------------------------------------------------
+
+fn view(model: Model) -> Element(Msg) {
+ let styles = [
+ #("max-width", "30ch"),
+ #("margin", "0 auto"),
+ #("display", "flex"),
+ #("flex-direction", "column"),
+ #("gap", "1em"),
+ ]
+
+ html.div([attribute.style(styles)], [
+ view_grocery_list(model),
+ view_new_item(),
+ html.div([], [html.button([], [html.text("Sync")])]),
+ ])
+}
+
+fn view_new_item() -> Element(Msg) {
+ let handle_click = fn(event) {
+ let path = ["target", "previousElementSibling", "value"]
+
+ event
+ |> decipher.at(path, dynamic.string)
+ |> result.map(UserAddedProduct)
+ }
+
+ html.div([], [
+ html.input([]),
+ html.button([event.on("click", handle_click)], [html.text("Add")]),
+ ])
+}
+
+fn view_grocery_list(model: Model) -> Element(Msg) {
+ let styles = [#("display", "flex"), #("flex-direction", "column-reverse")]
+
+ element.keyed(html.div([attribute.style(styles)], _), {
+ use #(name, quantity) <- list.map(model)
+ let item = view_grocery_item(name, quantity)
+
+ #(name, item)
+ })
+}
+
+fn view_grocery_item(name: String, quantity: Int) -> Element(Msg) {
+ let handle_input = fn(e) {
+ event.value(e)
+ |> result.nil_error
+ |> result.then(int.parse)
+ |> result.map(UserUpdatedQuantity(name, _))
+ |> result.replace_error([])
+ }
+
+ html.div([attribute.style([#("display", "flex"), #("gap", "1em")])], [
+ html.span([attribute.style([#("flex", "1")])], [html.text(name)]),
+ html.input([
+ attribute.style([#("width", "4em")]),
+ attribute.type_("number"),
+ attribute.value(int.to_string(quantity)),
+ attribute.min("0"),
+ event.on("input", handle_input),
+ ]),
+ ])
+}
diff --git a/packages/lustre/examples/99-full-stack-applications/server/gleam.toml b/packages/lustre/examples/99-full-stack-applications/server/gleam.toml
new file mode 100644
index 0000000..4ea8472
--- /dev/null
+++ b/packages/lustre/examples/99-full-stack-applications/server/gleam.toml
@@ -0,0 +1,19 @@
+name = "app"
+version = "1.0.0"
+
+# Fill out these fields if you intend to generate HTML documentation or publish
+# your project to the Hex package manager.
+#
+# description = ""
+# licences = ["Apache-2.0"]
+# repository = { type = "github", user = "username", repo = "project" }
+# links = [{ title = "Website", href = "https://gleam.run" }]
+#
+# For a full reference of all the available options, you can have a look at
+# https://gleam.run/writing-gleam/gleam-toml/.
+
+[dependencies]
+gleam_stdlib = ">= 0.34.0 and < 2.0.0"
+
+[dev-dependencies]
+gleeunit = ">= 1.0.0 and < 2.0.0"
diff --git a/packages/lustre/examples/99-full-stack-applications/server/manifest.toml b/packages/lustre/examples/99-full-stack-applications/server/manifest.toml
new file mode 100644
index 0000000..2a7bc47
--- /dev/null
+++ b/packages/lustre/examples/99-full-stack-applications/server/manifest.toml
@@ -0,0 +1,11 @@
+# This file was generated by Gleam
+# You typically do not need to edit this file
+
+packages = [
+ { name = "gleam_stdlib", version = "0.38.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "663CF11861179AF415A625307447775C09404E752FF99A24E2057C835319F1BE" },
+ { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" },
+]
+
+[requirements]
+gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" }
+gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
diff --git a/packages/lustre/examples/99-full-stack-applications/server/src/app.gleam b/packages/lustre/examples/99-full-stack-applications/server/src/app.gleam
new file mode 100644
index 0000000..452ccb7
--- /dev/null
+++ b/packages/lustre/examples/99-full-stack-applications/server/src/app.gleam
@@ -0,0 +1,5 @@
+import gleam/io
+
+pub fn main() {
+ io.println("Hello from app!")
+}
diff --git a/packages/lustre/examples/99-full-stack-applications/shared/gleam.toml b/packages/lustre/examples/99-full-stack-applications/shared/gleam.toml
new file mode 100644
index 0000000..8478ab1
--- /dev/null
+++ b/packages/lustre/examples/99-full-stack-applications/shared/gleam.toml
@@ -0,0 +1,19 @@
+name = "shared"
+version = "1.0.0"
+
+# Fill out these fields if you intend to generate HTML documentation or publish
+# your project to the Hex package manager.
+#
+# description = ""
+# licences = ["Apache-2.0"]
+# repository = { type = "github", user = "username", repo = "project" }
+# links = [{ title = "Website", href = "https://gleam.run" }]
+#
+# For a full reference of all the available options, you can have a look at
+# https://gleam.run/writing-gleam/gleam-toml/.
+
+[dependencies]
+gleam_stdlib = ">= 0.34.0 and < 2.0.0"
+
+[dev-dependencies]
+gleeunit = ">= 1.0.0 and < 2.0.0"
diff --git a/packages/lustre/examples/99-full-stack-applications/shared/manifest.toml b/packages/lustre/examples/99-full-stack-applications/shared/manifest.toml
new file mode 100644
index 0000000..2a7bc47
--- /dev/null
+++ b/packages/lustre/examples/99-full-stack-applications/shared/manifest.toml
@@ -0,0 +1,11 @@
+# This file was generated by Gleam
+# You typically do not need to edit this file
+
+packages = [
+ { name = "gleam_stdlib", version = "0.38.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "663CF11861179AF415A625307447775C09404E752FF99A24E2057C835319F1BE" },
+ { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" },
+]
+
+[requirements]
+gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" }
+gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
diff --git a/packages/lustre/examples/99-full-stack-applications/shared/src/shared.gleam b/packages/lustre/examples/99-full-stack-applications/shared/src/shared.gleam
new file mode 100644
index 0000000..4b3811b
--- /dev/null
+++ b/packages/lustre/examples/99-full-stack-applications/shared/src/shared.gleam
@@ -0,0 +1,5 @@
+import gleam/io
+
+pub fn main() {
+ io.println("Hello from shared!")
+}
diff --git a/packages/lustre/examples/99-server-components/README.md b/packages/lustre/examples/99-server-components/README.md
new file mode 100644
index 0000000..e69de29
diff --git a/packages/lustre/examples/99-server-components/gleam.toml b/packages/lustre/examples/99-server-components/gleam.toml
new file mode 100644
index 0000000..1d640d2
--- /dev/null
+++ b/packages/lustre/examples/99-server-components/gleam.toml
@@ -0,0 +1,27 @@
+name = "app"
+version = "1.0.0"
+
+# Fill out these fields if you intend to generate HTML documentation or publish
+# your project to the Hex package manager.
+#
+# description = ""
+# licences = ["Apache-2.0"]
+# repository = { type = "github", user = "username", repo = "project" }
+# links = [{ title = "Website", href = "https://gleam.run" }]
+#
+# For a full reference of all the available options, you can have a look at
+# https://gleam.run/writing-gleam/gleam-toml/.
+
+[dependencies]
+gleam_stdlib = "~> 0.36"
+lustre = { path = "../../" }
+mist = "~> 1.0"
+gleam_erlang = "~> 0.24"
+gleam_otp = "~> 0.10"
+gleam_http = "~> 3.6"
+lustre_ui = "~> 0.4"
+gleam_json = "~> 1.0"
+simplifile = "~> 1.5"
+
+[dev-dependencies]
+gleeunit = "~> 1.0"
diff --git a/packages/lustre/examples/99-server-components/manifest.toml b/packages/lustre/examples/99-server-components/manifest.toml
new file mode 100644
index 0000000..aea8226
--- /dev/null
+++ b/packages/lustre/examples/99-server-components/manifest.toml
@@ -0,0 +1,37 @@
+# This file was generated by Gleam
+# You typically do not need to edit this file
+
+packages = [
+ { name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" },
+ { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" },
+ { name = "gleam_community_colour", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "795964217EBEDB3DA656F5EB8F67D7AD22872EB95182042D3E7AFEF32D3FD2FE" },
+ { name = "gleam_crypto", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "ADD058DEDE8F0341F1ADE3AAC492A224F15700829D9A3A3F9ADF370F875C51B7" },
+ { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" },
+ { name = "gleam_http", version = "3.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" },
+ { name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" },
+ { name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" },
+ { name = "gleam_stdlib", version = "0.38.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "663CF11861179AF415A625307447775C09404E752FF99A24E2057C835319F1BE" },
+ { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" },
+ { name = "glisten", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "glisten", source = "hex", outer_checksum = "CF3A9383E9BA4A8CBAF2F7B799716290D02F2AC34E7A77556B49376B662B9314" },
+ { name = "gramps", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "3CCAA6E081225180D95C79679D383BBF51C8D1FDC1B84DA1DA444F628C373793" },
+ { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" },
+ { name = "logging", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "A996064F04EF6E67F0668FD0ACFB309830B05D0EE3A0C11BBBD2D4464334F792" },
+ { name = "lustre", version = "4.2.6", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib"], source = "local", path = "../.." },
+ { name = "lustre_ui", version = "0.6.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "lustre_ui", source = "hex", outer_checksum = "FA1F9E89D89CDD5DF376ED86ABA8A38441CB2E664CD4D402F22A49DA4D7BB56D" },
+ { name = "mist", version = "1.2.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "109B4D64E68C104CC23BB3CC5441ECD479DD7444889DA01113B75C6AF0F0E17B" },
+ { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" },
+ { name = "simplifile", version = "1.7.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "1D5DFA3A2F9319EC85825F6ED88B8E449F381B0D55A62F5E61424E748E7DDEB0" },
+ { name = "thoas", version = "1.2.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A" },
+]
+
+[requirements]
+gleam_erlang = { version = "~> 0.24" }
+gleam_http = { version = "~> 3.6" }
+gleam_json = { version = "~> 1.0" }
+gleam_otp = { version = "~> 0.10" }
+gleam_stdlib = { version = "~> 0.36" }
+gleeunit = { version = "~> 1.0" }
+lustre = { path = "../../" }
+lustre_ui = { version = "~> 0.4" }
+mist = { version = "~> 1.0" }
+simplifile = { version = "~> 1.5" }
diff --git a/packages/lustre/examples/99-server-components/src/app.gleam b/packages/lustre/examples/99-server-components/src/app.gleam
new file mode 100644
index 0000000..bc3227f
--- /dev/null
+++ b/packages/lustre/examples/99-server-components/src/app.gleam
@@ -0,0 +1,168 @@
+import counter
+import gleam/bytes_builder
+import gleam/erlang
+import gleam/erlang/process.{type Selector, type Subject}
+import gleam/http/request.{type Request}
+import gleam/http/response.{type Response}
+import gleam/json
+import gleam/option.{type Option, None, Some}
+import gleam/otp/actor
+import gleam/result
+import lustre
+import lustre/attribute
+import lustre/element.{element}
+import lustre/element/html.{html}
+import lustre/server_component
+import mist.{
+ type Connection, type ResponseData, type WebsocketConnection,
+ type WebsocketMessage,
+}
+
+pub fn main() {
+ let assert Ok(_) =
+ fn(req: Request(Connection)) -> Response(ResponseData) {
+ case request.path_segments(req) {
+ // Set up the websocket connection to the client. This is how we send
+ // DOM updates to the browser and receive events from the client.
+ ["counter"] ->
+ mist.websocket(
+ request: req,
+ on_init: socket_init,
+ on_close: socket_close,
+ handler: socket_update,
+ )
+
+ // We need to serve the server component runtime. There's also a minified
+ // version of this script for production.
+ ["lustre-server-component.mjs"] -> {
+ let assert Ok(priv) = erlang.priv_directory("lustre")
+ let path = priv <> "/static/lustre-server-component.mjs"
+
+ mist.send_file(path, offset: 0, limit: None)
+ |> result.map(fn(script) {
+ response.new(200)
+ |> response.prepend_header("content-type", "application/javascript")
+ |> response.set_body(script)
+ })
+ |> result.lazy_unwrap(fn() {
+ response.new(404)
+ |> response.set_body(mist.Bytes(bytes_builder.new()))
+ })
+ }
+
+ // For all other requests we'll just serve some HTML that renders the
+ // server component.
+ _ ->
+ response.new(200)
+ |> response.prepend_header("content-type", "text/html")
+ |> response.set_body(
+ html([], [
+ html.head([], [
+ html.link([
+ attribute.rel("stylesheet"),
+ attribute.href(
+ "https://cdn.jsdelivr.net/gh/lustre-labs/ui/priv/styles.css",
+ ),
+ ]),
+ html.script(
+ [
+ attribute.type_("module"),
+ attribute.src("/lustre-server-component.mjs"),
+ ],
+ "",
+ ),
+ ]),
+ html.body([], [
+ element(
+ "lustre-server-component",
+ [server_component.route("/counter")],
+ [html.p([], [html.text("This is a slot")])],
+ ),
+ ]),
+ ])
+ |> element.to_document_string_builder
+ |> bytes_builder.from_string_builder
+ |> mist.Bytes,
+ )
+ }
+ }
+ |> mist.new
+ |> mist.port(3000)
+ |> mist.start_http
+
+ process.sleep_forever()
+}
+
+//
+
+type Counter =
+ Subject(lustre.Action(counter.Msg, lustre.ServerComponent))
+
+fn socket_init(
+ conn: WebsocketConnection,
+) -> #(Counter, Option(Selector(lustre.Patch(counter.Msg)))) {
+ let self = process.new_subject()
+ let app = counter.app()
+ let assert Ok(counter) = lustre.start_actor(app, 0)
+
+ process.send(
+ counter,
+ server_component.subscribe(
+ // server components can have many connected clients, so we need a way to
+ // identify this client.
+ "ws",
+ // this callback is called whenever the server component has a new patch
+ // to send to the client. here we json encode that patch and send it to
+ // via the websocket connection.
+ //
+ // a more involved version would have us sending the patch to this socket's
+ // subject, and then it could be handled (perhaps with some other work) in
+ // the `mist.Custom` branch of `socket_update` below.
+ process.send(self, _),
+ ),
+ )
+
+ #(
+ // we store the server component's `Subject` as this socket's state so we
+ // can shut it down when the socket is closed.
+ counter,
+ Some(process.selecting(process.new_selector(), self, fn(a) { a })),
+ )
+}
+
+fn socket_update(
+ counter: Counter,
+ conn: WebsocketConnection,
+ msg: WebsocketMessage(lustre.Patch(counter.Msg)),
+) {
+ case msg {
+ mist.Text(json) -> {
+ // we attempt to decode the incoming text as an action to send to our
+ // server component runtime.
+ let action = json.decode(json, server_component.decode_action)
+
+ case action {
+ Ok(action) -> process.send(counter, action)
+ Error(_) -> Nil
+ }
+
+ actor.continue(counter)
+ }
+
+ mist.Binary(_) -> actor.continue(counter)
+ mist.Custom(patch) -> {
+ let assert Ok(_) =
+ patch
+ |> server_component.encode_patch
+ |> json.to_string
+ |> mist.send_text_frame(conn, _)
+
+ actor.continue(counter)
+ }
+ mist.Closed | mist.Shutdown -> actor.Stop(process.Normal)
+ }
+}
+
+fn socket_close(counter: Counter) {
+ process.send(counter, lustre.shutdown())
+}
diff --git a/packages/lustre/examples/99-server-components/src/counter.gleam b/packages/lustre/examples/99-server-components/src/counter.gleam
new file mode 100644
index 0000000..0c9f115
--- /dev/null
+++ b/packages/lustre/examples/99-server-components/src/counter.gleam
@@ -0,0 +1,58 @@
+import gleam/int
+import lustre
+import lustre/attribute
+import lustre/element.{type Element}
+import lustre/element/html
+import lustre/event
+import lustre/ui
+
+// MAIN ------------------------------------------------------------------------
+
+pub fn app() {
+ lustre.simple(init, update, view)
+}
+
+// MODEL -----------------------------------------------------------------------
+
+type Model =
+ Int
+
+fn init(initial_count: Int) -> Model {
+ case initial_count < 0 {
+ True -> 0
+ False -> initial_count
+ }
+}
+
+// UPDATE ----------------------------------------------------------------------
+
+pub opaque type Msg {
+ Incr
+ Decr
+}
+
+fn update(model: Model, msg: Msg) -> Model {
+ case msg {
+ Incr -> model + 1
+ Decr -> model - 1
+ }
+}
+
+// VIEW ------------------------------------------------------------------------
+
+fn view(model: Model) -> Element(Msg) {
+ let styles = [#("width", "100vw"), #("height", "100vh"), #("padding", "1rem")]
+ let count = int.to_string(model)
+
+ ui.centre(
+ [attribute.style(styles)],
+ ui.stack([], [
+ ui.button([event.on_click(Incr)], [element.text("+")]),
+ html.slot([]),
+ html.p([attribute.style([#("text-align", "center")])], [
+ element.text(count),
+ ]),
+ ui.button([event.on_click(Decr)], [element.text("-")]),
+ ]),
+ )
+}
diff --git a/packages/lustre/examples/README.md b/packages/lustre/examples/README.md
new file mode 100644
index 0000000..c3a6009
--- /dev/null
+++ b/packages/lustre/examples/README.md
@@ -0,0 +1,47 @@
+# Lustre examples
+
+Each of the examples in this directory is a self-contained, complete Gleam app
+that demonstrates a particular feature or concept of the library. For newcomers,
+we recommend looking through them in order, as each example tends to build on
+the previous ones. Feel free to jump in to any example that interests you, though!
+
+> **Note**: these examples all use [`lustre/ui`](https://github.com/lustre-labs/ui)
+> to show off something a little more visually interesting than unstyled HTML. None
+> of the ideas in these examples are specific to `lustre/ui`, though, and you should
+> know that you can follow along with any of these examples using only the standard
+> `lustre/element/html` module.
+
+## Examples
+
+- [`01-hello-world`](./01-hello-world) is a simple example to just get something
+ on the screen.
+
+- [`02-interactivity`](./02-interactivity) introduces the core Model-View-Update
+ loop that underpins every Lustre application.
+
+- [`03-controlled-inputs`](./03-controlled-inputs) demonstrates the most common
+ way to handle `` elements in Lustre.
+
+- [`04-custom-event-handlers`](./04-custom-event-handlers) shows you how to
+ write your own event handlers and decoders, instead of relying on the ones
+ provided by `lustre/event`.
+
+- [`05-http-requests`](./05-http-requests) demonstrates how side effects are
+ handled in Lustre, using the third-party [`lustre_http`](https://hexdocs.pm/lustre_http/)
+ package.
+
+- [`06-custom-effects`](./06-custom-effects) builds on the previous example and
+ shows you how to write your own side effects for Lustre to perform.
+
+- [`07-routing`](./07-routing) shows you how to use [`modem`](https://hexdocs.pm/modem/)
+ to set up routing and navigating between pages in a Lustre app.
+
+## Getting help
+
+If you're having trouble with Lustre or not sure what the right way to do
+something is, the best place to get help is the [Gleam Discord server](https://discord.gg/Fm8Pwmy).
+You could also open an issue on the [Lustre GitHub repository](https://github.com/lustre-labs/lustre/issues).
+
+While our docs are still a work in progress, the official [Elm guide](https://guide.elm-lang.org)
+is also a great resource for learning about the Model-View-Update architecture
+and the kinds of patterns that Lustre is built around.
diff --git a/packages/lustre/gleam.toml b/packages/lustre/gleam.toml
new file mode 100644
index 0000000..8996318
--- /dev/null
+++ b/packages/lustre/gleam.toml
@@ -0,0 +1,49 @@
+name = "lustre"
+version = "4.3.0"
+gleam = ">= 1.0.0"
+
+description = "An Elm-inspired framework for building HTML templates, single page applications, and server-rendered components in Gleam!"
+repository = { type = "github", user = "lustre-labs", repo = "lustre" }
+licences = ["MIT"]
+
+links = [
+ { title = "Sponsor", href = "https://github.com/sponsors/hayleigh-dot-dev" },
+]
+
+internal_modules = ["lustre/internals", "lustre/internals/*"]
+
+[documentation]
+pages = [
+ { title = "Examples directory", path = "reference/examples.html", source = "./pages/reference/examples.md" },
+ { title = " ", path = "#", source = "" },
+ { title = "Quickstart guide", path = "guide/01-quickstart.html", source = "./pages/guide/01-quickstart.md" },
+ { title = "Managing state", path = "guide/02-state-management.html", source = "./pages/guide/02-state-management.md" },
+ { title = "Side effects", path = "guide/03-side-effects.html", source = "./pages/guide/03-side-effects.md" },
+ { title = "Server-side rendering", path = "guide/04-server-side-rendering.html", source = "./pages/guide/04-server-side-rendering.md" },
+ # { title = "Full-stack apps", path = "guide/05-full-stack-apps.html", source = "./pages/guide/05-full-stack-apps.md" },
+ # { title = "Components", path = "#", source = "" },
+ # { title = "Server components", path = "#", source = "" },
+ # { title = " ", path = "#", source = "" },
+ # { title = "SPA deployments", path = "#", source = "" },
+ # { title = "Full-stack deployments", path = "#", source = "" },
+ # { title = " ", path = "#", source = "" },
+ # { title = "Using with Wisp", path = "#", source = "" },
+ # { title = "Using with Glen", path = "#", source = "" },
+ # { title = "Using with Mist", path = "#", source = "" },
+ # { title = " ", path = "#", source = "" },
+ { title = "For Elm developers", path = "cheatsheets/elm", source = "./pages/reference/for-elm-devs.md" },
+ { title = "For React developers", path = "cheatsheets/react", source = "./pages/reference/for-react-devs.md" },
+ { title = "For LiveView developers", path = "cheatsheets/liveview", source = "./pages/reference/for-liveview-devs.md" },
+]
+
+[dependencies]
+gleam_erlang = "~> 0.24"
+gleam_json = "~> 1.0 or ~> 2.0"
+gleam_otp = "~> 0.9"
+gleam_stdlib = "~> 0.36"
+
+[dev-dependencies]
+birdie = "~> 1.0"
+gleeunit = "~> 1.0"
+shellout = "~> 1.6"
+simplifile = "~> 1.4"
diff --git a/packages/lustre/manifest.toml b/packages/lustre/manifest.toml
new file mode 100644
index 0000000..0dd508b
--- /dev/null
+++ b/packages/lustre/manifest.toml
@@ -0,0 +1,33 @@
+# This file was generated by Gleam
+# You typically do not need to edit this file
+
+packages = [
+ { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" },
+ { name = "birdie", version = "1.1.5", build_tools = ["gleam"], requirements = ["argv", "filepath", "glance", "gleam_community_ansi", "gleam_erlang", "gleam_stdlib", "justin", "rank", "simplifile", "trie_again"], otp_app = "birdie", source = "hex", outer_checksum = "E1B6CB7B9EDE8F4C67F7E68C9FB45FBAA54881545F85D315D2B179560CC63F60" },
+ { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" },
+ { name = "glance", version = "0.11.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "glexer"], otp_app = "glance", source = "hex", outer_checksum = "8F3314D27773B7C3B9FB58D8C02C634290422CE531988C0394FA0DF8676B964D" },
+ { name = "gleam_community_ansi", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "FE79E08BF97009729259B6357EC058315B6FBB916FAD1C2FF9355115FEB0D3A4" },
+ { name = "gleam_community_colour", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "795964217EBEDB3DA656F5EB8F67D7AD22872EB95182042D3E7AFEF32D3FD2FE" },
+ { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" },
+ { name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" },
+ { name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" },
+ { name = "gleam_stdlib", version = "0.37.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "5398BD6C2ABA17338F676F42F404B9B7BABE1C8DC7380031ACB05BBE1BCF3742" },
+ { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" },
+ { name = "glexer", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glexer", source = "hex", outer_checksum = "BD477AD657C2B637FEF75F2405FAEFFA533F277A74EF1A5E17B55B1178C228FB" },
+ { name = "justin", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "justin", source = "hex", outer_checksum = "7FA0C6DB78640C6DC5FBFD59BF3456009F3F8B485BF6825E97E1EB44E9A1E2CD" },
+ { name = "rank", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "rank", source = "hex", outer_checksum = "5660E361F0E49CBB714CC57CC4C89C63415D8986F05B2DA0C719D5642FAD91C9" },
+ { name = "shellout", version = "1.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "shellout", source = "hex", outer_checksum = "E2FCD18957F0E9F67E1F497FC9FF57393392F8A9BAEAEA4779541DE7A68DD7E0" },
+ { name = "simplifile", version = "1.7.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "1D5DFA3A2F9319EC85825F6ED88B8E449F381B0D55A62F5E61424E748E7DDEB0" },
+ { name = "thoas", version = "1.2.0", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "540C8CB7D9257F2AD0A14145DC23560F91ACDCA995F0CCBA779EB33AF5D859D1" },
+ { name = "trie_again", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "trie_again", source = "hex", outer_checksum = "5B19176F52B1BD98831B57FDC97BD1F88C8A403D6D8C63471407E78598E27184" },
+]
+
+[requirements]
+birdie = { version = "~> 1.0" }
+gleam_erlang = { version = "~> 0.24" }
+gleam_json = { version = "~> 1.0" }
+gleam_otp = { version = "~> 0.9" }
+gleam_stdlib = { version = "~> 0.36" }
+gleeunit = { version = "~> 1.0" }
+shellout = { version = "~> 1.6" }
+simplifile = { version = "~> 1.4" }
diff --git a/packages/lustre/package-lock.json b/packages/lustre/package-lock.json
new file mode 100644
index 0000000..c11246c
--- /dev/null
+++ b/packages/lustre/package-lock.json
@@ -0,0 +1,3170 @@
+{
+ "name": "lustre-client-test",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "lustre-client-test",
+ "version": "0.1.0",
+ "license": "MIT",
+ "devDependencies": {
+ "esbuild": "^0.20.2",
+ "linkedom": "^0.16.11",
+ "npm-run-all": "^4.1.5",
+ "vitest": "^1.5.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
+ "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz",
+ "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz",
+ "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz",
+ "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz",
+ "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz",
+ "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz",
+ "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz",
+ "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz",
+ "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz",
+ "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz",
+ "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz",
+ "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz",
+ "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz",
+ "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz",
+ "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz",
+ "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz",
+ "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz",
+ "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz",
+ "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz",
+ "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz",
+ "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz",
+ "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz",
+ "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@jest/schemas": {
+ "version": "29.6.3",
+ "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz",
+ "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==",
+ "dev": true,
+ "dependencies": {
+ "@sinclair/typebox": "^0.27.8"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.4.15",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
+ "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
+ "dev": true
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.16.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.16.4.tgz",
+ "integrity": "sha512-GkhjAaQ8oUTOKE4g4gsZ0u8K/IHU1+2WQSgS1TwTcYvL+sjbaQjNHFXbOJ6kgqGHIO1DfUhI/Sphi9GkRT9K+Q==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.16.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.16.4.tgz",
+ "integrity": "sha512-Bvm6D+NPbGMQOcxvS1zUl8H7DWlywSXsphAeOnVeiZLQ+0J6Is8T7SrjGTH29KtYkiY9vld8ZnpV3G2EPbom+w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.16.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.16.4.tgz",
+ "integrity": "sha512-i5d64MlnYBO9EkCOGe5vPR/EeDwjnKOGGdd7zKFhU5y8haKhQZTN2DgVtpODDMxUr4t2K90wTUJg7ilgND6bXw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.16.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.16.4.tgz",
+ "integrity": "sha512-WZupV1+CdUYehaZqjaFTClJI72fjJEgTXdf4NbW69I9XyvdmztUExBtcI2yIIU6hJtYvtwS6pkTkHJz+k08mAQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.16.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.16.4.tgz",
+ "integrity": "sha512-ADm/xt86JUnmAfA9mBqFcRp//RVRt1ohGOYF6yL+IFCYqOBNwy5lbEK05xTsEoJq+/tJzg8ICUtS82WinJRuIw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.16.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.16.4.tgz",
+ "integrity": "sha512-tJfJaXPiFAG+Jn3cutp7mCs1ePltuAgRqdDZrzb1aeE3TktWWJ+g7xK9SNlaSUFw6IU4QgOxAY4rA+wZUT5Wfg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.16.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.16.4.tgz",
+ "integrity": "sha512-7dy1BzQkgYlUTapDTvK997cgi0Orh5Iu7JlZVBy1MBURk7/HSbHkzRnXZa19ozy+wwD8/SlpJnOOckuNZtJR9w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.16.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.16.4.tgz",
+ "integrity": "sha512-zsFwdUw5XLD1gQe0aoU2HVceI6NEW7q7m05wA46eUAyrkeNYExObfRFQcvA6zw8lfRc5BHtan3tBpo+kqEOxmg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
+ "version": "4.16.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.16.4.tgz",
+ "integrity": "sha512-p8C3NnxXooRdNrdv6dBmRTddEapfESEUflpICDNKXpHvTjRRq1J82CbU5G3XfebIZyI3B0s074JHMWD36qOW6w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.16.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.16.4.tgz",
+ "integrity": "sha512-Lh/8ckoar4s4Id2foY7jNgitTOUQczwMWNYi+Mjt0eQ9LKhr6sK477REqQkmy8YHY3Ca3A2JJVdXnfb3Rrwkng==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.16.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.16.4.tgz",
+ "integrity": "sha512-1xwwn9ZCQYuqGmulGsTZoKrrn0z2fAur2ujE60QgyDpHmBbXbxLaQiEvzJWDrscRq43c8DnuHx3QorhMTZgisQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.16.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.16.4.tgz",
+ "integrity": "sha512-LuOGGKAJ7dfRtxVnO1i3qWc6N9sh0Em/8aZ3CezixSTM+E9Oq3OvTsvC4sm6wWjzpsIlOCnZjdluINKESflJLA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.16.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.16.4.tgz",
+ "integrity": "sha512-ch86i7KkJKkLybDP2AtySFTRi5fM3KXp0PnHocHuJMdZwu7BuyIKi35BE9guMlmTpwwBTB3ljHj9IQXnTCD0vA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.16.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.16.4.tgz",
+ "integrity": "sha512-Ma4PwyLfOWZWayfEsNQzTDBVW8PZ6TUUN1uFTBQbF2Chv/+sjenE86lpiEwj2FiviSmSZ4Ap4MaAfl1ciF4aSA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.16.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.16.4.tgz",
+ "integrity": "sha512-9m/ZDrQsdo/c06uOlP3W9G2ENRVzgzbSXmXHT4hwVaDQhYcRpi9bgBT0FTG9OhESxwK0WjQxYOSfv40cU+T69w==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.16.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.16.4.tgz",
+ "integrity": "sha512-YunpoOAyGLDseanENHmbFvQSfVL5BxW3k7hhy0eN4rb3gS/ct75dVD0EXOWIqFT/nE8XYW6LP6vz6ctKRi0k9A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@sinclair/typebox": {
+ "version": "0.27.8",
+ "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
+ "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
+ "dev": true
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
+ "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
+ "dev": true
+ },
+ "node_modules/@vitest/expect": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.5.2.tgz",
+ "integrity": "sha512-rf7MTD1WCoDlN3FfYJ9Llfp0PbdtOMZ3FIF0AVkDnKbp3oiMW1c8AmvRZBcqbAhDUAvF52e9zx4WQM1r3oraVA==",
+ "dev": true,
+ "dependencies": {
+ "@vitest/spy": "1.5.2",
+ "@vitest/utils": "1.5.2",
+ "chai": "^4.3.10"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.5.2.tgz",
+ "integrity": "sha512-7IJ7sJhMZrqx7HIEpv3WrMYcq8ZNz9L6alo81Y6f8hV5mIE6yVZsFoivLZmr0D777klm1ReqonE9LyChdcmw6g==",
+ "dev": true,
+ "dependencies": {
+ "@vitest/utils": "1.5.2",
+ "p-limit": "^5.0.0",
+ "pathe": "^1.1.1"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.5.2.tgz",
+ "integrity": "sha512-CTEp/lTYos8fuCc9+Z55Ga5NVPKUgExritjF5VY7heRFUfheoAqBneUlvXSUJHUZPjnPmyZA96yLRJDP1QATFQ==",
+ "dev": true,
+ "dependencies": {
+ "magic-string": "^0.30.5",
+ "pathe": "^1.1.1",
+ "pretty-format": "^29.7.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.5.2.tgz",
+ "integrity": "sha512-xCcPvI8JpCtgikT9nLpHPL1/81AYqZy1GCy4+MCHBE7xi8jgsYkULpW5hrx5PGLgOQjUpb6fd15lqcriJ40tfQ==",
+ "dev": true,
+ "dependencies": {
+ "tinyspy": "^2.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.5.2.tgz",
+ "integrity": "sha512-sWOmyofuXLJ85VvXNsroZur7mOJGiQeM0JN3/0D1uU8U9bGFM69X1iqHaRXl6R8BwaLY6yPCogP257zxTzkUdA==",
+ "dev": true,
+ "dependencies": {
+ "diff-sequences": "^29.6.3",
+ "estree-walker": "^3.0.3",
+ "loupe": "^2.3.7",
+ "pretty-format": "^29.7.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.11.3",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
+ "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
+ "dev": true,
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-walk": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz",
+ "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/array-buffer-byte-length": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz",
+ "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.5",
+ "is-array-buffer": "^3.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/arraybuffer.prototype.slice": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz",
+ "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==",
+ "dev": true,
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.1",
+ "call-bind": "^1.0.5",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.22.3",
+ "es-errors": "^1.2.1",
+ "get-intrinsic": "^1.2.3",
+ "is-array-buffer": "^3.0.4",
+ "is-shared-array-buffer": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/assertion-error": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
+ "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
+ "dev": true,
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/available-typed-arrays": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
+ "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
+ "dev": true,
+ "dependencies": {
+ "possible-typed-array-names": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true
+ },
+ "node_modules/boolbase": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
+ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
+ "dev": true
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/cac": {
+ "version": "6.7.14",
+ "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
+ "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
+ "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
+ "dev": true,
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "set-function-length": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/chai": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz",
+ "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==",
+ "dev": true,
+ "dependencies": {
+ "assertion-error": "^1.1.0",
+ "check-error": "^1.0.3",
+ "deep-eql": "^4.1.3",
+ "get-func-name": "^2.0.2",
+ "loupe": "^2.3.6",
+ "pathval": "^1.1.1",
+ "type-detect": "^4.0.8"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/check-error": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz",
+ "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==",
+ "dev": true,
+ "dependencies": {
+ "get-func-name": "^2.0.2"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
+ "dev": true
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true
+ },
+ "node_modules/confbox": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.7.tgz",
+ "integrity": "sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==",
+ "dev": true
+ },
+ "node_modules/cross-spawn": {
+ "version": "6.0.5",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
+ "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
+ "dev": true,
+ "dependencies": {
+ "nice-try": "^1.0.4",
+ "path-key": "^2.0.1",
+ "semver": "^5.5.0",
+ "shebang-command": "^1.2.0",
+ "which": "^1.2.9"
+ },
+ "engines": {
+ "node": ">=4.8"
+ }
+ },
+ "node_modules/css-select": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
+ "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==",
+ "dev": true,
+ "dependencies": {
+ "boolbase": "^1.0.0",
+ "css-what": "^6.1.0",
+ "domhandler": "^5.0.2",
+ "domutils": "^3.0.1",
+ "nth-check": "^2.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/css-what": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
+ "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/cssom": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz",
+ "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==",
+ "dev": true
+ },
+ "node_modules/data-view-buffer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz",
+ "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.6",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/data-view-byte-length": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz",
+ "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/data-view-byte-offset": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz",
+ "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.6",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-eql": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz",
+ "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==",
+ "dev": true,
+ "dependencies": {
+ "type-detect": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/define-data-property": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+ "dev": true,
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/define-properties": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
+ "dev": true,
+ "dependencies": {
+ "define-data-property": "^1.0.1",
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/diff-sequences": {
+ "version": "29.6.3",
+ "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz",
+ "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==",
+ "dev": true,
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/dom-serializer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
+ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
+ "dev": true,
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.2",
+ "entities": "^4.2.0"
+ },
+ "funding": {
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+ }
+ },
+ "node_modules/domelementtype": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
+ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ]
+ },
+ "node_modules/domhandler": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
+ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
+ "dev": true,
+ "dependencies": {
+ "domelementtype": "^2.3.0"
+ },
+ "engines": {
+ "node": ">= 4"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
+ }
+ },
+ "node_modules/domutils": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
+ "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
+ "dev": true,
+ "dependencies": {
+ "dom-serializer": "^2.0.0",
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domutils?sponsor=1"
+ }
+ },
+ "node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/error-ex": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+ "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+ "dev": true,
+ "dependencies": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "node_modules/es-abstract": {
+ "version": "1.23.3",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz",
+ "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==",
+ "dev": true,
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.1",
+ "arraybuffer.prototype.slice": "^1.0.3",
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.7",
+ "data-view-buffer": "^1.0.1",
+ "data-view-byte-length": "^1.0.1",
+ "data-view-byte-offset": "^1.0.0",
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "es-set-tostringtag": "^2.0.3",
+ "es-to-primitive": "^1.2.1",
+ "function.prototype.name": "^1.1.6",
+ "get-intrinsic": "^1.2.4",
+ "get-symbol-description": "^1.0.2",
+ "globalthis": "^1.0.3",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.2",
+ "has-proto": "^1.0.3",
+ "has-symbols": "^1.0.3",
+ "hasown": "^2.0.2",
+ "internal-slot": "^1.0.7",
+ "is-array-buffer": "^3.0.4",
+ "is-callable": "^1.2.7",
+ "is-data-view": "^1.0.1",
+ "is-negative-zero": "^2.0.3",
+ "is-regex": "^1.1.4",
+ "is-shared-array-buffer": "^1.0.3",
+ "is-string": "^1.0.7",
+ "is-typed-array": "^1.1.13",
+ "is-weakref": "^1.0.2",
+ "object-inspect": "^1.13.1",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.5",
+ "regexp.prototype.flags": "^1.5.2",
+ "safe-array-concat": "^1.1.2",
+ "safe-regex-test": "^1.0.3",
+ "string.prototype.trim": "^1.2.9",
+ "string.prototype.trimend": "^1.0.8",
+ "string.prototype.trimstart": "^1.0.8",
+ "typed-array-buffer": "^1.0.2",
+ "typed-array-byte-length": "^1.0.1",
+ "typed-array-byte-offset": "^1.0.2",
+ "typed-array-length": "^1.0.6",
+ "unbox-primitive": "^1.0.2",
+ "which-typed-array": "^1.1.15"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
+ "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
+ "dev": true,
+ "dependencies": {
+ "get-intrinsic": "^1.2.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz",
+ "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==",
+ "dev": true,
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz",
+ "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==",
+ "dev": true,
+ "dependencies": {
+ "get-intrinsic": "^1.2.4",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-to-primitive": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
+ "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
+ "dev": true,
+ "dependencies": {
+ "is-callable": "^1.1.4",
+ "is-date-object": "^1.0.1",
+ "is-symbol": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
+ "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
+ "dev": true,
+ "hasInstallScript": true,
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.20.2",
+ "@esbuild/android-arm": "0.20.2",
+ "@esbuild/android-arm64": "0.20.2",
+ "@esbuild/android-x64": "0.20.2",
+ "@esbuild/darwin-arm64": "0.20.2",
+ "@esbuild/darwin-x64": "0.20.2",
+ "@esbuild/freebsd-arm64": "0.20.2",
+ "@esbuild/freebsd-x64": "0.20.2",
+ "@esbuild/linux-arm": "0.20.2",
+ "@esbuild/linux-arm64": "0.20.2",
+ "@esbuild/linux-ia32": "0.20.2",
+ "@esbuild/linux-loong64": "0.20.2",
+ "@esbuild/linux-mips64el": "0.20.2",
+ "@esbuild/linux-ppc64": "0.20.2",
+ "@esbuild/linux-riscv64": "0.20.2",
+ "@esbuild/linux-s390x": "0.20.2",
+ "@esbuild/linux-x64": "0.20.2",
+ "@esbuild/netbsd-x64": "0.20.2",
+ "@esbuild/openbsd-x64": "0.20.2",
+ "@esbuild/sunos-x64": "0.20.2",
+ "@esbuild/win32-arm64": "0.20.2",
+ "@esbuild/win32-ia32": "0.20.2",
+ "@esbuild/win32-x64": "0.20.2"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/execa": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
+ "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==",
+ "dev": true,
+ "dependencies": {
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^8.0.1",
+ "human-signals": "^5.0.0",
+ "is-stream": "^3.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^5.1.0",
+ "onetime": "^6.0.0",
+ "signal-exit": "^4.1.0",
+ "strip-final-newline": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=16.17"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
+ }
+ },
+ "node_modules/execa/node_modules/cross-spawn": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+ "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "dev": true,
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/execa/node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/execa/node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/execa/node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/execa/node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/for-each": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
+ "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==",
+ "dev": true,
+ "dependencies": {
+ "is-callable": "^1.1.3"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/function.prototype.name": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz",
+ "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1",
+ "functions-have-names": "^1.2.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/functions-have-names": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
+ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-func-name": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
+ "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==",
+ "dev": true,
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
+ "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
+ "dev": true,
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "has-proto": "^1.0.1",
+ "has-symbols": "^1.0.3",
+ "hasown": "^2.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-stream": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz",
+ "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==",
+ "dev": true,
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/get-symbol-description": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz",
+ "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.5",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/globalthis": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz",
+ "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==",
+ "dev": true,
+ "dependencies": {
+ "define-properties": "^1.1.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
+ "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
+ "dev": true,
+ "dependencies": {
+ "get-intrinsic": "^1.1.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true
+ },
+ "node_modules/has-bigints": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
+ "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+ "dev": true,
+ "dependencies": {
+ "es-define-property": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-proto": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
+ "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
+ "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "dev": true,
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "dev": true,
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/hosted-git-info": {
+ "version": "2.8.9",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
+ "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
+ "dev": true
+ },
+ "node_modules/html-escaper": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",
+ "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==",
+ "dev": true
+ },
+ "node_modules/htmlparser2": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz",
+ "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==",
+ "dev": true,
+ "funding": [
+ "https://github.com/fb55/htmlparser2?sponsor=1",
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3",
+ "domutils": "^3.1.0",
+ "entities": "^4.5.0"
+ }
+ },
+ "node_modules/human-signals": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
+ "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=16.17.0"
+ }
+ },
+ "node_modules/internal-slot": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz",
+ "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==",
+ "dev": true,
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "hasown": "^2.0.0",
+ "side-channel": "^1.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/is-array-buffer": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz",
+ "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
+ "dev": true
+ },
+ "node_modules/is-bigint": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz",
+ "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==",
+ "dev": true,
+ "dependencies": {
+ "has-bigints": "^1.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-boolean-object": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
+ "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-callable": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
+ "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.13.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
+ "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
+ "dev": true,
+ "dependencies": {
+ "hasown": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-data-view": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz",
+ "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==",
+ "dev": true,
+ "dependencies": {
+ "is-typed-array": "^1.1.13"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-date-object": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
+ "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
+ "dev": true,
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-negative-zero": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
+ "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-number-object": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz",
+ "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==",
+ "dev": true,
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-regex": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
+ "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-shared-array-buffer": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz",
+ "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-stream": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
+ "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
+ "dev": true,
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-string": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
+ "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==",
+ "dev": true,
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-symbol": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz",
+ "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==",
+ "dev": true,
+ "dependencies": {
+ "has-symbols": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-typed-array": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz",
+ "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==",
+ "dev": true,
+ "dependencies": {
+ "which-typed-array": "^1.1.14"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakref": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
+ "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
+ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
+ "dev": true
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true
+ },
+ "node_modules/js-tokens": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.0.tgz",
+ "integrity": "sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==",
+ "dev": true
+ },
+ "node_modules/json-parse-better-errors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
+ "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==",
+ "dev": true
+ },
+ "node_modules/linkedom": {
+ "version": "0.16.11",
+ "resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.16.11.tgz",
+ "integrity": "sha512-WgaTVbj7itjyXTsCvgerpneERXShcnNJF5VIV+/4SLtyRLN+HppPre/WDHRofAr2IpEuujSNgJbCBd5lMl6lRw==",
+ "dev": true,
+ "dependencies": {
+ "css-select": "^5.1.0",
+ "cssom": "^0.5.0",
+ "html-escaper": "^3.0.3",
+ "htmlparser2": "^9.1.0",
+ "uhyphen": "^0.2.0"
+ }
+ },
+ "node_modules/load-json-file": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz",
+ "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.1.2",
+ "parse-json": "^4.0.0",
+ "pify": "^3.0.0",
+ "strip-bom": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/local-pkg": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz",
+ "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==",
+ "dev": true,
+ "dependencies": {
+ "mlly": "^1.4.2",
+ "pkg-types": "^1.0.3"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/loupe": {
+ "version": "2.3.7",
+ "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz",
+ "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==",
+ "dev": true,
+ "dependencies": {
+ "get-func-name": "^2.0.1"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.10",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz",
+ "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.4.15"
+ }
+ },
+ "node_modules/memorystream": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz",
+ "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.10.0"
+ }
+ },
+ "node_modules/merge-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+ "dev": true
+ },
+ "node_modules/mimic-fn": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
+ "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/mlly": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.6.1.tgz",
+ "integrity": "sha512-vLgaHvaeunuOXHSmEbZ9izxPx3USsk8KCQ8iC+aTlp5sKRSoZvwhHh5L9VbKSaVC6sJDqbyohIS76E2VmHIPAA==",
+ "dev": true,
+ "dependencies": {
+ "acorn": "^8.11.3",
+ "pathe": "^1.1.2",
+ "pkg-types": "^1.0.3",
+ "ufo": "^1.3.2"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "dev": true
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.7",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
+ "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/nice-try": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
+ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
+ "dev": true
+ },
+ "node_modules/normalize-package-data": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
+ "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
+ "dev": true,
+ "dependencies": {
+ "hosted-git-info": "^2.1.4",
+ "resolve": "^1.10.0",
+ "semver": "2 || 3 || 4 || 5",
+ "validate-npm-package-license": "^3.0.1"
+ }
+ },
+ "node_modules/npm-run-all": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz",
+ "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "chalk": "^2.4.1",
+ "cross-spawn": "^6.0.5",
+ "memorystream": "^0.3.1",
+ "minimatch": "^3.0.4",
+ "pidtree": "^0.3.0",
+ "read-pkg": "^3.0.0",
+ "shell-quote": "^1.6.1",
+ "string.prototype.padend": "^3.0.0"
+ },
+ "bin": {
+ "npm-run-all": "bin/npm-run-all/index.js",
+ "run-p": "bin/run-p/index.js",
+ "run-s": "bin/run-s/index.js"
+ },
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/npm-run-path": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
+ "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
+ "dev": true,
+ "dependencies": {
+ "path-key": "^4.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/npm-run-path/node_modules/path-key": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
+ "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/nth-check": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
+ "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
+ "dev": true,
+ "dependencies": {
+ "boolbase": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/nth-check?sponsor=1"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.1",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
+ "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.assign": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz",
+ "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.5",
+ "define-properties": "^1.2.1",
+ "has-symbols": "^1.0.3",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/onetime": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
+ "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
+ "dev": true,
+ "dependencies": {
+ "mimic-fn": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz",
+ "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==",
+ "dev": true,
+ "dependencies": {
+ "yocto-queue": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parse-json": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
+ "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==",
+ "dev": true,
+ "dependencies": {
+ "error-ex": "^1.3.1",
+ "json-parse-better-errors": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
+ "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true
+ },
+ "node_modules/path-type": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz",
+ "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==",
+ "dev": true,
+ "dependencies": {
+ "pify": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/pathe": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
+ "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
+ "dev": true
+ },
+ "node_modules/pathval": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz",
+ "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==",
+ "dev": true,
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
+ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
+ "dev": true
+ },
+ "node_modules/pidtree": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz",
+ "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==",
+ "dev": true,
+ "bin": {
+ "pidtree": "bin/pidtree.js"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/pify": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
+ "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/pkg-types": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.1.0.tgz",
+ "integrity": "sha512-/RpmvKdxKf8uILTtoOhAgf30wYbP2Qw+L9p3Rvshx1JZVX+XQNZQFjlbmGHEGIm4CkVPlSn+NXmIM8+9oWQaSA==",
+ "dev": true,
+ "dependencies": {
+ "confbox": "^0.1.7",
+ "mlly": "^1.6.1",
+ "pathe": "^1.1.2"
+ }
+ },
+ "node_modules/possible-typed-array-names": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz",
+ "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.4.38",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
+ "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "nanoid": "^3.3.7",
+ "picocolors": "^1.0.0",
+ "source-map-js": "^1.2.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/pretty-format": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
+ "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==",
+ "dev": true,
+ "dependencies": {
+ "@jest/schemas": "^29.6.3",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^18.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/pretty-format/node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "18.3.0",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.0.tgz",
+ "integrity": "sha512-wRiUsea88TjKDc4FBEn+sLvIDesp6brMbGWnJGjew2waAc9evdhja/2LvePc898HJbHw0L+MTWy7NhpnELAvLQ==",
+ "dev": true
+ },
+ "node_modules/read-pkg": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz",
+ "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==",
+ "dev": true,
+ "dependencies": {
+ "load-json-file": "^4.0.0",
+ "normalize-package-data": "^2.3.2",
+ "path-type": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/regexp.prototype.flags": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz",
+ "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.6",
+ "define-properties": "^1.2.1",
+ "es-errors": "^1.3.0",
+ "set-function-name": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.8",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
+ "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
+ "dev": true,
+ "dependencies": {
+ "is-core-module": "^2.13.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.16.4",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.16.4.tgz",
+ "integrity": "sha512-kuaTJSUbz+Wsb2ATGvEknkI12XV40vIiHmLuFlejoo7HtDok/O5eDDD0UpCVY5bBX5U5RYo8wWP83H7ZsqVEnA==",
+ "dev": true,
+ "dependencies": {
+ "@types/estree": "1.0.5"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.16.4",
+ "@rollup/rollup-android-arm64": "4.16.4",
+ "@rollup/rollup-darwin-arm64": "4.16.4",
+ "@rollup/rollup-darwin-x64": "4.16.4",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.16.4",
+ "@rollup/rollup-linux-arm-musleabihf": "4.16.4",
+ "@rollup/rollup-linux-arm64-gnu": "4.16.4",
+ "@rollup/rollup-linux-arm64-musl": "4.16.4",
+ "@rollup/rollup-linux-powerpc64le-gnu": "4.16.4",
+ "@rollup/rollup-linux-riscv64-gnu": "4.16.4",
+ "@rollup/rollup-linux-s390x-gnu": "4.16.4",
+ "@rollup/rollup-linux-x64-gnu": "4.16.4",
+ "@rollup/rollup-linux-x64-musl": "4.16.4",
+ "@rollup/rollup-win32-arm64-msvc": "4.16.4",
+ "@rollup/rollup-win32-ia32-msvc": "4.16.4",
+ "@rollup/rollup-win32-x64-msvc": "4.16.4",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/safe-array-concat": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz",
+ "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "get-intrinsic": "^1.2.4",
+ "has-symbols": "^1.0.3",
+ "isarray": "^2.0.5"
+ },
+ "engines": {
+ "node": ">=0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safe-regex-test": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz",
+ "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.6",
+ "es-errors": "^1.3.0",
+ "is-regex": "^1.1.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/semver": {
+ "version": "5.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/set-function-length": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+ "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
+ "dev": true,
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/set-function-name": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
+ "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
+ "dev": true,
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "functions-have-names": "^1.2.3",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
+ "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==",
+ "dev": true,
+ "dependencies": {
+ "shebang-regex": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
+ "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/shell-quote": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz",
+ "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
+ "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.4",
+ "object-inspect": "^1.13.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true
+ },
+ "node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "dev": true,
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
+ "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/spdx-correct": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
+ "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==",
+ "dev": true,
+ "dependencies": {
+ "spdx-expression-parse": "^3.0.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/spdx-exceptions": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz",
+ "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==",
+ "dev": true
+ },
+ "node_modules/spdx-expression-parse": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
+ "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
+ "dev": true,
+ "dependencies": {
+ "spdx-exceptions": "^2.1.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/spdx-license-ids": {
+ "version": "3.0.17",
+ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz",
+ "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==",
+ "dev": true
+ },
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true
+ },
+ "node_modules/std-env": {
+ "version": "3.7.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz",
+ "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==",
+ "dev": true
+ },
+ "node_modules/string.prototype.padend": {
+ "version": "3.1.6",
+ "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.6.tgz",
+ "integrity": "sha512-XZpspuSB7vJWhvJc9DLSlrXl1mcA2BdoY5jjnS135ydXqLoqhs96JjDtCkjJEQHvfqZIp9hBuBMgI589peyx9Q==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trim": {
+ "version": "1.2.9",
+ "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz",
+ "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.0",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimend": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz",
+ "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimstart": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz",
+ "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/strip-bom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+ "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/strip-final-newline": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
+ "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/strip-literal": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.0.tgz",
+ "integrity": "sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==",
+ "dev": true,
+ "dependencies": {
+ "js-tokens": "^9.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/tinybench": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.8.0.tgz",
+ "integrity": "sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==",
+ "dev": true
+ },
+ "node_modules/tinypool": {
+ "version": "0.8.4",
+ "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz",
+ "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tinyspy": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz",
+ "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==",
+ "dev": true,
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/type-detect": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
+ "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/typed-array-buffer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz",
+ "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "es-errors": "^1.3.0",
+ "is-typed-array": "^1.1.13"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/typed-array-byte-length": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz",
+ "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "for-each": "^0.3.3",
+ "gopd": "^1.0.1",
+ "has-proto": "^1.0.3",
+ "is-typed-array": "^1.1.13"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typed-array-byte-offset": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz",
+ "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==",
+ "dev": true,
+ "dependencies": {
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.7",
+ "for-each": "^0.3.3",
+ "gopd": "^1.0.1",
+ "has-proto": "^1.0.3",
+ "is-typed-array": "^1.1.13"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typed-array-length": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz",
+ "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "for-each": "^0.3.3",
+ "gopd": "^1.0.1",
+ "has-proto": "^1.0.3",
+ "is-typed-array": "^1.1.13",
+ "possible-typed-array-names": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/ufo": {
+ "version": "1.5.3",
+ "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.3.tgz",
+ "integrity": "sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==",
+ "dev": true
+ },
+ "node_modules/uhyphen": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz",
+ "integrity": "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==",
+ "dev": true
+ },
+ "node_modules/unbox-primitive": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
+ "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-bigints": "^1.0.2",
+ "has-symbols": "^1.0.3",
+ "which-boxed-primitive": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/validate-npm-package-license": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+ "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
+ "dev": true,
+ "dependencies": {
+ "spdx-correct": "^3.0.0",
+ "spdx-expression-parse": "^3.0.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "5.2.10",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.10.tgz",
+ "integrity": "sha512-PAzgUZbP7msvQvqdSD+ErD5qGnSFiGOoWmV5yAKUEI0kdhjbH6nMWVyZQC/hSc4aXwc0oJ9aEdIiF9Oje0JFCw==",
+ "dev": true,
+ "dependencies": {
+ "esbuild": "^0.20.1",
+ "postcss": "^8.4.38",
+ "rollup": "^4.13.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite-node": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.5.2.tgz",
+ "integrity": "sha512-Y8p91kz9zU+bWtF7HGt6DVw2JbhyuB2RlZix3FPYAYmUyZ3n7iTp8eSyLyY6sxtPegvxQtmlTMhfPhUfCUF93A==",
+ "dev": true,
+ "dependencies": {
+ "cac": "^6.7.14",
+ "debug": "^4.3.4",
+ "pathe": "^1.1.1",
+ "picocolors": "^1.0.0",
+ "vite": "^5.0.0"
+ },
+ "bin": {
+ "vite-node": "vite-node.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/vitest": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.5.2.tgz",
+ "integrity": "sha512-l9gwIkq16ug3xY7BxHwcBQovLZG75zZL0PlsiYQbf76Rz6QGs54416UWMtC0jXeihvHvcHrf2ROEjkQRVpoZYw==",
+ "dev": true,
+ "dependencies": {
+ "@vitest/expect": "1.5.2",
+ "@vitest/runner": "1.5.2",
+ "@vitest/snapshot": "1.5.2",
+ "@vitest/spy": "1.5.2",
+ "@vitest/utils": "1.5.2",
+ "acorn-walk": "^8.3.2",
+ "chai": "^4.3.10",
+ "debug": "^4.3.4",
+ "execa": "^8.0.1",
+ "local-pkg": "^0.5.0",
+ "magic-string": "^0.30.5",
+ "pathe": "^1.1.1",
+ "picocolors": "^1.0.0",
+ "std-env": "^3.5.0",
+ "strip-literal": "^2.0.0",
+ "tinybench": "^2.5.1",
+ "tinypool": "^0.8.3",
+ "vite": "^5.0.0",
+ "vite-node": "1.5.2",
+ "why-is-node-running": "^2.2.2"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "@vitest/browser": "1.5.2",
+ "@vitest/ui": "1.5.2",
+ "happy-dom": "*",
+ "jsdom": "*"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/which": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+ "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+ "dev": true,
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "which": "bin/which"
+ }
+ },
+ "node_modules/which-boxed-primitive": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz",
+ "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==",
+ "dev": true,
+ "dependencies": {
+ "is-bigint": "^1.0.1",
+ "is-boolean-object": "^1.1.0",
+ "is-number-object": "^1.0.4",
+ "is-string": "^1.0.5",
+ "is-symbol": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-typed-array": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz",
+ "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==",
+ "dev": true,
+ "dependencies": {
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.7",
+ "for-each": "^0.3.3",
+ "gopd": "^1.0.1",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/why-is-node-running": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz",
+ "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==",
+ "dev": true,
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz",
+ "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==",
+ "dev": true,
+ "engines": {
+ "node": ">=12.20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ }
+ }
+}
diff --git a/packages/lustre/package.json b/packages/lustre/package.json
new file mode 100644
index 0000000..3cd6217
--- /dev/null
+++ b/packages/lustre/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "lustre-client-test",
+ "version": "0.1.0",
+ "description": "testing for client scripts, and using vitest experimental bench",
+ "scripts": {
+ "bench": "run-s build:bench bench:vitest",
+ "bench:vitest": "vitest bench --config ./vitest.config.js",
+
+ "build": "run-p build:test:**",
+ "build:bench": "run-s build:test:vdom",
+ "build:test:02": "cd examples/02-interactivity && gleam build",
+ "build:test:vdom": "cd test-apps/vdom-test-templates && gleam build",
+
+ "run:vitest": "vitest --config ./vitest.config.js",
+
+ "test": "run-s build \"run:vitest -- --run\"",
+ "test:02": "run-s build:test:02 \"run:vitest -- --run 02-interactivity.test\"",
+ "test:vdom": "run-s build:test:vdom \"run:vitest -- --run vdom.ffi \"",
+
+ "watch:test": "run-p \"watch:init:**\"",
+ "watch:init:build": "run-p build:test:**",
+ "watch:init:vitest": "run-s run:vitest"
+ },
+ "author": "Jacob Scearcy",
+ "license": "MIT",
+ "devDependencies": {
+ "esbuild": "^0.20.2",
+ "linkedom": "^0.16.11",
+ "npm-run-all": "^4.1.5",
+ "vitest": "^1.5.0"
+ }
+}
diff --git a/packages/lustre/pages/guide/01-quickstart.md b/packages/lustre/pages/guide/01-quickstart.md
new file mode 100644
index 0000000..6713967
--- /dev/null
+++ b/packages/lustre/pages/guide/01-quickstart.md
@@ -0,0 +1,422 @@
+# 01 Quickstart guide
+
+Welcome to the Lustre quickstart guide! This document should get you up to speed
+with the core ideas that underpin every Lustre application as well as how to get
+something on the screen.
+
+## What is a SPA?
+
+Lustre can be used to create HTML in many different contexts, but it is primarily
+designed to be used to build Single-Page Applications – or SPAs. SPAs are a type
+of Web application that render content primarily in the browser (rather than on
+the server) and, crucially, do not require a full page load when navigating
+between pages or loading new content.
+
+To help build these kinds of applications, Lustre comes with an opinionated
+runtime. Some of Lustre's core features include:
+
+- **Declarative rendering**: User interfaces are constructed using a declarative
+ API that describes HTML as a function of your application's state. This is in
+ contrast to more traditional imperative approaches to direct DOM mutation like
+ jQuery.
+
+- **State management**: If UIs are a function of state, then orchestrating state
+ changes is crucial! Lustre provides a simple message-based state management
+ system modelled after OTP [gen_servers](https://www.erlang.org/doc/design_principles/gen_server_concepts),
+ Gleam's [actors](https://hexdocs.pm/gleam_otp/gleam/otp/actor.html), and the
+ [Elm Architecture](https://guide.elm-lang.org/architecture/).
+
+- **Managed side effects**: Managing asynchronous operations like HTTP requests
+ and timers can be tricky when JavaScript is single-threaded. Lustre provides a
+ runtime to manage these side effects and let them communicate with your application
+ using the same messages as your update loop.
+
+## Your first Lustre program
+
+To get started, let's create a new Gleam application and add Lustre as a dependency.
+
+```sh
+gleam new app && cd app && gleam add lustre
+```
+
+By default, Gleam builds projects for the Erlang target unless told otherwise. We
+can change this by adding a `target` field to the `gleam.toml` file generated in
+the root of the project.
+
+```diff
+ name = "app"
++ target = "javascript"
+ version = "1.0.0"
+
+ ...
+```
+
+The simplest type of Lustre application is constructed with the `element` function.
+This produces an application that renders a static piece of content without the
+typical update loop.
+
+We can start by importing `lustre` and `lustre/element` and just rendering some
+text:
+
+```gleam
+import lustre
+import lustre/element
+
+pub fn main() {
+ let app = lustre.element(element.text("Hello, world!"))
+ let assert Ok(_) = lustre.start(app, "#app", Nil)
+
+ Nil
+}
+```
+
+Lustre has some official development tooling published in the
+[`lustre_dev_tools`](https://hexdocs.pm/lustre_dev_tools/) package. Most projects
+will probably want to add those too!
+
+> **Note**: the lustre_dev_tools development server watches your filesystem for
+> changes to your gleam code and can automatically reload the browser. For linux
+> users this requires [inotify-tools](https://github.com/inotify-tools/inotify-tools)
+> be installed. If you do not or cannot install this, the development server will
+> still run but it will not watch your files for changes.
+
+> **Note**: currently one of lustre_dev_tools' dependencies is not compatible with
+> the most recent version of `gleam_json`, making it impossible to install. To fix
+> this, add `gleam_json = "1.0.1"` as a dependency in your `gleam.toml` file.
+
+```sh
+gleam add --dev lustre_dev_tools
+```
+
+It's important to make sure the development tooling is added as a `--dev`
+dependency. This ensures they're never included in production builds of your app.
+
+To start a development server, we can run:
+
+```sh
+gleam run -m lustre/dev start
+```
+
+The first time you run this command might take a little while, but subsequent runs
+should be much faster!
+
+> **Note**: Lustre uses esbuild under the hood, and attempts to download the [right
+> binary for your platform](https://esbuild.github.io/getting-started/#download-a-build).
+> If you're not connected to the internet, on an unsupported platform, or don't
+> want Lustre to download the binary you can grab or build it yourself and place it
+> in `build/.lustre/bin/esbuild`.
+
+Once the server is up and running you should be able to visit http://localhost:1234
+and be greeted with your "Hello, world!" message.
+
+We mentioned Lustre has a declarative API for constructing HTML. Let's see what
+that looks like by building something slightly more complex.
+
+```gleam
+import lustre
+import lustre/attribute
+import lustre/element
+import lustre/element/html
+
+pub fn main() {
+ let app =
+ lustre.element(
+ html.div([], [
+ html.h1([], [element.text("Hello, world!")]),
+ html.figure([], [
+ html.img([attribute.src("https://cataas.com/cat")]),
+ html.figcaption([], [element.text("A cat!")])
+ ])
+ ])
+ )
+ let assert Ok(_) = lustre.start(app, "#app", Nil)
+
+ Nil
+}
+```
+
+Here we _describe_ the structure of the HTML we want to render, and leave the
+busywork to Lustre's runtime: that's what makes it declarative!
+
+"**Where are the templates?**" we hear you cry. Lustre doesn't have a separate
+templating syntax like JSX or HEEx for a few reasons (lack of metaprogramming
+built into Gleam, for one). Some folks might find this a bit odd at first, but
+we encourage you to give it a try. Realising that your UI is _just functions_
+can be a bit of a lightbulb moment as you build more complex applications.
+
+## Adding interactivity
+
+Rendering static HTML is great, but we said at the beginning Lustre was designed
+primarily for building SPAs – and SPAs are interactive! To do that we'll need
+to move on from `lustre.element` to the first of Lustre's application constructors
+that includes an update loop: `lustre.simple`.
+
+```gleam
+import gleam/int
+import lustre
+import lustre/element
+import lustre/element/html
+import lustre/event
+
+pub fn main() {
+ let app = lustre.simple(init, update, view)
+ let assert Ok(_) = lustre.start(app, "#app", Nil)
+
+ Nil
+}
+```
+
+There are three main building blocks to every interactive Lustre application:
+
+- A `Model` that represents your application's state and an `init` function
+ to create it.
+
+- A `Msg` type that represents all the different ways the outside world can
+ communicate with your application and an `update` function that modifies
+ your model in response to those messages.
+
+- A `view` function that renders your model to HTML.
+
+We'll build a simple counter application to demonstrate these concepts. Our
+model can be an `Int` and our `init` function will initialise it to `0`:
+
+```gleam
+pub type Model = Int
+
+fn init(_flags) -> Model {
+ 0
+}
+```
+
+> **Note**: The `init` function always takes a single argument! These are the "flags"
+> or start arguments you can pass in when your application is started with
+> `lustre.start`. For the time being, we can ignore them, but they're useful for
+> passing in configuration or other data when your application starts.
+
+The main update loop in a Lustre application revolves around messages passed in
+from the outside world. For our counter application, we'll have two messages to
+increment and decrement the counter:
+
+```gleam
+pub type Msg {
+ Increment
+ Decrement
+}
+
+pub fn update(model: Model, msg: Msg) -> Model {
+ case msg {
+ Increment -> model + 1
+ Decrement -> model - 1
+ }
+}
+```
+
+Each time a message is produced from an event listener, Lustre will call your
+`update` function with the current model and the incoming message. The result
+will be the new application state that is then passed to the `view` function:
+
+```gleam
+pub fn view(model: Model) -> element.Element(Msg) {
+ let count = int.to_string(model)
+
+ html.div([], [
+ html.button([event.on_click(Increment)], [
+ element.text("+")
+ ]),
+ element.text(count),
+ html.button([event.on_click(Decrement)], [
+ element.text("-")
+ ])
+ ])
+}
+```
+
+The above snippet attaches two click event listeners that produce an `Increment`
+or `Decrement` message when clicked. The Lustre runtime is responsible for
+attaching these event listeners and calling your `update` function with the
+resulting message.
+
+> **Note**: notice that the return type of `view` is `element.Element(Msg)`. The
+> type parameter `Msg` tells us the kinds of messages this element might produce
+> from events: type safety to the rescue!
+
+This forms the core of every Lustre application:
+
+- A model produces some view.
+- The view can produce messages in response to user interaction.
+- Those messages are passed to the update function to produce a new model.
+- ... and the cycle continues.
+
+## Talking to the outside world
+
+This "closed loop" of messages and updates works well if all we need is an
+interactive document, but many applications will also need to talk to the outside
+world – whether that's fetching data from an API, setting up a WebSocket connection,
+or even just setting a timer.
+
+Lustre manages these side effects through an abstraction called an `Effect`. In
+essence, effects are any functions that talk with the outside world and might
+want to send messages back to your application. Lustre lets you write your own
+effects, but for now we'll use a community package called
+[`lustre_http`](https://hexdocs.pm/lustre_http/index.html) to fetch a new cat image
+every time the counter is incremented.
+
+Because this is a separate package, make sure to add it to your project first.
+While we're here, we'll also add `gleam_json` so we can decode the response from
+the cat API:
+
+```sh
+$ gleam add lustre_http
+```
+
+Now we are introducing side effects, we need to graduate from `lustre.simple` to
+the more powerful `lustre.application` constructor.
+
+```gleam
+import gleam/dynamic
+import gleam/int
+import gleam/list
+import lustre
+import lustre/attribute
+import lustre/effect
+import lustre/element
+import lustre/element/html
+import lustre/event
+import lustre_http
+
+pub fn main() {
+ let app = lustre.application(init, update, view)
+ let assert Ok(_) = lustre.start(app, "#app", Nil)
+
+ Nil
+}
+```
+
+If you edited your previous counter app, you'll notice the program no longer
+compiles. Specifically, the type of our `init` and `update` functions are wrong
+for the new `lustre.application` constructor!
+
+In order to tell Lustre about what effects it should perform, these functions now
+need to return a _tuple_ of the new model and any effects. We can amend our `init`
+function like so:
+
+```gleam
+pub type Model {
+ Model(count: Int, cats: List(String))
+}
+
+fn init(_flags) -> #(Model, effect.Effect(Msg)) {
+ #(Model(0, []), effect.none())
+}
+```
+
+The `effect.none` function is a way of saying "no effects" – we don't need to do
+anything when the application starts. We've also changed our `Model` type from a
+simple type alias to a Gleam [record](https://tour.gleam.run/data-types/records/)
+that holds both the current count and a list of cat image URLs.
+
+In our `update` function, we want to fetch a new cat image every time the counter
+is incremented. To do this we need two things:
+
+- An `Effect` to describe the request the runtime should perform.
+- A variant of our `Msg` to handle the response.
+
+The `lustre_http` package has the effect side of things handled, so we just need
+to modify our `Msg` type to include a new variant for the response:
+
+```gleam
+pub type Msg {
+ UserIncrementedCount
+ UserDecrementedCount
+ ApiReturnedCat(Result(String, lustre_http.HttpError))
+}
+```
+
+> **Note**: Concerned your message type is too verbose? Read our thoughts on why
+> this is a good thing in our [state management guide](./02-state-management.html).
+
+Finally, we can modify our `update` function to also fetch a cat image when the
+counter is incremented and handle the response:
+
+```gleam
+pub fn update(model: Model, msg: Msg) -> #(Model, effect.Effect(Msg)) {
+ case msg {
+ UserIncrementedCount -> #(Model(..model, count: model.count + 1), get_cat())
+ UserDecrementedCount -> #(Model(..model, count: model.count - 1), effect.none())
+ ApiReturnedCat(Ok(cat)) -> #(Model(..model, cats: [cat, ..model.cats]), effect.none())
+ ApiReturnedCat(Error(_)) -> #(model, effect.none())
+ }
+}
+
+fn get_cat() -> effect.Effect(Msg) {
+ let decoder = dynamic.field("_id", dynamic.string)
+ let expect = lustre_http.expect_json(decoder, ApiReturnedCat)
+
+ lustre_http.get("https://cataas.com/cat?json=true", expect)
+}
+```
+
+> **Note**: The `get_cat` function returns an `Effect` that tells the runtime how
+> to fetch a cat image. It's important to know that the `get_cat` function doesn't
+> perform the request directly! This is why we need to add the `ApiReturnedCat` message
+> variant: the runtime needs to know what to do with the response when it arrives.
+
+This model of managed effects can feel cumbersome at first, but it comes with some
+benefits. Forcing side effects to produce a message means our message type naturally
+describes all the ways the world can communicate with our application; as an app
+grows being able to get this kind of overview is invaluable! It also means we can
+test our update loop in isolation from the runtime and side effects: we could write
+tests that verify a particular sequence of messages produces an expected model
+without needing to mock out HTTP requests or timers.
+
+Before we forget, let's also update our `view` function to actually display the
+cat images we're fetching:
+
+```gleam
+pub fn view(model: Model) -> element.Element(Msg) {
+ let count = int.to_string(model.count)
+
+ html.div([], [
+ html.button([event.on_click(UserIncrementedCount)], [
+ element.text("+")
+ ]),
+ element.text(count),
+ html.button([event.on_click(UserDecrementedCount)], [
+ element.text("-")
+ ]),
+ html.div(
+ [],
+ list.map(model.cats, fn(cat) {
+ html.img([attribute.src("https://cataas.com/cat/" <> cat)])
+ }),
+ ),
+ ])
+}
+```
+
+## Where to go from here
+
+Believe it or not, you've already seen about 80% of what Lustre has to offer! From
+these core concepts, you can build rich interactive applications that are predictable
+and maintainable. Where to go from here depends on what you want to build, and
+how you like to learn:
+
+- There are a number of [examples](https://github.com/lustre-labs/lustre/tree/main/examples)
+ if the Lustre repository that gradually introduce more complex applications
+ and ideas.
+
+- The [rest of this guide](./02-state-management.html) also continues to teach
+ Lustre's high-level concepts and best-practices.
+
+- Of course, if you want to dive in and start making things straight away, the
+ [API documentation](https://hexdocs.pm/lustre/lustre.html) is always handy to keep open.
+
+## Getting help
+
+If you're having trouble with Lustre or not sure what the right way to do
+something is, the best place to get help is the [Gleam Discord server](https://discord.gg/Fm8Pwmy).
+You could also open an issue on the [Lustre GitHub repository](https://github.com/lustre-labs/lustre/issues).
+
+While our docs are still a work in progress, the official [Elm guide](https://guide.elm-lang.org)
+is also a great resource for learning about the Model-View-Update architecture
+and the kinds of patterns that Lustre is built around.
diff --git a/packages/lustre/pages/guide/02-state-management.md b/packages/lustre/pages/guide/02-state-management.md
new file mode 100644
index 0000000..b29ac5c
--- /dev/null
+++ b/packages/lustre/pages/guide/02-state-management.md
@@ -0,0 +1,244 @@
+# 02 State management
+
+We saw in the quickstart guide that all Lustre applications are built around the
+Model-View-Update (MVU) architecture. This means that the state of the application
+is stored in a single, immutable data structure called the model, and updated as
+messages are dispatched to the runtime.
+
+The MVU architecture is an example of _unidirectional data flow_:
+
+- Your model describes the entire state of your application at a given point in
+ time.
+
+- The UI is a [pure](https://github.com/lustre-labs/lustre/blob/main/pages/hints/pure-functions.md)
+ function of that model: if the model doesn't change, the UI doesn't change.
+
+- Events from the outside world – user interaction, HTTP responses, ... – send
+ messages to an update function that constructs a new model.
+
+- The UI re-renders based on the new state.
+
+```text
+ +--------+
+ | |
+ | update |
+ | |
+ +--------+
+ ^ |
+ | |
+ Msg | | Model
+ | |
+ | v
++------+ +------------------------+
+| | Model | |
+| init |------------------------>| Lustre Runtime |
+| | | |
++------+ +------------------------+
+ ^ |
+ | |
+ Msg | | Model
+ | |
+ | v
+ +--------+
+ | |
+ | view |
+ | |
+ +--------+
+```
+
+This is in contrast to _bidirectional_ approaches to state management, where the
+UI can modify state directly. For some developers this can be a difficult idea
+to get used to, but it brings a number of benefits:
+
+- A **single source of truth** makes it easier to reason about the state of your
+ application. State management is lifted _out_ of UI code, letting it focus just
+ on presentation and making it easier to test and refactor.
+
+- Message-driven **declarative state updates** give you a holistic view of how
+ your application can change over time. Tracking incoming messages gives you a
+ history of state updates and can be serialised and logged for debugging or
+ testing purposes.
+
+- State updates are **pure**. We will learn more about this in the [next guide](./03-side-effects.html),
+ but for now it is enough to know that this means testing your state changes is
+ much easier because mocking messages is simpler than mocking side effects!
+
+The rest of this guide contains some learned-wisdom and best practices for managing
+state in Lustre applications.
+
+## The best model is not always a record
+
+It is overwhelmingly common to see the model of a Lustre application as a single
+record. This is a sensible place to start, but there are other options! Gleam's
+custom types allow us to model our data as disjoint variants. Using these as your
+application's model can be particularly useful when you have different states that
+do not need to persist across navigations:
+
+```gleam
+type Model {
+ LoggedIn(LoggedInModel)
+ Public(PublicModel)
+}
+
+type LoggedInModel {
+ ...
+}
+
+type PublicModel {
+ ...
+}
+```
+
+Here, we have a model that represents our application as either having a logged in
+user or just one of the public routes. This pushes us towards the great practice of
+[making impossible states impossible](https://github.com/stereobooster/pragmatic-types/blob/master/posts/making-impossible-states-impossible.md).
+Now, we can write separate update and view functions that only handle the states
+they care about.
+
+Another option is to use a _type alias_ to represent some state using existing
+Gleam types. It's important to remember that your model represents _application_
+state and not necessarily _page_ state. This can manifest as simple as aliasing
+Gleam's `Result` type or maybe a `Dict` representing loaded posts.
+
+## Messages not actions
+
+Lustre is not the first frontend framework to use the MVU architecture or to
+focus on dispatching messages to update state. State management libraries like
+Redux and Zustand follow a very similar pattern. The devil is in the details
+though, and these libraries often talk in terms of _actions_ but you'll see
+Elm and Lustre prefer the term _message_.
+
+Actions frame incoming events as _things to do_: "add a new todo", "make an HTTP
+request", etc. This can work well in the beginning, but as your application grows
+and the number of things you can do grows, naming messages as actions can become
+problematic.
+
+In particular, it encourages you to recursively call your `update` function with
+different messages when you want to compose behaviour. Gleam is a functional
+programming language: we should use functions to update our state, not message
+dispatching! Communicating through messages is a way for the _outside world_ to
+talk to our application, not for our applications to talk to themselves.
+
+A recursive update function makes it difficult to see the consequences of any one
+message as you need to trace through the recursive calls in your head to understand
+which messages are being dispatched and in what order.
+
+Instead, we recommend you name your messages according to a **Subject Verb Object**
+pattern. This frames messages based on who (or what) sent them, what state or
+"thing" they're working on, and what they did or want to do. Imagine a password
+reset form, the user can type in a new password and submit it and our app waits
+for a response. As a first-pass we might end up with something like this:
+
+```gleam
+type Msg {
+ SetPassword(String)
+ ResetPassword
+ PasswordReset(Result(Nil, String))
+}
+```
+
+This is quite muddled, and is compounded as we add more messages to our app
+(especially if they also relate to the password!). It's hard to tell from looking
+at our messages what our app might _really_ be doing: we'd have to dig into our
+`update` function and possibly our `view` to work out what our intent was. One
+super power of the MVU pattern is that we can look at our messages to get a
+holistic view of what our app can handle. Things become much clearer if we refactor
+this example to the Subject Verb Object naming pattern:
+
+```gleam
+type Msg {
+ UserUpdatedPassword(String)
+ UserRequestedPasswordReset
+ BackendResetPassword(Result(Nil, String))
+}
+```
+
+It's now immediately obvious at a glance:
+
+1. Where these messages are coming from (user interaction, the network, ...)
+2. What sort of event or intention they represent
+
+As our apps grow in size, we'll be thankful for this clarity!
+
+## View functions not components
+
+Although Lustre does have a way to create encapsulated stateful components (something
+we sorely missed in Elm) it shouldn't be the default. The word "component" is a bit
+overloaded in the frontend world, so for clarity Lustre considers _components_
+as stateful nested Model-View-Update applications and calls stateless functions
+that return `Element`s _view functions_.
+
+The best Lustre code bases take the lessons learned from similar languages like
+Elm, Erlang, and Elixir and keep the number of components low and the number of
+simple view functions much higher. If you're coming from a typical frontend
+framework the idea of eschewing stateful components might seem quite strange, but
+there are some tangible benefits to this approach:
+
+- **Favouring view functions forces us to be intentional with state.**
+
+ Frameworks often make it easy to add state to components, which in turn makes
+ it easy to add state without really thinking about whether we need it or whether
+ we're taking the best approach.
+
+ View functions on the other hand _only_ have arguments, and adding a new argument
+ is a much more deliberate act. This gives us a chance to consider whether we're
+ modelling things the right way or whether we're trying to do too much.
+
+- **Components are bad for code organisation.**
+
+ It can be tempting to use components as a way to organise code. You might see
+ this commonly in React and Vue codebases: you have a folder for components, a
+ folder for hooks, and so on. Using components as a means of organisation often
+ leads to us drawing weird boundaries around our code and spreading out things
+ that should be together.
+
+ By sticking to view functions we're much more likely to keep code grouped based
+ on _what it does_ rather than what it _is_ and this approach is much more idiomatic
+ to Gleam on the whole, and also an approach favoured by Elm and Elixir alike.
+
+- **Avoiding components makes your code easier to test.**
+
+ When we reach for components too soon or too frequently, we often end up needing
+ to pull in a complete E2E testing framework to make sure our code is behaving
+ correctly, or we might end up exposing our components' internals for testing:
+ defeating the purpose of encapsulation in the first place!
+
+ By sticking to plain view functions and functions to transform data before
+ rendering, we end up with a codebase that is much easier to test with Gleam's
+ available testing tools.
+
+- **Overusing components makes refactoring more challenging.**
+
+ Imagine you have a table component with tabs to switch between different views.
+ If some time in the future you decide to pull the tabs out so they can be
+ rendered elsewhere on the page you'll discover that the tabs' state was tightly
+ coupled to the table. Now we are forced to refactor the table component so the
+ tab state can be passed in as an attribute. We'll also need to refactor the
+ _parent_ to contain the state of the tabs so it can be passed down to both
+ components.
+
+ By avoiding components this sort of refactoring becomes simpler: we were already
+ managing the state further up the component tree so moving things around is
+ much less painful.
+
+- **Creating components is more boilerplate.**
+
+ Components share the same shape as any other Lustre application. That means for
+ any component you want to create, you also need to define an `init`, `update`,
+ and `view` function, a `Model` type, and a `Msg` type. If you find yourself
+ thinking "wow, this is a lot of boilerplate just to do X" then listen to your
+ gut!
+
+## Related examples
+
+If you'd like to see some of the ideas in action, we have a number of examples
+that demonstrate how to use Lustre in practice:
+
+- [`02-interactivity`](https://github.com/lustre-labs/lustre/tree/main/examples/02-interactivity)
+- [`03-controlled-inputs`](https://github.com/lustre-labs/lustre/tree/main/examples/03-controlled-inputs)
+
+## Getting help
+
+If you're having trouble with Lustre or not sure what the right way to do
+something is, the best place to get help is the [Gleam Discord server](https://discord.gg/Fm8Pwmy).
+You could also open an issue on the [Lustre GitHub repository](https://github.com/lustre-labs/lustre/issues).
diff --git a/packages/lustre/pages/guide/03-side-effects.md b/packages/lustre/pages/guide/03-side-effects.md
new file mode 100644
index 0000000..f873724
--- /dev/null
+++ b/packages/lustre/pages/guide/03-side-effects.md
@@ -0,0 +1,268 @@
+# 03 Side effects
+
+Lustre's implementation of the Model-View-Update architecture includes one
+additional piece of the puzzle: managed side effects. If we take the MVU diagram
+from the previous guide and upgrade it to include managed effects, it looks like
+this:
+
+```text
+ +--------+
+ | |
+ | update |
+ | |
+ +--------+
+ ^ |
+ | |
+ Msg | | #(Model, Effect(msg))
+ | |
+ | v
++------+ +------------------------+
+| | #(Model, Effect(msg)) | |
+| init |------------------------>| Lustre Runtime |
+| | | |
++------+ +------------------------+
+ ^ |
+ | |
+ Msg | | Model
+ | |
+ | v
+ +--------+
+ | |
+ | view |
+ | |
+ +--------+
+```
+
+Well what does managed effects mean, exactly? In Lustre, we expect your `init`,
+`update`, and `view` functions to be [_pure_](https://github.com/lustre-labs/lustre/blob/main/pages/hints/pure-functions.md).
+That means they shouldn't perform side effects like making an HTTP request or writing
+to local storage: we should be able to run your functions 100 times with the same
+input and get the same output every time!
+
+Of course, in real applications performing HTTP requests and writing to local
+storage turn out to be quite useful things to do. If we shouldn't perform side
+effects in our code how do we do them then? Lustre has an [`Effect`](https://hexdocs.pm/lustre/lustre/effect.html)
+type that _tells the runtime what side effects to perform_. So we say "Hey, I
+want to make an HTTP request to this URL and when you get the response, dispatch
+this message to me". The runtime takes care of performing the side effect and
+turning the result into something our `update` function understands.
+
+## Why managed effects?
+
+This can feel like a lot of ceremony to go through just to make an HTTP request.
+The natural question is: why not just let us make these requests ourselves?
+
+Managed effects have a number of benefits that come from _separating our programs
+from the outside world_:
+
+1. **Predictability**: by keeping side effects out of our `update` function, we
+ can be confident that our application's state is only ever changed in one
+ place. This makes it easier to reason about our code and track down bugs.
+
+2. **Testability**: because our application code is pure, we can test it without
+ needing to mock out HTTP services or browser APIs. We can test our `update`
+ function, for example, by passing in a sequence of messages: no network mocks
+ required!
+
+3. **Reusability**: Lustre applications can run in a variety of environments and
+ contexts. The more we push platform-specific code into managed effects, the
+ easier time we'll have running our application as a [server component](https://hexdocs.pm/lustre/lustre/server_component.html)
+ or as a static site.
+
+## Packages for common effects
+
+The community has started to build packages that cover common side effects. For
+many applications it's enough to drop these packages in and start using them
+without needing to write any custom effects.
+
+> **Note**: _all_ of these packages are community maintained and unrelated to the
+> core Lustre organisation. If you run into issues please open an issue on the
+> package's repository!
+
+- [`lustre_http`](https://hexdocs.pm/lustre_http/) lets you make HTTP requests
+ and describe what responses to expect from them.
+
+- [`lustre_websocket`](https://hexdocs.pm/lustre_websocket/) handles WebSocket
+ connections and messages.
+
+- [`modem`](https://hexdocs.pm/modem/) and [`lustre_routed`](https://hexdocs.pm/lustre_routed/)
+ are two packages that help you manage navigation and routing.
+
+- [`lustre_animation`](https://hexdocs.pm/lustre_animation/) is a simple package
+ for interpolating between values over time.
+
+## Running effects
+
+We know that effects need to be performed by the runtime, but how does the runtime
+know when we want it to run an effect? If you have been using the `lustre.simple`
+application constructor until now, it is time to upgrade to
+[`lustre.application`](https://hexdocs.pm/lustre/lustre.html#application)!
+
+Full Lustre applications differ from simple applications in one important way by
+returning a tuple of `#(Model, Effect(Msg))` from your `init` and `update`
+functions:
+
+```gleam
+pub fn simple(
+ init: fn(flags) -> model,
+ update: fn(model, msg) -> model,
+ view: fn(model) -> Element(msg),
+) -> App(flags, model, msg)
+
+pub fn application(
+ init: fn(flags) -> #(model, Effect(msg)),
+ update: fn(model, msg) -> #(model, Effect(msg)),
+ view: fn(model) -> Element(msg),
+) -> App(flags, model, msg)
+```
+
+We can, for example, launch an HTTP request on application start by using `lustre_http.get`
+in our `init` function:
+
+```gleam
+fn init(_flags) {
+ let model = Model(...)
+ let get_ip = lustre_http.get(
+ "https://api.ipify.org",
+ ApiReturnedIpAddress
+ )
+
+ #(model, get_ip)
+}
+```
+
+> **Note**: to tell the runtime we _don't_ want to perform any side effects this
+> time, we can use [`effect.none()`](https://hexdocs.pm/lustre/lustre/effect.html#none).
+
+## Writing your own effects
+
+When you need to do something one of the existing packages doesn't cover, you need
+to write your own effect. You can do that by passing a callback to
+[`effect.from`](https://hexdocs.pm/lustre/lustre/effect.html#from). Custom effects
+are called with an argument – commonly called `dispatch` – that you can use to
+send messages back to your application's `update` function.
+
+Below is an example of a custom effect that reads a value from local storage:
+
+```js
+// ffi.mjs
+import { Ok, Error } from "./gleam.mjs";
+
+export function read(key) {
+ const value = window.localStorage.getItem(key);
+ return value ? new Ok(value) : new Error(undefined);
+}
+```
+
+```gleam
+fn read(key: String, to_msg: fn(Result(String, Nil)) -> msg) -> Effect(msg) {
+ effect.from(fn(dispatch) {
+ do_read(key)
+ |> to_msg
+ |> dispatch
+ })
+}
+
+@external(javascript, "ffi.mjs", "read")
+fn do_read(key: String) -> Result(String, Nil) {
+ Error(Nil)
+}
+```
+
+> **Note**: we provide a default implementation of the `do_read` function that
+> always fails. Where possible it's good to provide an implementation for all of
+> Gleam's targets. This makes it much easier to run your code as a
+> [server component](https://hexdocs.pm/lustre/lustre/server_component.html) in
+> the future.
+
+### Effects that touch the DOM
+
+Lustre runs all your side effects after your `update` function returns but _before_
+your `view` function is called. A common bug folks run into is trying to interact
+with a particular element in the DOM before it's had a chance to render. As a
+rule of thumb, you should _always_ wrap custom effects that interact with the DOM
+in a [`requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame)
+call to ensure the DOM has had a chance to update first.
+
+### Effects without dispatch
+
+So far, we have seen side effects that are expected to _return something_ to our
+program. If we fire an HTTP request, it wouldn't be much use if we couldn't get
+the response back! Sometimes folks wrongly assume effects _must_ use the `dispatch`
+function they're given, but this isn't true!
+
+It's also totally valid to write effects that don't dispatch any messages. Earlier
+we saw an example of how to read from local storage, we might also want an effect
+to _write_ to local storage and there's not much to dispatch in that case!
+
+```js
+// ffi.mjs
+export function write(key, value) {
+ window.localStorage.setItem(key, value);
+}
+```
+
+```gleam
+// app.gleam
+fn write(key: String, value: String) -> Effect(msg) {
+ effect.from(fn(_) {
+ do_write(key, value)
+ })
+}
+
+@external(javascript, "ffi.mjs", "write")
+fn do_write(key: String, value: String) -> Nil {
+ Nil
+}
+```
+
+### Effects with multiple dispatch
+
+Similar to effects that don't dispatch any messages, some folks skip over the fact
+effects can dispatch _multiple_ messages. Packages like [`lustre_websocket`](https://hexdocs.pm/lustre_websocket/)
+and [`modem`](https://hexdocs.pm/modem/) set up effects that will dispatch many
+messages over the lifetime of your program.
+
+Once you have a reference to that `dispatch` function, you're free to call it as
+many times as you want!
+
+```js
+// ffi.mjs
+export function every(interval, cb) {
+ window.setInterval(cb, interval);
+}
+```
+
+```gleam
+// app.gleam
+fn every(interval: Int, tick: msg) -> Effect(msg) {
+ effect.from(fn(dispatch) {
+ do_every(interval, fn() {
+ dispatch(tick)
+ })
+ })
+}
+
+@external(javascript, "ffi.mjs", "every")
+fn do_every(interval: Int, cb: fn() -> Nil) -> Nil {
+ Nil
+}
+```
+
+Here we set up an effect that will continuously dispatch a `tick` message at a
+fixed interval.
+
+## Related examples
+
+If you'd like to see some of the ideas in action, we have a number of examples
+that demonstrate how Lustre's effects system works in practice:
+
+- [`05-http-requests`](https://github.com/lustre-labs/lustre/tree/main/examples/05-http-requests)
+- [`06-custom-effects`](https://github.com/lustre-labs/lustre/tree/main/examples/06-custom-effects)
+- [`07-routing`](https://github.com/lustre-labs/lustre/tree/main/examples/07-routing)
+
+## Getting help
+
+If you're having trouble with Lustre or not sure what the right way to do
+something is, the best place to get help is the [Gleam Discord server](https://discord.gg/Fm8Pwmy).
+You could also open an issue on the [Lustre GitHub repository](https://github.com/lustre-labs/lustre/issues).
diff --git a/packages/lustre/pages/guide/04-server-side-rendering.md b/packages/lustre/pages/guide/04-server-side-rendering.md
new file mode 100644
index 0000000..27a2d89
--- /dev/null
+++ b/packages/lustre/pages/guide/04-server-side-rendering.md
@@ -0,0 +1,216 @@
+# 04 Server-side rendering
+
+Up until now, we have focused on Lustre's ability as a framework for building
+Single Page Applications (SPAs). While Lustre's development and feature set is
+primarily focused on SPA development, that doesn't mean it can't be used on the
+backend as well! In this guide we'll set up a small [mist](https://hexdocs.pm/mist/)
+server that renders some static HTML using Lustre.
+
+## Setting up the project
+
+We'll start by adding the dependencies we need and scaffolding the HTTP server.
+Besides Lustre and Mist, we also need `gleam_erlang` (to keep our application
+alive) and `gleam_http` (for types and functions to work with HTTP requests and
+responses):
+
+```sh
+gleam new app && cd app && gleam add gleam_erlang gleam_http lustre mist
+```
+
+Besides imports for `mist` and `gleam_http` modules, we also need to import some
+modules to render HTML with Lustre. Importantly, we _don't_ need anything from the
+main `lustre` module: we're not building an application with a runtime!
+
+```gleam
+import gleam/bytes_builder
+import gleam/erlang/process
+import gleam/http/request.{type Request}
+import gleam/http/response.{type Response}
+import lustre/element
+import lustre/element/html.{html}
+import mist.{type Connection, type ResponseData}
+```
+
+We'll modify Mist's example and write a simple request handler that responds to
+requests to `/greet/:name` with a greeting message:
+
+```gleam
+pub fn main() {
+ let empty_body = mist.Bytes(bytes_builder.new())
+ let not_found = response.set_body(response.new(404), empty_body)
+
+ let assert Ok(_) =
+ fn(req: Request(Connection)) -> Response(ResponseData) {
+ case request.path_segments(req) {
+ ["greet", name] -> greet(name)
+ _ -> not_found
+ }
+ }
+ |> mist.new
+ |> mist.port(3000)
+ |> mist.start_http
+
+ process.sleep_forever()
+}
+```
+
+Let's take a peek inside that `greet` function:
+
+```gleam
+fn greet(name: String) -> Response(ResponseData) {
+ let res = response.new(200)
+ let html =
+ html([], [
+ html.head([], [html.title([], "Greetings!")]),
+ html.body([], [
+ html.h1([], [html.text("Hey there, " <> name <> "!")])
+ ])
+ ])
+
+ response.set_body(res,
+ html
+ |> element.to_document_string
+ |> bytes_builder.from_string
+ |> mist.Bytes
+ )
+}
+```
+
+The `lustre/element` module has functions for rendering Lustre elements to a
+string (or string builder); the `to_document_string` function helpfully prepends
+the `` declaration to the output.
+
+It's important to realise that `element.to_string` and `element.to_document_string`
+can render _any_ Lustre element! This means you could take the `view` function
+from your client-side SPA and render it server-side, too.
+
+## Hydration
+
+If we know we can render our apps server-side, the next logical question is how
+do we handle _hydration_? Hydration is the process of taking the static HTML
+generated by the server and turning it into a fully interactive client application,
+ideally doing as little work as possible.
+
+Most frameworks today support hydration or some equivalent, for example by
+serialising the state of each component into the HTML and then picking up where
+the server left off. Lustre doesn't have a built-in hydration mechanism, but
+because of the way it works, it's easy to implement one yourself!
+
+We've said many times now that in Lustre, your `view` is just a
+[pure function](https://github.com/lustre-labs/lustre/blob/main/pages/hints/pure-functions.md)
+of your model. We should produce the same HTML every time we call `view` with the
+same model, no matter how many times we call it.
+
+Let's use that to our advantage! We know our app's `init` function is responsible
+for producing the initial model, so all we need is a way to make sure the initial
+model on the client is the same as what the server used to render the page.
+
+```gleam
+pub fn view(model: Int) -> Element(Msg) {
+ let count = int.to_string(model)
+
+ html.div([], [
+ html.button([event.on_click(Decr)], [html.text("-")]),
+ html.button([event.on_click(Incr)], [html.text("+")]),
+ html.p([], [html.text("Count: " <> count)])
+ ])
+}
+```
+
+We've seen the counter example a thousand times over now, but it's a good example
+to show off how simple hydration can be. The `view` function produces some HTML
+with events attached, but we already know Lustre can render _any_ element to a
+string so that shouldn't be a problem.
+
+Let's imagine our HTTP server responds with the following HTML:
+
+```gleam
+import app/counter
+import gleam/bytes_builder
+import gleam/http/response.{type Response}
+import gleam/json
+import lustre/attribute
+import lustre/element.{type Element}
+import lustre/element/html.{html}
+import mist.{type ResponseData}
+
+fn app() -> Response(ResponseData) {
+ let res = response.new(200)
+
+ let model = 5
+ let html =
+ html([], [
+ html.head([], [
+ html.script([attribute.type_("module"), attribute.src("...")], ""),
+ html.script([attribute.type_("application/json"), attribute.id("model")],
+ json.int(model)
+ |> json.to_string
+ )
+ ]),
+ html.body([], [
+ html.div([attribute.id("app")], [
+ counter.view(model)
+ ])
+ ])
+ ])
+
+ response.set_body(res,
+ html
+ |> element.to_document_string
+ |> bytes_builder.from_string
+ |> mist.Bytes
+ )
+}
+```
+
+We've rendered the shell of our application, as well as the counter using `5` as
+the initial model. Importantly, we've included a `
+
+
+
+
+
+