aboutsummaryrefslogtreecommitdiff
path: root/docs/src
diff options
context:
space:
mode:
Diffstat (limited to 'docs/src')
-rw-r--r--docs/src/app.ffi.mjs103
-rw-r--r--docs/src/app.gleam134
-rw-r--r--docs/src/app/effects.gleam0
-rw-r--r--docs/src/app/layout.gleam222
-rw-r--r--docs/src/app/page/api/lustre.gleam116
-rw-r--r--docs/src/app/page/api/lustre/attribute.gleam365
-rw-r--r--docs/src/app/page/api/lustre/effect.gleam61
-rw-r--r--docs/src/app/page/api/lustre/element.gleam88
-rw-r--r--docs/src/app/page/api/lustre/element/html.gleam1083
-rw-r--r--docs/src/app/page/api/lustre/element/svg.gleam532
-rw-r--r--docs/src/app/page/api/lustre/event.gleam188
-rw-r--r--docs/src/app/page/docs/components.gleam19
-rw-r--r--docs/src/app/page/docs/managing_state.gleam19
-rw-r--r--docs/src/app/page/docs/quickstart.gleam21
-rw-r--r--docs/src/app/page/docs/server_side_rendering.gleam19
-rw-r--r--docs/src/app/page/docs/side_effects.gleam19
-rw-r--r--docs/src/app/ui/hooks.gleam120
-rw-r--r--docs/src/app/ui/markdown.gleam34
-rw-r--r--docs/src/app/ui/radix.gleam74
-rw-r--r--docs/src/highlight.ffi.mjs151
-rw-r--r--docs/src/markdown.ffi.mjs164
21 files changed, 822 insertions, 2710 deletions
diff --git a/docs/src/app.ffi.mjs b/docs/src/app.ffi.mjs
index f5e7f29..3983f27 100644
--- a/docs/src/app.ffi.mjs
+++ b/docs/src/app.ffi.mjs
@@ -1,95 +1,10 @@
-import { fromMarkdown } from "mdast-util-from-markdown";
-import { attribute } from "../lustre/lustre/attribute.mjs";
-import { element, text } from "../lustre/lustre/element.mjs";
-import { List } from "./gleam.mjs";
-import * as Markdown from "./app/ui/markdown.mjs";
-
-// Parsing markdown and then walking the AST is expensive so we're going to trade
-// some memory for speed and cache the results.
-const cache = new Map();
-
-// fn parse_markdown(md: String) -> #(List(Element(msg)), List(Element(msg)))
-export const parse_markdown = (md) => {
- if (cache.has(md)) return cache.get(md);
-
- const ast = fromMarkdown(md);
- const summary = [];
- const content = ast.children.map(function to_lustre_element(node) {
- switch (node.type) {
- case "code":
- return Markdown.code(node.value);
-
- case "emphasis":
- return Markdown.emphasis(
- List.fromArray(node.children.map(to_lustre_element))
- );
-
- case "heading": {
- const [title, rest] = node.children[0].value.split("|");
- const tags = List.fromArray(rest ? rest.trim().split(" ") : []);
- const id =
- /^[A-Z]/.test(title.trim()) && node.depth === 3
- ? `${title.toLowerCase().trim().replace(/\s/g, "-")}-type`
- : `${title.toLowerCase().trim().replace(/\s/g, "-")}`;
-
- if (node.depth > 1) {
- summary.push(
- element(
- "a",
- List.fromArray([
- attribute("href", `#${id}`),
- attribute("class", "text-sm text-gray-400 no-underline"),
- attribute("class", "hover:text-gray-700 hover:underline"),
- attribute(
- "class",
- node.depth === 2 ? `mt-4 first:mt-0` : `ml-2`
- ),
- ]),
- List.fromArray([text(title.trim())])
- )
- );
- }
-
- return Markdown.heading(node.depth, title.trim(), tags, id);
- }
-
- case "inlineCode":
- return Markdown.inline_code(node.value);
-
- case "link":
- return Markdown.link(node.url, List.fromArray(node.children));
-
- case "list":
- return Markdown.list(
- !!node.ordered,
- List.fromArray(node.children.map(to_lustre_element))
- );
-
- case "listItem":
- return Markdown.list_item(
- List.fromArray(node.children.map(to_lustre_element))
- );
-
- case "paragraph":
- return Markdown.paragraph(
- List.fromArray(node.children.map(to_lustre_element))
- );
-
- case "strong":
- return Markdown.strong(
- List.fromArray(node.children.map(to_lustre_element))
- );
-
- case "text":
- return Markdown.text(node.value);
-
- default:
- return Markdown.text("");
- }
- });
-
- const result = [List.fromArray(content), List.fromArray(summary)];
- cache.set(md, result);
-
- return result;
+export function base() {
+ return import.meta.env.BASE_URL;
+}
+
+export const fetch_post = (path, dispatch) => {
+ fetch(`${import.meta.env.BASE_URL}page/${path.slice(1)}.md`)
+ .then((res) => res.text())
+ .then((content) => dispatch(content))
+ .catch(console.error);
};
diff --git a/docs/src/app.gleam b/docs/src/app.gleam
index a84d040..d448ef7 100644
--- a/docs/src/app.gleam
+++ b/docs/src/app.gleam
@@ -1,24 +1,18 @@
// IMPORTS ---------------------------------------------------------------------
-import app/page/api/lustre as lustre_api
-import app/page/api/lustre/attribute as attribute_api
-import app/page/api/lustre/effect as effect_api
-import app/page/api/lustre/element as element_api
-import app/page/api/lustre/element/html as html_api
-import app/page/api/lustre/element/svg as svg_api
-import app/page/api/lustre/event as event_api
-import app/page/docs/components as components_docs
-import app/page/docs/managing_state as managing_state_docs
-import app/page/docs/quickstart as quickstart_docs
-import app/page/docs/server_side_rendering as server_side_rendering_docs
-import app/page/docs/side_effects as side_effects_docs
+import app/layout
+import gleam/function
+import gleam/map.{Map}
import lustre
+import lustre/attribute
+import lustre/effect.{Effect}
import lustre/element.{Element}
+import lustre/element/html
// MAIN ------------------------------------------------------------------------
pub fn main(route: Route) -> fn(Msg) -> Nil {
- let app = lustre.simple(init, update, view)
+ let app = lustre.application(init, update, view)
let assert Ok(dispatch) = lustre.start(app, "body", route)
dispatch
@@ -27,51 +21,117 @@ pub fn main(route: Route) -> fn(Msg) -> Nil {
// MODEL -----------------------------------------------------------------------
type Model {
- Model(route: Route)
+ Model(route: Route, content: String, history: Map(String, String))
}
-fn init(route: Route) -> Model {
- Model(route)
+fn init(route: Route) -> #(Model, Effect(Msg)) {
+ let content = ""
+ let history = map.new()
+ let model = Model(route, content, history)
+ let effects = case route.path {
+ "/" -> fetch_post("/docs/quickstart", history)
+ _ -> fetch_post(route.path, history)
+ }
+
+ #(model, effects)
}
// UPDATE ----------------------------------------------------------------------
pub type Msg {
OnRouteChange(Route)
+ GotPost(String, String)
}
pub type Route {
Route(path: String, hash: String)
}
-fn update(model: Model, msg: Msg) -> Model {
+fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
+ // We need to do this because Gleam doesn't support record field access in
+ // guards just yet.
+ let current_path = model.route.path
+
case msg {
- OnRouteChange(route) -> Model(..model, route: route)
+ OnRouteChange(Route(path: "/", hash: _) as route) -> #(
+ Model(..model, route: route),
+ fetch_post("/docs/quickstart", model.history),
+ )
+
+ // Only fetch the markdown
+ OnRouteChange(Route(path: path, hash: _) as route) if path != current_path -> #(
+ Model(..model, route: route),
+ fetch_post(path, model.history),
+ )
+
+ GotPost(path, content) -> #(
+ Model(
+ ..model,
+ content: content,
+ history: map.insert(model.history, path, content),
+ ),
+ effect.none(),
+ )
+ }
+}
+
+fn fetch_post(path: String, history: Map(String, String)) -> Effect(Msg) {
+ use dispatch <- effect.from
+
+ case map.get(history, path) {
+ Ok(content) -> dispatch(GotPost(path, content))
+ Error(_) ->
+ fetch_post_content(path, function.compose(GotPost(path, _), dispatch))
}
}
+@external(javascript, "./app.ffi.mjs", "fetch_post")
+fn fetch_post_content(path: String, dispatch: fn(String) -> Nil) -> Nil
+
// VIEW ------------------------------------------------------------------------
fn view(model: Model) -> Element(Msg) {
case model.route.path {
- "/" -> quickstart_docs.view()
-
- "/api" -> lustre_api.view()
- "/api/lustre" -> lustre_api.view()
- "/api/lustre/attribute" -> attribute_api.view()
- "/api/lustre/effect" -> effect_api.view()
- "/api/lustre/element" -> element_api.view()
- "/api/lustre/element/html" -> html_api.view()
- "/api/lustre/element/svg" -> svg_api.view()
- "/api/lustre/event" -> event_api.view()
-
- "/docs" -> quickstart_docs.view()
- "/docs/quickstart" -> quickstart_docs.view()
- "/docs/managing-state" -> managing_state_docs.view()
- "/docs/side-effects" -> side_effects_docs.view()
- "/docs/components" -> components_docs.view()
- "/docs/server-side-rendering" -> server_side_rendering_docs.view()
-
- _ -> quickstart_docs.view()
+ "/" ->
+ html.body(
+ [],
+ [
+ html.div(
+ [
+ attribute.class(
+ "w-screen h-screen flex justify-center items-center",
+ ),
+ attribute.style([
+ #("background-color", "hsla(226,0%,100%,1)"),
+ #(
+ "background-image",
+ " radial-gradient(at 62% 13%, hsla(170,76%,60%,1) 0px, transparent 65%),
+ radial-gradient(at 67% 42%, hsla(234,89%,70%,1) 0px, transparent 65%),
+ radial-gradient(at 10% 7%, hsla(213,93%,57%,1) 0px, transparent 65%),
+ radial-gradient(at 32% 46%, hsla(291,93%,80%,1) 0px, transparent 65%)",
+ ),
+ ]),
+ ],
+ [
+ html.hgroup(
+ [],
+ [
+ html.h1(
+ [attribute.class("text-8xl")],
+ [element.text("Lustre.")],
+ ),
+ html.p(
+ [attribute.class("pl-1")],
+ [element.text("Web apps from space!")],
+ ),
+ ],
+ ),
+ ],
+ ),
+ layout.docs_section(model.content),
+ ],
+ )
+
+ _ -> layout.docs_page(model.content)
}
}
diff --git a/docs/src/app/effects.gleam b/docs/src/app/effects.gleam
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/docs/src/app/effects.gleam
diff --git a/docs/src/app/layout.gleam b/docs/src/app/layout.gleam
index 9f3b294..5ec9222 100644
--- a/docs/src/app/layout.gleam
+++ b/docs/src/app/layout.gleam
@@ -1,19 +1,106 @@
// IMPORTS ---------------------------------------------------------------------
+import app/ui/hooks
+import app/ui/radix
+import app/ui/markdown
import gleam/list
+import gleam/string
import lustre/attribute
import lustre/element.{Element}
import lustre/element/html
+import lustre/event
-pub fn docs(md: String) -> Element(msg) {
- let #(content, summary) = parse_markdown(md)
+pub fn docs_page(md: String) -> Element(msg) {
+ let #(content, summary) = markdown.parse(md)
html.body(
- [attribute.class("bg-gray-50 prose max-w-none")],
+ [attribute.class("prose prose-lustre max-w-none")],
[
html.div(
[attribute.class("max-w-[96rem] mx-auto grid grid-cols-8")],
- [docs_left(), docs_content(content), docs_right(summary)],
+ [docs_top(), docs_left(), docs_content(content), docs_right(summary)],
+ ),
+ ],
+ )
+}
+
+pub fn docs_section(md: String) -> Element(msg) {
+ let #(content, summary) = markdown.parse(md)
+
+ html.div(
+ [attribute.class("prose prose-lustre max-w-none")],
+ [
+ html.div(
+ [attribute.class("max-w-[96rem] mx-auto grid grid-cols-8")],
+ [docs_top(), docs_left(), docs_content(content), docs_right(summary)],
+ ),
+ ],
+ )
+}
+
+fn docs_top() -> Element(msg) {
+ html.header(
+ [attribute.class("sticky top-0 z-10 col-span-8 lg:hidden bg-white")],
+ [docs_top_toggle()],
+ )
+}
+
+fn docs_top_toggle() -> Element(msg) {
+ use open, set_open, _ <- hooks.use_state(False)
+
+ case open {
+ True -> docs_top_open(set_open(False))
+ False -> docs_top_closed(set_open(True))
+ }
+}
+
+fn docs_top_open(close: msg) -> Element(msg) {
+ html.div(
+ [attribute.class("relative")],
+ [
+ html.div(
+ [attribute.class("flex justify-between items-center px-4 py-2")],
+ [
+ html.h2(
+ [attribute.class("text-indigo-600 my-0")],
+ [element.text("Lustre.")],
+ ),
+ html.button(
+ [
+ event.on_click(close),
+ attribute.class("hover:bg-gray-200 rounded p-2"),
+ ],
+ [radix.cross([attribute.class("w-4 h-4")])],
+ ),
+ ],
+ ),
+ html.nav(
+ [
+ attribute.class(
+ "absolute top-0 w-full rounded-b-2xl px-4 mt-12 bg-white shadow",
+ ),
+ ],
+ docs_left_links(),
+ ),
+ ],
+ )
+}
+
+fn docs_top_closed(open: msg) -> Element(msg) {
+ html.div(
+ [
+ attribute.class(
+ "flex justify-between items-center px-4 py-2 border-b shadow",
+ ),
+ ],
+ [
+ html.h2(
+ [attribute.class("text-indigo-600 my-0")],
+ [element.text("Lustre.")],
+ ),
+ html.button(
+ [event.on_click(open), attribute.class("hover:bg-gray-100 rounded p-2")],
+ [radix.hamburger([attribute.class("w-4 h-4")])],
),
],
)
@@ -23,7 +110,7 @@ fn docs_left() -> Element(msg) {
html.aside(
[
attribute.style([#("align-self", "start")]),
- attribute.class("sticky top-0 border-r hidden px-4 h-screen"),
+ attribute.class("relative sticky top-0 hidden px-4 pb-10 h-screen"),
attribute.class("lg:block lg:col-span-2"),
attribute.class("xl:col-span-2"),
],
@@ -31,46 +118,86 @@ fn docs_left() -> Element(msg) {
html.div(
[
attribute.class(
- "absolute right-0 inset-y-0 w-[50vw] bg-gray-100 -z-10",
+ "absolute right-0 inset-y-0 w-[50vw] bg-gradient-to-b from-white to-gray-100 -z-10",
),
],
[],
),
- html.h2([attribute.class("text-indigo-600")], [element.text("Lustre.")]),
- docs_left_section(
- "Docs",
- [
- #("Quickstart", "/docs/quickstart"),
- #("Managing state", "/docs/managing-state"),
- #("Side effects", "/docs/side-effects"),
- #("Components", "/docs/components"),
- #("Server-side rendering", "/docs/server-side-rendering"),
- ],
- ),
- docs_left_section(
- "Reference",
- [
- #("lustre", "/api/lustre"),
- #("lustre/attribute", "/api/lustre/attribute"),
- #("lustre/effect", "/api/lustre/effect"),
- #("lustre/element", "/api/lustre/element"),
- #("lustre/element/html", "/api/lustre/element/html"),
- #("lustre/element/svg", "/api/lustre/element/svg"),
- #("lustre/event", "/api/lustre/event"),
- ],
- ),
- docs_left_section(
- "External",
+ html.div(
+ [attribute.class("flex flex-col h-full overflow-y-scroll")],
[
- #("GitHub", "https://github.com/hayleigh-dot-dev/gleam-lustre"),
- #("Discord", "https://discord.gg/Fm8Pwmy"),
- #("Buy me a coffee?", "https://github.com/sponsors/hayleigh-dot-dev"),
+ html.h2(
+ [attribute.class("mb-0")],
+ [
+ html.a(
+ [
+ attribute.href("/"),
+ attribute.class("text-indigo-600 no-underline"),
+ ],
+ [element.text("Lustre")],
+ ),
+ ],
+ ),
+ html.p(
+ [attribute.class("text-gray-400 font-bold")],
+ [element.text("Web apps from space.")],
+ ),
+ ..docs_left_links()
],
),
],
)
}
+fn docs_left_links() -> List(Element(msg)) {
+ let link = string.append(base_url(), _)
+
+ [
+ docs_left_section(
+ "Docs",
+ [
+ #("Quickstart", link("docs/quickstart")),
+ #("Managing state", link("docs/managing-state")),
+ #("Side effects", link("docs/side-effects")),
+ #("Components", link("docs/components")),
+ #("Server-side rendering", link("docs/server-side-rendering")),
+ ],
+ ),
+ docs_left_section(
+ "Guides",
+ [
+ #("Using with Mist", link("guides/mist")),
+ #("Using with Wisp", link("guides/wisp")),
+ ],
+ ),
+ docs_left_section(
+ "Reference",
+ [
+ #("lustre", link("api/lustre")),
+ #("lustre/attribute", link("api/lustre/attribute")),
+ #("lustre/effect", link("api/lustre/effect")),
+ #("lustre/element", link("api/lustre/element")),
+ #("lustre/element/html", link("api/lustre/element/html")),
+ #("lustre/element/svg", link("api/lustre/element/svg")),
+ #("lustre/event", link("api/lustre/event")),
+ ],
+ ),
+ docs_left_section(
+ "External",
+ [
+ #("GitHub", "https://github.com/hayleigh-dot-dev/gleam-lustre"),
+ #("Discord", "https://discord.gg/Fm8Pwmy"),
+ #("Buy me a coffee?", "https://github.com/sponsors/hayleigh-dot-dev"),
+ ],
+ ),
+ ]
+}
+
+@external(javascript, "../app.ffi.mjs", "base")
+fn base_url() -> String {
+ "/"
+}
+
fn docs_left_section(
title: String,
pages: List(#(String, String)),
@@ -78,12 +205,20 @@ fn docs_left_section(
html.nav(
[],
[
- html.h2([], [element.text(title)]),
+ html.h2([attribute.class("my-0 lg:mt-8 lg:mb-4")], [element.text(title)]),
html.ul(
[attribute.class("ml-2")],
{
use #(name, url) <- list.map(pages)
- html.li([], [html.a([attribute.href(url)], [element.text(name)])])
+ html.li(
+ [],
+ [
+ html.a(
+ [attribute.href(url), attribute.class("font-serif")],
+ [element.text(name)],
+ ),
+ ],
+ )
},
),
],
@@ -105,19 +240,22 @@ fn docs_right(summary: List(Element(msg))) -> Element(msg) {
html.aside(
[
attribute.style([#("align-self", "start")]),
- attribute.class("sticky top-0 border-l hidden p-4 py-10 h-screen"),
+ attribute.class("sticky relative top-0 hidden p-4 py-10 h-screen"),
attribute.class("xl:block xl:col-span-1"),
],
[
html.div(
+ [
+ attribute.class(
+ "absolute left-0 inset-y-0 w-[50vw] bg-gradient-to-b from-white to-gray-100 -z-10",
+ ),
+ ],
+ [],
+ ),
+ html.div(
[attribute.class("flex flex-col h-full overflow-y-scroll")],
summary,
),
],
)
}
-
-// EXTERNALS -------------------------------------------------------------------
-
-@external(javascript, "../app.ffi.mjs", "parse_markdown")
-fn parse_markdown(md: String) -> #(List(Element(msg)), List(Element(msg)))
diff --git a/docs/src/app/page/api/lustre.gleam b/docs/src/app/page/api/lustre.gleam
deleted file mode 100644
index 63f8547..0000000
--- a/docs/src/app/page/api/lustre.gleam
+++ /dev/null
@@ -1,116 +0,0 @@
-// IMPORTS ---------------------------------------------------------------------
-
-import app/layout
-import gleam/string
-import lustre/element.{Element}
-
-// PAGE ------------------------------------------------------------------------
-
-pub fn view() -> Element(msg) {
- [title, applications, components, utilities]
- |> string.join("\n")
- |> layout.docs
-}
-
-// CONTENT: TITLE --------------------------------------------------------------
-
-const title: String = "
-# lustre
-"
-
-const applications: String = "
-## Applications
-
-### App | erlang javascript
-
-```gleam
-pub type App(flags, model, msg)
-```
-
-### Error | erlang javascript
-
-```gleam
-pub type Error {
- AppAlreadyStarted
- AppNotYetStarted
- ComponentAlreadyRegistered
- ElementNotFound
- NotABrowser
-}
-```
-
-### element | javascript
-
-```gleam
-pub fn element(el: Element(msg)) -> App(Nil, Nil, msg)
-```
-
-### simple | javascript
-
-```gleam
-pub fn simple(
- init: fn(flags) -> model,
- update: fn(model, msg) -> model,
- view: fn(model) -> Element(msg)
-) -> App(flags, model, msg)
-```
-
-### application | javascript
-
-```gleam
-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)
-```
-
-### start | javascript
-
-```gleam
-pub fn start(
- app: App(flags, model, msg),
- selector: String,
- flags: flags,
-) -> Result(fn(msg) -> Nil, Error)
-```
-
-### destroy | javascript
-
-```gleam
-pub fn destroy(app: App(flags, model, msg)) -> Nil
-```
-"
-
-const components: String = "
-## Components
-
-### component | javascript
-
-```gleam
-pub fn component(
- name: String,
- init: fn() -> #(model, Effect(msg)),
- update: fn(model, msg) -> #(model, Effect(msg)),
- view: fn(model) -> Element(msg),
- on_attribute_change: Map(String, Decoder(msg)),
-) -> Result(Nil, Error)
-```
-"
-
-const utilities: String = "
-## Utilities
-
-### is_browser | erlang javascript
-
-```gleam
-pub fn is_browser() -> Bool
-```
-
-### is_registered | erlang javascript
-
-```gleam
-pub fn is_registered(_name: String) -> Bool
-```
-
-"
diff --git a/docs/src/app/page/api/lustre/attribute.gleam b/docs/src/app/page/api/lustre/attribute.gleam
deleted file mode 100644
index 4578e46..0000000
--- a/docs/src/app/page/api/lustre/attribute.gleam
+++ /dev/null
@@ -1,365 +0,0 @@
-// IMPORTS ---------------------------------------------------------------------
-
-import app/layout
-import gleam/string
-import lustre/element.{Element}
-
-// PAGE ------------------------------------------------------------------------
-
-pub fn view() -> Element(msg) {
- [
- title,
- constructing_attributes,
- mapping_attributes,
- conversions,
- common_attributes,
- input_attributes,
- more_input_attributes,
- range_attributes,
- textarea_attributes,
- link_attributes,
- embedded_content,
- audio_and_video,
- ]
- |> string.join("\n")
- |> layout.docs
-}
-
-// CONTENT: TITLE --------------------------------------------------------------
-
-const title: String = "
-# lustre/attribute
-"
-
-// CONTENT: CONSTRUCTING ATTRIBUTES --------------------------------------------
-
-const constructing_attributes: String = "
-## Constructing attributes
-
-### Attribute | erlang javascript
-
-```gleam
-pub opaque type Attribute(msg)
-```
-
-### attribute | erlang javascript
-
-```gleam
-pub fn attribute(name: String, value: String) -> Attribute(msg)
-```
-
-### property | erlang javascript
-
-```gleam
-pub fn property(name: String, value: any) -> Attribute(msg)
-```
-
-### on | erlang javascript
-
-```gleam
-pub fn on(name: String, handler: fn(Dynamic) -> Option(msg)) -> Attribute(msg)
-```
-"
-
-// CONTENT: MAPPING ATTRIBUTES -------------------------------------------------
-
-const mapping_attributes: String = "
-## Mapping attributes
-
-### map | erlang javascript
-
-```gleam
-pub fn map(attr: Attribute(a), f: fn(a) -> b) -> Attribute(b)
-```
-"
-
-// CONTENT: CONVERSIONS --------------------------------------------------------
-
-const conversions: String = "
-## Conversions
-
-### to_string | erlang javascript
-
-```gleam
-pub fn to_string(attr: Attribute(msg)) -> String
-```
-
-### to_string_builder | erlang javascript
-
-```gleam
-pub fn to_string_builder(attr: Attribute(msg)) -> StringBuilder
-```
-"
-
-// CONTENT: COMMON ATTRIBUTES --------------------------------------------------
-
-const common_attributes: String = "
-## Common attributes
-
-### style | erlang javascript
-
-```gleam
-pub fn style(properties: List(#(String, String))) -> Attribute(msg)
-```
-
-### class | erlang javascript
-
-```gleam
-pub fn class(name: String) -> Attribute(msg)
-```
-
-### classes | erlang javascript
-
-```gleam
-pub fn classes(names: List(#(String, Bool))) -> Attribute(msg)
-```
-
-### id | erlang javascript
-
-```gleam
-pub fn id(name: String) -> Attribute(msg)
-```
-"
-
-// CONTENT: INPUT ATTRIBUTES ---------------------------------------------------
-
-const input_attributes: String = "
-## Input attributes
-
-### type_ | erlang javascript
-
-```gleam
-pub fn type_(name: String) -> Attribute(msg)
-```
-
-### value | erlang javascript
-
-```gleam
-pub fn value(val: Dynamic) -> Attribute(msg)
-```
-
-### checked | erlang javascript
-
-```gleam
-pub fn checked(is_checked: Bool) -> Attribute(msg)
-```
-
-### placeholder | erlang javascript
-
-```gleam
-pub fn placeholder(text: String) -> Attribute(msg)
-```
-
-### selected | erlang javascript
-
-```gleam
-pub fn selected(is_selected: Bool) -> Attribute(msg
-```
-"
-
-// CONTENT: MORE INPUT ATTRIBUTES ----------------------------------------------
-
-const more_input_attributes: String = "
-## More input attributes
-
-### accept | erlang javascript
-
-```gleam
-pub fn accept(types: List(String)) -> Attribute(msg)
-```
-
-### accept_charset | erlang javascript
-
-```gleam
-pub fn accept_charset(types: List(String)) -> Attribute(msg)
-```
-
-### msg | erlang javascript
-
-```gleam
-pub fn msg(uri: String) -> Attribute(msg)
-```
-
-### autocomplete | erlang javascript
-
-```gleam
-pub fn autocomplete(name: String) -> Attribute(msg)
-```
-
-### autofocus | erlang javascript
-
-```gleam
-pub fn autofocus(should_autofocus: Bool) -> Attribute(msg)
-```
-
-### disabled | erlang javascript
-
-```gleam
-pub fn disabled(is_disabled: Bool) -> Attribute(msg)
-```
-
-### name | erlang javascript
-
-```gleam
-pub fn name(name: String) -> Attribute(msg)
-```
-
-### pattern | erlang javascript
-
-```gleam
-pub fn pattern(regex: String) -> Attribute(msg)
-```
-
-### readonly | erlang javascript
-
-```gleam
-pub fn readonly(is_readonly: Bool) -> Attribute(msg)
-```
-
-### required | erlang javascript
-
-```gleam
-pub fn required(is_required: Bool) -> Attribute(msg)
-```
-
-### for | erlang javascript
-
-```gleam
-pub fn for(id: String) -> Attribute(msg)
-```
-"
-
-// CONTENT: RANGE ATTRIBUTES ---------------------------------------------------
-
-const range_attributes: String = "
-## Range attributes
-
-### max | erlang javascript
-
-```gleam
-pub fn max(val: String) -> Attribute(msg)
-```
-
-### min | erlang javascript
-
-```gleam
-pub fn min(val: String) -> Attribute(msg)
-```
-
-### step | erlang javascript
-
-```gleam
-pub fn step(val: String) -> Attribute(msg)
-```
-"
-
-// CONTENT: TEXTAREA ATTRIBUTES ------------------------------------------------
-
-const textarea_attributes: String = "
-## Textarea attributes
-
-### cols | erlang javascript
-
-```gleam
-pub fn cols(val: Int) -> Attribute(msg)
-```
-
-### rows | erlang javascript
-
-```gleam
-pub fn rows(val: Int) -> Attribute(msg)
-```
-
-### wrap | erlang javascript
-
-```gleam
-pub fn wrap(mode: String) -> Attribute(msg)
-```
-"
-
-// CONTENT: LINK ATTRIBUTES ----------------------------------------------------
-
-const link_attributes: String = "
-## Link attributes
-
-### href | erlang javascript
-
-```gleam
-pub fn href(uri: String) -> Attribute(msg)
-```
-
-### target | erlang javascript
-
-```gleam
-pub fn target(target: String) -> Attribute(msg)
-```
-
-### download | erlang javascript
-
-```gleam
-pub fn download(filename: String) -> Attribute(msg)
-```
-
-### rel | erlang javascript
-
-```gleam
-pub fn rel(relationship: String) -> Attribute(msg)
-```
-"
-
-// CONTENT: EMBEDDED CONTENT ---------------------------------------------------
-
-const embedded_content: String = "
-## Embedded content
-
-### gleam | erlang javascript
-
-```gleam
-pub fn src(uri: String) -> Attribute(msg)
-```
-
-### gleam | erlang javascript
-
-```gleam
-pub fn height(val: Int) -> Attribute(msg)
-```
-
-### gleam | erlang javascript
-
-```gleam
-pub fn width(val: Int) -> Attribute(msg)
-```
-
-### gleam | erlang javascript
-
-```gleam
-pub fn alt(text: String) -> Attribute(msg)
-```
-"
-
-// CONTENT: AUDIO AND VIDEO ATTRIBUTES -----------------------------------------
-
-const audio_and_video: String = "
-## Audio and video attributes
-
-
-### autoplay | erlang javascript
-
-```gleam
-pub fn autoplay(should_autoplay: Bool) -> Attribute(msg)
-```
-
-
-### controls | erlang javascript
-
-```gleam
-pub fn controls(visible: Bool) -> Attribute(msg)
-```
-
-
-### loop | erlang javascript
-
-```gleam
-pub fn loop(should_loop: Bool) -> Attribute(msg)
-```
-"
diff --git a/docs/src/app/page/api/lustre/effect.gleam b/docs/src/app/page/api/lustre/effect.gleam
deleted file mode 100644
index ea3e1af..0000000
--- a/docs/src/app/page/api/lustre/effect.gleam
+++ /dev/null
@@ -1,61 +0,0 @@
-// IMPORTS ---------------------------------------------------------------------
-
-import app/layout
-import gleam/string
-import lustre/element.{Element}
-
-// PAGE ------------------------------------------------------------------------
-
-pub fn view() -> Element(msg) {
- [title, constructing_effects, manipulating_effects]
- |> string.join("\n")
- |> layout.docs
-}
-
-// CONTENT: TITLE --------------------------------------------------------------
-
-const title: String = "
-# lustre/effect
-"
-
-// CONTENT: CONSTRUCTING EFFECTS ------------------------------------------------
-
-const constructing_effects: String = "
-## Constructing Effects
-
-### Effect | erlang javascript
-
-```gleam
-pub opaque type Effect(action)
-```
-
-### from | erlang javascript
-
-```gleam
-pub fn from(effect: fn(fn(action) -> Nil) -> Nil) -> Effect(action)
-```
-
-### none | erlang javascript
-
-```gleam
-pub fn none() -> Effect(action)
-```
-
-### batch | erlang javascript
-
-```gleam
-pub fn batch(effects: List(Effect(action))) -> Effect(action)
-```
-"
-
-// CONTENT: MANIPULATING EFFECTS -----------------------------------------------
-
-const manipulating_effects: String = "
-## Manipulating Effects
-
-### map | erlang javascript
-
-```gleam
-pub fn map(effect: Effect(a), f: fn(a) -> b) -> Effect(b)
-```
-"
diff --git a/docs/src/app/page/api/lustre/element.gleam b/docs/src/app/page/api/lustre/element.gleam
deleted file mode 100644
index 69a1273..0000000
--- a/docs/src/app/page/api/lustre/element.gleam
+++ /dev/null
@@ -1,88 +0,0 @@
-// IMPORTS ---------------------------------------------------------------------
-
-import app/layout
-import gleam/string
-import lustre/element.{Element}
-
-// PAGE ------------------------------------------------------------------------
-
-pub fn view() -> Element(msg) {
- [title, constructing_elements, mapping_elements, conversions]
- |> string.join("\n")
- |> layout.docs
-}
-
-// CONTENT: TITLE --------------------------------------------------------------
-
-const title: String = "
-# lustre/element
-"
-
-// CONTENT: CONSTRUCTING ELEMENTS ----------------------------------------------
-
-const constructing_elements: String = "
-## Constructing elements
-
-### Element | erlang javascript
-
-```gleam
-pub opaque type Element(msg)
-```
-
-### element | erlang javascript
-
-```gleam
-pub fn element(
- tag: String,
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### namespaced | erlang javascript
-
-```gleam
-pub fn namespaced(
- namespace: String,
- tag: String,
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### text | erlang javascript
-
-```gleam
-pub fn text(content: String) -> Element(msg)
-```
-"
-
-// CONTENT: MAPPING ELEMENTS ---------------------------------------------------
-
-const mapping_elements: String = "
-## Mapping elements
-
-### map | erlang javascript
-
-```gleam
-pub fn map(element: Element(a), f: fn(a) -> b) -> Element(b)
-```
-"
-
-// CONTENT: CONVERSIONS --------------------------------------------------------
-
-const conversions: String = "
-## Conversions
-
-### to_string | erlang javascript
-
-```gleam
-pub fn to_string(element: Element(msg)) -> String
-```
-
-### to_string_builder | erlang javascript
-
-```gleam
-pub fn to_string_builder(element: Element(msg)) -> StringBuilder
-```
-"
diff --git a/docs/src/app/page/api/lustre/element/html.gleam b/docs/src/app/page/api/lustre/element/html.gleam
deleted file mode 100644
index 6688195..0000000
--- a/docs/src/app/page/api/lustre/element/html.gleam
+++ /dev/null
@@ -1,1083 +0,0 @@
-// IMPORTS ---------------------------------------------------------------------
-
-import app/layout
-import gleam/string
-import lustre/element.{Element}
-
-// PAGE ------------------------------------------------------------------------
-
-pub fn view() -> Element(msg) {
- [
- title,
- main_root,
- document_metadata,
- sectioning_root,
- content_sectioning,
- text_content,
- inline_text_semantics,
- image_and_multimedia,
- embedded_content,
- svg_and_mathml,
- scripting,
- demarcating_edits,
- table_content,
- forms,
- interactive_elements,
- web_components,
- ]
- |> string.join("\n")
- |> layout.docs
-}
-
-// CONTENT: TITLE --------------------------------------------------------------
-
-const title: String = "
-# lustre/element/html
-"
-
-// CONTENT: MAIN ROOT ---------------------------------------------------------
-
-const main_root: String = "
-## Main Root
-
-### html | erlang javascript
-
-```gleam
-pub fn html(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-"
-
-// CONTENT: DOCUMENT METADATA --------------------------------------------------
-
-const document_metadata: String = "
-## Document Metadata
-
-### base | erlang javascript
-
-```gleam
-pub fn base(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### head | erlang javascript
-
-```gleam
-pub fn head(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### link | erlang javascript
-
-```gleam
-pub fn link(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### meta | erlang javascript
-
-```gleam
-pub fn meta(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### style | erlang javascript
-
-```gleam
-pub fn style(attrs: List(Attribute(msg)), css: String) -> Element(msg)
-```
-
-### title | erlang javascript
-
-```gleam
-pub fn title(attrs: List(Attribute(msg)), content: String) -> Element(msg)
-```
-"
-
-// CONTENT: SECTIONING ROOT ----------------------------------------------------
-
-const sectioning_root: String = "
-## Sectioning root
-
-### body | erlang javascript
-
-```gleam
-pub fn body(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-"
-
-// CONTENT: CONTENT SECTIONING -------------------------------------------------
-
-const content_sectioning: String = "
-## Content sectioning
-
-### address | erlang javascript
-
-```gleam
-pub fn address(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### article | erlang javascript
-
-```gleam
-pub fn article(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### aside | erlang javascript
-
-```gleam
-pub fn aside(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### footer | erlang javascript
-
-```gleam
-pub fn footer(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### header | erlang javascript
-
-```gleam
-pub fn header(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### h1 | erlang javascript
-
-```gleam
-pub fn h1(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### h2 | erlang javascript
-
-```gleam
-pub fn h2(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### h3 | erlang javascript
-
-```gleam
-pub fn h3(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### h4 | erlang javascript
-
-```gleam
-pub fn h4(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### h5 | erlang javascript
-
-```gleam
-pub fn h5(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### h6 | erlang javascript
-
-```gleam
-pub fn h6(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### hgroup | erlang javascript
-
-```gleam
-pub fn hgroup(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### main | erlang javascript
-
-```gleam
-pub fn main(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### nav | erlang javascript
-
-```gleam
-pub fn nav(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### section | erlang javascript
-
-```gleam
-pub fn section(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### search | erlang javascript
-
-```gleam
-pub fn search(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-"
-
-// CONTENT: TEXT CONTENT -------------------------------------------------------
-
-const text_content: String = "
-## Text content
-
-### blockquote | erlang javascript
-
-```gleam
-pub fn blockquote(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### dd | erlang javascript
-
-```gleam
-pub fn dd(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### div | erlang javascript
-
-```gleam
-pub fn div(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### dl | erlang javascript
-
-```gleam
-pub fn dl(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### dt | erlang javascript
-
-```gleam
-pub fn dt(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### figcaption | erlang javascript
-
-```gleam
-pub fn figcaption(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### figure | erlang javascript
-
-```gleam
-pub fn figure(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### hr | erlang javascript
-
-```gleam
-pub fn hr(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### li | erlang javascript
-
-```gleam
-pub fn li(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### menu | erlang javascript
-
-```gleam
-pub fn menu(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### ol | erlang javascript
-
-```gleam
-pub fn ol(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### p | erlang javascript
-
-```gleam
-pub fn p(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### pre | erlang javascript
-
-```gleam
-pub fn pre(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### ul | erlang javascript
-
-```gleam
-pub fn ul(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-"
-
-// CONTENT: INLINE TEXT SEMANTICS ----------------------------------------------
-const inline_text_semantics: String = "
-## Inline text semantics
-
-### a | erlang javascript
-
-```gleam
-pub fn a(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### abbr | erlang javascript
-
-```gleam
-pub fn abbr(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### b | erlang javascript
-
-```gleam
-pub fn b(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### bdi | erlang javascript
-
-```gleam
-pub fn bdi(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### bdo | erlang javascript
-
-```gleam
-pub fn bdo(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### br | erlang javascript
-
-```gleam
-pub fn br(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### cite | erlang javascript
-
-```gleam
-pub fn cite(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### code | erlang javascript
-
-```gleam
-pub fn code(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### data | erlang javascript
-
-```gleam
-pub fn data(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### dfn | erlang javascript
-
-```gleam
-pub fn dfn(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### em | erlang javascript
-
-```gleam
-pub fn em(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### i | erlang javascript
-
-```gleam
-pub fn i(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### kbd | erlang javascript
-
-```gleam
-pub fn kbd(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### mark | erlang javascript
-
-```gleam
-pub fn mark(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### q | erlang javascript
-
-```gleam
-pub fn q(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### rp | erlang javascript
-
-```gleam
-pub fn rp(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### rt | erlang javascript
-
-```gleam
-pub fn rt(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### ruby | erlang javascript
-
-```gleam
-pub fn ruby(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### s | erlang javascript
-
-```gleam
-pub fn s(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### samp | erlang javascript
-
-```gleam
-pub fn samp(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### small | erlang javascript
-
-```gleam
-pub fn small(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### span | erlang javascript
-
-```gleam
-pub fn span(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### strong | erlang javascript
-
-```gleam
-pub fn strong(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### sub | erlang javascript
-
-```gleam
-pub fn sub(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### sup | erlang javascript
-
-```gleam
-pub fn sup(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### time | erlang javascript
-
-```gleam
-pub fn time(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### u | erlang javascript
-
-```gleam
-pub fn u(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### var | erlang javascript
-
-```gleam
-pub fn var(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### wbr | erlang javascript
-
-```gleam
-pub fn wbr(attrs: List(Attribute(msg))) -> Element(msg)
-```
-"
-
-// CONTENT: IMAGE AND MULTIMEDIA -----------------------------------------------
-const image_and_multimedia: String = "
-## Image and multimedia
-
-### area | erlang javascript
-
-```gleam
-pub fn area(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### audio | erlang javascript
-
-```gleam
-pub fn audio(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### img | erlang javascript
-
-```gleam
-pub fn img(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### map | erlang javascript
-
-```gleam
-pub fn map(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### track | erlang javascript
-
-```gleam
-pub fn track(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### video | erlang javascript
-
-```gleam
-pub fn video(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-"
-
-// CONTENT: EMBEDDED CONTENT ---------------------------------------------------
-
-const embedded_content: String = "
-## Embedded content
-
-### embed | erlang javascript
-
-```gleam
-pub fn embed(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### iframe | erlang javascript
-
-```gleam
-pub fn iframe(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### object | erlang javascript
-
-```gleam
-pub fn object(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### picture | erlang javascript
-
-```gleam
-pub fn picture(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### portal | erlang javascript
-
-```gleam
-pub fn portal(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### source | erlang javascript
-
-```gleam
-pub fn source(attrs: List(Attribute(msg))) -> Element(msg)
-```
-"
-
-// CONTENT: SVG AND MATHML -----------------------------------------------------
-
-const svg_and_mathml: String = "
-## SVG and MathML
-
-### svg | erlang javascript
-
-```gleam
-pub fn svg(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-
-### math | erlang javascript
-
-```gleam
-pub fn math(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-"
-
-// CONTENT: SCRIPTING ----------------------------------------------------------
-
-const scripting: String = "
-## Scripting
-
-### canvas | erlang javascript
-
-```gleam
-pub fn canvas(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### noscript | erlang javascript
-
-```gleam
-pub fn noscript(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### script | erlang javascript
-
-```gleam
-pub fn script(attrs: List(Attribute(msg)), js: String) -> Element(msg)
-```
-"
-
-// CONTENT: DEMARCATING EDITS --------------------------------------------------
-
-const demarcating_edits: String = "
-## Demarcating edits
-
-### del | erlang javascript
-
-```gleam
-pub fn del(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### ins | erlang javascript
-
-```gleam
-pub fn ins(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-"
-
-// CONTENT: TABLE CONTENT ------------------------------------------------------
-
-const table_content: String = "
-## Table content
-
-### caption | erlang javascript
-
-```gleam
-pub fn caption(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### col | erlang javascript
-
-```gleam
-pub fn col(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### colgroup | erlang javascript
-
-```gleam
-pub fn colgroup(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### table | erlang javascript
-
-```gleam
-pub fn table(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### tbody | erlang javascript
-
-```gleam
-pub fn tbody(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### td | erlang javascript
-
-```gleam
-pub fn td(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### tfoot | erlang javascript
-
-```gleam
-pub fn tfoot(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### th | erlang javascript
-
-```gleam
-pub fn th(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### thead | erlang javascript
-
-```gleam
-pub fn thead(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### tr | erlang javascript
-
-```gleam
-pub fn tr(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-"
-
-// CONTENT: FORMS --------------------------------------------------------------
-const forms: String = "
-## Forms
-
-### button | erlang javascript
-
-```gleam
-pub fn button(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### datalist | erlang javascript
-
-```gleam
-pub fn datalist(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### fieldset | erlang javascript
-
-```gleam
-pub fn fieldset(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### form | erlang javascript
-
-```gleam
-pub fn form(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### input | erlang javascript
-
-```gleam
-pub fn input(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### label | erlang javascript
-
-```gleam
-pub fn label(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### legend | erlang javascript
-
-```gleam
-pub fn legend(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### meter | erlang javascript
-
-```gleam
-pub fn meter(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### optgroup | erlang javascript
-
-```gleam
-pub fn optgroup(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### option | erlang javascript
-
-```gleam
-pub fn option(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### output | erlang javascript
-
-```gleam
-pub fn output(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### progress | erlang javascript
-
-```gleam
-pub fn progress(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### select | erlang javascript
-
-```gleam
-pub fn select(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### textarea | erlang javascript
-
-```gleam
-pub fn textarea(attrs: List(Attribute(msg))) -> Element(msg)
-```
-"
-
-// CONTENT: INTERACTIVE ELEMENTS -----------------------------------------------
-const interactive_elements: String = "
-## Interactive elements
-
-### details | erlang javascript
-
-```gleam
-pub fn details(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### dialog | erlang javascript
-
-```gleam
-pub fn dialog(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### summary | erlang javascript
-
-```gleam
-pub fn summary(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-"
-
-// CONTENT: WEB COMPONENTS -----------------------------------------------------
-const web_components: String = "
-## Web components
-
-### slot | erlang javascript
-
-```gleam
-pub fn slot(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### template | erlang javascript
-
-```gleam
-pub fn template(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-"
diff --git a/docs/src/app/page/api/lustre/element/svg.gleam b/docs/src/app/page/api/lustre/element/svg.gleam
deleted file mode 100644
index eb4011c..0000000
--- a/docs/src/app/page/api/lustre/element/svg.gleam
+++ /dev/null
@@ -1,532 +0,0 @@
-// IMPORTS ---------------------------------------------------------------------
-
-import app/layout
-import gleam/string
-import lustre/element.{Element}
-
-// PAGE ------------------------------------------------------------------------
-
-pub fn view() -> Element(msg) {
- [
- title,
- animation_elements,
- basic_shapes,
- container_elements,
- descriptive_elements,
- filter_effects,
- gradient_elements,
- graphical_elements,
- lighting_elements,
- non_rendered_elements,
- renderable_elements,
- ]
- |> string.join("\n")
- |> layout.docs
-}
-
-// CONTENT: TITLE --------------------------------------------------------------
-
-const title: String = "
-# lustre/element/svg
-"
-
-// CONTENT: ANIMATION ELEMENTS ------------------------------------------------
-
-const animation_elements: String = "
-## Animation elements
-
-### animate | erlang javascript
-
-```
-pub fn animate(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### animate_motion | erlang javascript
-
-```
-pub fn animate_motion(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### animate_transform | erlang javascript
-
-```
-pub fn animate_transform(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### mpath | erlang javascript
-
-```
-pub fn mpath(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### set | erlang javascript
-
-```
-pub fn set(attrs: List(Attribute(msg))) -> Element(msg)
-```
-"
-
-// CONTENT: BASIC SHAPES -------------------------------------------------------
-
-const basic_shapes: String = "
-## Basic shapes
-
-### circle | erlang javascript
-
-```
-pub fn circle(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### ellipse | erlang javascript
-
-```
-pub fn ellipse(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### line | erlang javascript
-
-```
-pub fn line(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### polygon | erlang javascript
-
-```
-pub fn polygon(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### polyline | erlang javascript
-
-```
-pub fn polyline(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### rect | erlang javascript
-
-```
-pub fn rect(attrs: List(Attribute(msg))) -> Element(msg)
-```
-"
-
-// CONTENT: CONTAINER ELEMENTS -------------------------------------------------
-
-const container_elements: String = "
-## Container elements
-
-### a | erlang javascript
-
-```gleam
-pub fn a(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### defs | erlang javascript
-
-```gleam
-pub fn defs(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### g | erlang javascript
-
-```gleam
-pub fn g(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### marker | erlang javascript
-
-```gleam
-pub fn marker(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### mask | erlang javascript
-
-```gleam
-pub fn mask(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### missing_glyph | erlang javascript
-
-```gleam
-pub fn missing_glyph(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### pattern | erlang javascript
-
-```gleam
-pub fn pattern(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### svg | erlang javascript
-
-```gleam
-pub fn svg(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### switch | erlang javascript
-
-```gleam
-pub fn switch(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### symbol | erlang javascript
-
-```gleam
-pub fn symbol(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-"
-
-// CONTENT: DESCRIPTIVE ELEMENTS -----------------------------------------------
-
-const descriptive_elements: String = "
-## Descriptive elements
-
-### desc | erlang javascript
-
-```gleam
-pub fn desc(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### metadata | erlang javascript
-
-```gleam
-pub fn metadata(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### title | erlang javascript
-
-```gleam
-pub fn title(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-"
-
-// CONTENT: FILTER EFFECTS -----------------------------------------------------
-
-const filter_effects: String = "
-## Filter effects
-
-### fe_blend | erlang javascript
-
-```
-pub fn fe_blend(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### fe_color_matrix | erlang javascript
-
-```
-pub fn fe_color_matrix(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### fe_component_transfer | erlang javascript
-
-```
-pub fn fe_component_transfer(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### fe_composite | erlang javascript
-
-```
-pub fn fe_composite(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### fe_convolve_matrix | erlang javascript
-
-```
-pub fn fe_convolve_matrix(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### fe_diffuse_lighting | erlang javascript
-
-```gleam
-pub fn fe_diffuse_lighting(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### fe_displacement_map | erlang javascript
-
-```
-pub fn fe_displacement_map(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### fe_drop_shadow | erlang javascript
-
-```
-pub fn fe_drop_shadow(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### fe_flood | erlang javascript
-
-```
-pub fn fe_flood(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### fe_func_a | erlang javascript
-
-```
-pub fn fe_func_a(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### fe_func_b | erlang javascript
-
-```
-pub fn fe_func_b(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### fe_func_g | erlang javascript
-
-```
-pub fn fe_func_g(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### fe_func_r | erlang javascript
-
-```
-pub fn fe_func_r(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### fe_gaussian_blur | erlang javascript
-
-```
-pub fn fe_gaussian_blur(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### fe_image | erlang javascript
-
-```
-pub fn fe_image(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### fe_merge | erlang javascript
-
-```gleam
-pub fn fe_merge(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### fe_merge_node | erlang javascript
-
-```
-pub fn fe_merge_node(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### fe_morphology | erlang javascript
-
-```
-pub fn fe_morphology(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### fe_offset | erlang javascript
-
-```
-pub fn fe_offset(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### fe_specular_lighting | erlang javascript
-
-```gleam
-pub fn fe_specular_lighting(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### fe_tile | erlang javascript
-
-```gleam
-pub fn fe_tile(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### fe_turbulence | erlang javascript
-
-```
-pub fn fe_turbulence(attrs: List(Attribute(msg))) -> Element(msg)
-```
-"
-
-// CONTENT: GRADIENT ELEMENTS --------------------------------------------------
-
-const gradient_elements: String = "
-## Gradient elements
-
-### linear_gradient | erlang javascript
-
-```gleam
-pub fn linear_gradient(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### radial_gradient | erlang javascript
-
-```gleam
-pub fn radial_gradient(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### stop | erlang javascript
-
-```
-pub fn stop(attrs: List(Attribute(msg))) -> Element(msg)
-```
-"
-
-// CONTENT: GRAPHICAL ELEMENTS -------------------------------------------------
-
-const graphical_elements: String = "
-## Graphical elements
-
-### image | erlang javascript
-
-```
-pub fn image(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### path | erlang javascript
-
-```
-pub fn path(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### text | erlang javascript
-
-```
-pub fn text(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### use_ | erlang javascript
-
-```
-pub fn use_(attrs: List(Attribute(msg))) -> Element(msg)
-```
-"
-
-// CONTENT: LIGHTING ELEMENTS --------------------------------------------------
-
-const lighting_elements: String = "
-## Lighting elements
-
-### fe_distant_light | erlang javascript
-
-```
-pub fn fe_distant_light(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### fe_point_light | erlang javascript
-
-```
-pub fn fe_point_light(attrs: List(Attribute(msg))) -> Element(msg)
-```
-
-### fe_spot_light | erlang javascript
-
-```
-pub fn fe_spot_light(attrs: List(Attribute(msg))) -> Element(msg)
-```
-"
-
-// CONTENT: NON-RENDERED ELEMENTS ----------------------------------------------
-
-const non_rendered_elements: String = "
-## Non-rendered elements
-
-### clip_path | erlang javascript
-
-```gleam
-pub fn clip_path(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### script | erlang javascript
-
-```
-pub fn script(attrs: List(Attribute(msg)), js: String) -> Element(msg)
-```
-
-### style | erlang javascript
-
-```
-pub fn style(attrs: List(Attribute(msg)), css: String) -> Element(msg)
-```
-"
-
-// CONTENT: RENDERABLE ELEMENTS ------------------------------------------------
-
-const renderable_elements: String = "
-## Renderable elements
-
-### foreign_object | erlang javascript
-
-```gleam
-pub fn foreign_object(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### text_path | erlang javascript
-
-```gleam
-pub fn text_path(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-
-### tspan | erlang javascript
-
-```gleam
-pub fn tspan(
- attrs: List(Attribute(msg)),
- children: List(Element(msg)),
-) -> Element(msg)
-```
-"
diff --git a/docs/src/app/page/api/lustre/event.gleam b/docs/src/app/page/api/lustre/event.gleam
deleted file mode 100644
index 4b098e5..0000000
--- a/docs/src/app/page/api/lustre/event.gleam
+++ /dev/null
@@ -1,188 +0,0 @@
-// IMPORTS ---------------------------------------------------------------------
-
-import app/layout
-import gleam/string
-import lustre/element.{Element}
-
-// PAGE ------------------------------------------------------------------------
-
-pub fn view() -> Element(msg) {
- [
- title,
- mouse_events,
- keyboard_events,
- form_messages,
- focus_events,
- custom_events,
- ]
- |> string.join("\n")
- |> layout.docs
-}
-
-// CONTENT: TITLE --------------------------------------------------------------
-
-const title: String = "
-# lustre/event
-"
-
-// CONTENT: MOUSE EVENTS -------------------------------------------------------
-
-const mouse_events: String = "
-## Mouse events
-
-### on_click | erlang javascript
-
-```gleam
-pub fn on_click(msg: msg) -> Attribute(msg)
-```
-
-### on_mouse_down | erlang javascript
-
-```gleam
-pub fn on_mouse_down(msg: msg) -> Attribute(msg)
-```
-
-### on_mouse_up | erlang javascript
-
-```gleam
-pub fn on_mouse_up(msg: msg) -> Attribute(msg)
-```
-
-### on_mouse_enter | erlang javascript
-
-```gleam
-pub fn on_mouse_enter(msg: msg) -> Attribute(msg)
-```
-
-### on_mouse_leave | erlang javascript
-
-```gleam
-pub fn on_mouse_leave(msg: msg) -> Attribute(msg)
-```
-
-### on_mouse_over | erlang javascript
-
-```gleam
-pub fn on_mouse_over(msg: msg) -> Attribute(msg)
-```
-
-### on_mouse_out | erlang javascript
-
-```gleam
-pub fn on_mouse_out(msg: msg) -> Attribute(msg)
-```
-"
-
-// CONTENT: KEYBOARD EVENTS ----------------------------------------------------
-
-const keyboard_events: String = "
-## Keyboard events
-
-### on_keypress | erlang javascript
-
-```gleam
-pub fn on_keypress(msg: fn(String) -> msg) -> Attribute(msg)
-```
-
-### on_keydown | erlang javascript
-
-```gleam
-pub fn on_keydown(msg: fn(String) -> msg) -> Attribute(msg)
-```
-
-### on_keyup | erlang javascript
-
-```gleam
-pub fn on_keyup(msg: fn(String) -> msg) -> Attribute(msg)
-```
-"
-
-// CONTENT: FORM MESSAGES ------------------------------------------------------
-
-const form_messages: String = "
-## Form messages
-
-### on_input | erlang javascript
-
-```gleam
-pub fn on_input(msg: fn(String) -> msg) -> Attribute(msg)
-```
-
-### on_change | erlang javascript
-
-```gleam
-pub fn on_change(msg: fn(Bool) -> msg) -> Attribute(msg)
-```
-
-### on_submit | erlang javascript
-
-```gleam
-pub fn on_submit(msg: msg) -> Attribute(msg)
-```
-"
-
-// CONTENT: FOCUS EVENTS -------------------------------------------------------
-
-const focus_events: String = "
-## Focus events
-
-### on_focus | erlang javascript
-
-```gleam
-pub fn on_focus(msg: msg) -> Attribute(msg)
-```
-
-### on_blur | erlang javascript
-
-```gleam
-pub fn on_blur(msg: msg) -> Attribute(msg)
-```
-"
-
-// CONTENT: CUSTOM EVENTS ------------------------------------------------------
-
-const custom_events: String = "
-## Custom events
-
-### on | erlang javascript
-
-```gleam
-pub fn on(name: String, handler: fn(Dynamic) -> Option(msg)) -> Attribute(msg)
-```
-
-### prevent_default | javascript
-
-```gleam
-pub fn prevent_default(event: Dynamic) -> Nil
-```
-
-### stop_propagation | javascript
-
-```gleam
-pub fn stop_propagation(event: Dynamic) -> Nil
-```
-
-### value | erlang javascript
-
-```gleam
-pub fn value(event: Dynamic) -> Decoded(String)
-```
-
-### checked | erlang javascript
-
-```gleam
-pub fn checked(event: Dynamic) -> Decoded(Bool)
-```
-
-### mouse_position | erlang javascript
-
-```gleam
-pub fn mouse_position(event: Dynamic) -> Decoded(#(Float, Float))
-```
-
-### emit | javascript
-
-```gleam
-pub fn emit(event: String, data: any) -> Effect(msg)
-```
-"
diff --git a/docs/src/app/page/docs/components.gleam b/docs/src/app/page/docs/components.gleam
deleted file mode 100644
index 765443d..0000000
--- a/docs/src/app/page/docs/components.gleam
+++ /dev/null
@@ -1,19 +0,0 @@
-// IMPORTS ---------------------------------------------------------------------
-
-import app/layout
-import gleam/string
-import lustre/element.{Element}
-
-// PAGE ------------------------------------------------------------------------
-
-pub fn view() -> Element(msg) {
- [title]
- |> string.join("\n")
- |> layout.docs
-}
-
-// CONTENT: TITLE --------------------------------------------------------------
-
-const title: String = "
-# Components
-"
diff --git a/docs/src/app/page/docs/managing_state.gleam b/docs/src/app/page/docs/managing_state.gleam
deleted file mode 100644
index e337b20..0000000
--- a/docs/src/app/page/docs/managing_state.gleam
+++ /dev/null
@@ -1,19 +0,0 @@
-// IMPORTS ---------------------------------------------------------------------
-
-import app/layout
-import gleam/string
-import lustre/element.{Element}
-
-// PAGE ------------------------------------------------------------------------
-
-pub fn view() -> Element(msg) {
- [title]
- |> string.join("\n")
- |> layout.docs
-}
-
-// CONTENT: TITLE --------------------------------------------------------------
-
-const title: String = "
-# Managing state
-"
diff --git a/docs/src/app/page/docs/quickstart.gleam b/docs/src/app/page/docs/quickstart.gleam
deleted file mode 100644
index e78b97f..0000000
--- a/docs/src/app/page/docs/quickstart.gleam
+++ /dev/null
@@ -1,21 +0,0 @@
-// IMPORTS ---------------------------------------------------------------------
-
-import app/layout
-import gleam/string
-import gleam/io
-import lustre/element.{Element}
-
-// PAGE ------------------------------------------------------------------------
-
-pub fn view() -> Element(msg) {
- [title]
- |> io.debug
- |> string.join("\n")
- |> io.debug
- |> layout.docs
-}
-
-// CONTENT: TITLE --------------------------------------------------------------
-
-const title: String = "# Quickstart
-"
diff --git a/docs/src/app/page/docs/server_side_rendering.gleam b/docs/src/app/page/docs/server_side_rendering.gleam
deleted file mode 100644
index 561b532..0000000
--- a/docs/src/app/page/docs/server_side_rendering.gleam
+++ /dev/null
@@ -1,19 +0,0 @@
-// IMPORTS ---------------------------------------------------------------------
-
-import app/layout
-import gleam/string
-import lustre/element.{Element}
-
-// PAGE ------------------------------------------------------------------------
-
-pub fn view() -> Element(msg) {
- [title]
- |> string.join("\n")
- |> layout.docs
-}
-
-// CONTENT: TITLE --------------------------------------------------------------
-
-const title: String = "
-# Server-side rendering
-"
diff --git a/docs/src/app/page/docs/side_effects.gleam b/docs/src/app/page/docs/side_effects.gleam
deleted file mode 100644
index 965ec77..0000000
--- a/docs/src/app/page/docs/side_effects.gleam
+++ /dev/null
@@ -1,19 +0,0 @@
-// IMPORTS ---------------------------------------------------------------------
-
-import app/layout
-import gleam/string
-import lustre/element.{Element}
-
-// PAGE ------------------------------------------------------------------------
-
-pub fn view() -> Element(msg) {
- [title]
- |> string.join("\n")
- |> layout.docs
-}
-
-// CONTENT: TITLE --------------------------------------------------------------
-
-const title: String = "
-# Side effects
-"
diff --git a/docs/src/app/ui/hooks.gleam b/docs/src/app/ui/hooks.gleam
new file mode 100644
index 0000000..5fd807e
--- /dev/null
+++ b/docs/src/app/ui/hooks.gleam
@@ -0,0 +1,120 @@
+// 🚨 This module makes quite judicious use of `dynamic.unsafe_coerce` to wire
+// things up. As things are, this is sound because we control things in such a
+// way that it's impossible to pass in things that don't match up to the expected
+// types.
+//
+// If you're defining a new hook to export for this module, pay extra attention
+// to make sure you aren't introducing any soundness issues! 🚨
+
+// IMPORTS ---------------------------------------------------------------------
+
+import gleam/dynamic.{Dynamic, dynamic}
+import gleam/function
+import gleam/map.{Map}
+import gleam/result
+import lustre.{Error}
+import lustre/attribute.{Attribute, property}
+import lustre/effect.{Effect}
+import lustre/element.{Element, element}
+import lustre/event
+
+// HOOKS: STATE ----------------------------------------------------------------
+
+///
+///
+pub fn use_state(
+ init: state,
+ view: fn(state, fn(state) -> Msg, fn(msg) -> Msg) -> Element(Msg),
+) -> Element(msg) {
+ let attrs = [property("state", init), property("view", view), on_dispatch()]
+ let assert Ok(_) = register_hook("use-state")
+
+ element("use-state", attrs, [])
+}
+
+// HOOKS: REDUCER --------------------------------------------------------------
+
+///
+///
+pub fn use_reducer(
+ init: state,
+ update: fn(state, action) -> state,
+ view: fn(state, fn(action) -> Msg, fn(action) -> Msg) -> Element(Msg),
+) -> Element(msg) {
+ // The `use_reducer` hook is actually just the `use_state` hook under the hood
+ // with a wrapper around the `set_state` callback. We could just call out to
+ // `use_state` directly but we're doing it like this so that the DOM renders
+ // a separate `use-reducer` element, which I think is nicer.
+ let view = fn(state, set_state, emit) {
+ view(state, function.compose(update(state, _), set_state), emit)
+ }
+ let attrs = [property("state", init), property("view", view), on_dispatch()]
+ let assert Ok(_) = register_hook("use-reducer")
+
+ element("use-reducer", attrs, [])
+}
+
+// HOOKS: INTERNAL COMPONENT ---------------------------------------------------
+
+fn register_hook(name: String) -> Result(Nil, Error) {
+ // If a component is already registered we will just assume it's because this
+ // hook as already been used before. This isn't really an error state so we'll
+ // just return `Ok(Nil)` and let our hooks continue.
+ case lustre.is_registered(name) {
+ True -> Ok(Nil)
+ False ->
+ lustre.component(
+ name,
+ init_hook,
+ update_hook,
+ view_hook,
+ map.from_list([
+ #("state", dynamic.decode1(Set("state", _), Ok)),
+ #("view", dynamic.decode1(Set("view", _), Ok)),
+ ]),
+ )
+ }
+}
+
+type Model =
+ Map(String, Dynamic)
+
+fn init_hook() -> #(Model, Effect(msg)) {
+ #(map.new(), effect.none())
+}
+
+/// The type for messages handled internally by the different hooks. You typially
+/// won't need to import or refer to this type directly.
+///
+pub opaque type Msg {
+ Set(String, Dynamic)
+ Emit(Dynamic)
+}
+
+fn update_hook(model: Model, msg: Msg) -> #(Model, Effect(msg)) {
+ case msg {
+ Set(key, val) -> #(map.insert(model, key, val), effect.none())
+ Emit(msg) -> #(model, event.emit("dispatch", msg))
+ }
+}
+
+fn view_hook(model: Model) -> Element(Msg) {
+ case map.get(model, "state"), map.get(model, "view") {
+ Ok(state), Ok(view) -> {
+ let state = dynamic.unsafe_coerce(state)
+ let view = dynamic.unsafe_coerce(view)
+
+ view(state, Set("state", _), Emit)
+ }
+ _, _ -> element.text("???")
+ }
+}
+
+// EVENTS ----------------------------------------------------------------------
+
+fn on_dispatch() -> Attribute(msg) {
+ use event <- event.on("dispatch")
+ event
+ |> dynamic.field("detail", dynamic)
+ |> result.map(dynamic.unsafe_coerce)
+}
diff --git a/docs/src/app/ui/markdown.gleam b/docs/src/app/ui/markdown.gleam
index 8bc00da..5a78d0f 100644
--- a/docs/src/app/ui/markdown.gleam
+++ b/docs/src/app/ui/markdown.gleam
@@ -2,14 +2,34 @@
import gleam/int
import gleam/list
-import lustre/attribute
-import lustre/element.{Element}
+import lustre/attribute.{attribute}
+import lustre/element.{Element, element}
import lustre/element/html
+@external(javascript, "../../markdown.ffi.mjs", "parse_markdown")
+pub fn parse(md: String) -> #(List(Element(msg)), List(Element(msg)))
+
// MARKDOWN ELEMENTS -----------------------------------------------------------
+//
+// These are used in the FFI markdown renderer to convert the markdown AST into
+// lustre elements.
+//
-pub fn code(src: String) -> Element(msg) {
- html.pre([], [html.code([], [element.text(src)])])
+pub fn code(src: String, hash: String, lang: String) -> Element(msg) {
+ html.pre(
+ [attribute.class("not-prose rounded-xl")],
+ [
+ html.code(
+ [
+ attribute("data-hash", hash),
+ attribute("data-lang", lang),
+ attribute.class("language-" <> lang),
+ attribute.style([#("background", "transparent")]),
+ ],
+ [element.text(src)],
+ ),
+ ],
+ )
}
pub fn emphasis(content: List(Element(msg))) {
@@ -31,7 +51,7 @@ pub fn heading(
[attribute.class("flex items-center justify-between"), attribute.id(id)],
[
heading_title(title, id),
- html.p([attribute.class("flex gap-4")], tags),
+ html.p([attribute.class("flex gap-4 font-sans")], tags),
],
)
2 ->
@@ -42,7 +62,7 @@ pub fn heading(
],
[
heading_title(title, id),
- html.p([attribute.class("flex gap-4")], tags),
+ html.p([attribute.class("flex gap-4 font-sans")], tags),
],
)
3 ->
@@ -50,7 +70,7 @@ pub fn heading(
[attribute.class("flex items-center justify-between"), attribute.id(id)],
[
heading_title(title, id),
- html.p([attribute.class("flex gap-2")], tags),
+ html.p([attribute.class("flex gap-2 font-sans")], tags),
],
)
}
diff --git a/docs/src/app/ui/radix.gleam b/docs/src/app/ui/radix.gleam
new file mode 100644
index 0000000..d6c9755
--- /dev/null
+++ b/docs/src/app/ui/radix.gleam
@@ -0,0 +1,74 @@
+// All these icons are created by the amazing folks at Radix, and can be found
+// here: https://www.radix-ui.com/icons
+//
+
+// IMPORTS ---------------------------------------------------------------------
+
+import lustre/attribute.{Attribute, attribute}
+import lustre/element.{Element}
+import lustre/element/html.{svg}
+import lustre/element/svg
+
+// BASE ICON -------------------------------------------------------------------
+
+fn icon(attrs: List(Attribute(msg)), path: String) -> Element(msg) {
+ svg(
+ [attribute("viewBox", "0 0 15 15"), attribute("fill", "none"), ..attrs],
+ [
+ svg.path([
+ attribute("d", path),
+ attribute("fill", "currentColor"),
+ attribute("fill-rule", "evenodd"),
+ attribute("clip-rule", "evenodd"),
+ ]),
+ ],
+ )
+}
+
+// LOGOS -----------------------------------------------------------------------
+
+pub fn github(attrs: List(Attribute(msg))) -> Element(msg) {
+ icon(
+ attrs,
+ "M7.49933 0.25C3.49635 0.25 0.25 3.49593 0.25 7.50024C0.25 10.703 2.32715 13.4206 5.2081 14.3797C5.57084 14.446 5.70302 14.2222 5.70302 14.0299C5.70302 13.8576 5.69679 13.4019 5.69323 12.797C3.67661 13.235 3.25112 11.825 3.25112 11.825C2.92132 10.9874 2.44599 10.7644 2.44599 10.7644C1.78773 10.3149 2.49584 10.3238 2.49584 10.3238C3.22353 10.375 3.60629 11.0711 3.60629 11.0711C4.25298 12.1788 5.30335 11.8588 5.71638 11.6732C5.78225 11.205 5.96962 10.8854 6.17658 10.7043C4.56675 10.5209 2.87415 9.89918 2.87415 7.12104C2.87415 6.32925 3.15677 5.68257 3.62053 5.17563C3.54576 4.99226 3.29697 4.25521 3.69174 3.25691C3.69174 3.25691 4.30015 3.06196 5.68522 3.99973C6.26337 3.83906 6.8838 3.75895 7.50022 3.75583C8.1162 3.75895 8.73619 3.83906 9.31523 3.99973C10.6994 3.06196 11.3069 3.25691 11.3069 3.25691C11.7026 4.25521 11.4538 4.99226 11.3795 5.17563C11.8441 5.68257 12.1245 6.32925 12.1245 7.12104C12.1245 9.9063 10.4292 10.5192 8.81452 10.6985C9.07444 10.9224 9.30633 11.3648 9.30633 12.0413C9.30633 13.0102 9.29742 13.7922 9.29742 14.0299C9.29742 14.2239 9.42828 14.4496 9.79591 14.3788C12.6746 13.4179 14.75 10.7025 14.75 7.50024C14.75 3.49593 11.5036 0.25 7.49933 0.25Z",
+ )
+}
+
+pub fn discord(attrs: List(Attribute(msg))) -> Element(msg) {
+ icon(
+ attrs,
+ "M5.07451 1.82584C5.03267 1.81926 4.99014 1.81825 4.94803 1.82284C4.10683 1.91446 2.82673 2.36828 2.07115 2.77808C2.02106 2.80525 1.97621 2.84112 1.93869 2.88402C1.62502 3.24266 1.34046 3.82836 1.11706 4.38186C0.887447 4.95076 0.697293 5.55032 0.588937 5.98354C0.236232 7.39369 0.042502 9.08728 0.0174948 10.6925C0.0162429 10.7729 0.0351883 10.8523 0.0725931 10.9234C0.373679 11.496 1.02015 12.027 1.66809 12.4152C2.32332 12.8078 3.08732 13.1182 3.70385 13.1778C3.85335 13.1922 4.00098 13.1358 4.10282 13.0255C4.2572 12.8581 4.5193 12.4676 4.71745 12.1643C4.80739 12.0267 4.89157 11.8953 4.95845 11.7901C5.62023 11.9106 6.45043 11.9801 7.50002 11.9801C8.54844 11.9801 9.37796 11.9107 10.0394 11.7905C10.1062 11.8957 10.1903 12.0269 10.2801 12.1643C10.4783 12.4676 10.7404 12.8581 10.8947 13.0255C10.9966 13.1358 11.1442 13.1922 11.2937 13.1778C11.9102 13.1182 12.6742 12.8078 13.3295 12.4152C13.9774 12.027 14.6239 11.496 14.925 10.9234C14.9624 10.8523 14.9813 10.7729 14.9801 10.6925C14.9551 9.08728 14.7613 7.39369 14.4086 5.98354C14.3003 5.55032 14.1101 4.95076 13.8805 4.38186C13.6571 3.82836 13.3725 3.24266 13.0589 2.88402C13.0214 2.84112 12.9765 2.80525 12.9264 2.77808C12.1708 2.36828 10.8907 1.91446 10.0495 1.82284C10.0074 1.81825 9.96489 1.81926 9.92305 1.82584C9.71676 1.85825 9.5391 1.96458 9.40809 2.06355C9.26977 2.16804 9.1413 2.29668 9.0304 2.42682C8.86968 2.61544 8.71437 2.84488 8.61428 3.06225C8.27237 3.03501 7.90138 3.02 7.5 3.02C7.0977 3.02 6.72593 3.03508 6.38337 3.06244C6.28328 2.84501 6.12792 2.61549 5.96716 2.42682C5.85626 2.29668 5.72778 2.16804 5.58947 2.06355C5.45846 1.96458 5.2808 1.85825 5.07451 1.82584ZM11.0181 11.5382C11.0395 11.5713 11.0615 11.6051 11.0838 11.6392C11.2169 11.843 11.3487 12.0385 11.4508 12.1809C11.8475 12.0916 12.352 11.8818 12.8361 11.5917C13.3795 11.2661 13.8098 10.8918 14.0177 10.5739C13.9852 9.06758 13.7993 7.50369 13.4773 6.21648C13.38 5.82759 13.2038 5.27021 12.9903 4.74117C12.7893 4.24326 12.5753 3.82162 12.388 3.5792C11.7376 3.24219 10.7129 2.88582 10.0454 2.78987C10.0308 2.79839 10.0113 2.81102 9.98675 2.82955C9.91863 2.881 9.84018 2.95666 9.76111 3.04945C9.71959 3.09817 9.68166 3.1471 9.64768 3.19449C9.953 3.25031 10.2253 3.3171 10.4662 3.39123C11.1499 3.6016 11.6428 3.89039 11.884 4.212C12.0431 4.42408 12.0001 4.72494 11.788 4.884C11.5759 5.04306 11.2751 5.00008 11.116 4.788C11.0572 4.70961 10.8001 4.4984 10.1838 4.30877C9.58933 4.12585 8.71356 3.98 7.5 3.98C6.28644 3.98 5.41067 4.12585 4.81616 4.30877C4.19988 4.4984 3.94279 4.70961 3.884 4.788C3.72494 5.00008 3.42408 5.04306 3.212 4.884C2.99992 4.72494 2.95694 4.42408 3.116 4.212C3.35721 3.89039 3.85011 3.6016 4.53383 3.39123C4.77418 3.31727 5.04571 3.25062 5.35016 3.19488C5.31611 3.14738 5.27808 3.09831 5.23645 3.04945C5.15738 2.95666 5.07893 2.881 5.01081 2.82955C4.98628 2.81102 4.96674 2.79839 4.95217 2.78987C4.28464 2.88582 3.25999 3.24219 2.60954 3.5792C2.42226 3.82162 2.20825 4.24326 2.00729 4.74117C1.79376 5.27021 1.61752 5.82759 1.52025 6.21648C1.19829 7.50369 1.01236 9.06758 0.97986 10.5739C1.18772 10.8918 1.61807 11.2661 2.16148 11.5917C2.64557 11.8818 3.15003 12.0916 3.5468 12.1809C3.64885 12.0385 3.78065 11.843 3.9138 11.6392C3.93626 11.6048 3.95838 11.5708 3.97996 11.5375C3.19521 11.2591 2.77361 10.8758 2.50064 10.4664C2.35359 10.2458 2.4132 9.94778 2.63377 9.80074C2.85435 9.65369 3.15236 9.71329 3.29941 9.93387C3.56077 10.3259 4.24355 11.0201 7.50002 11.0201C10.7565 11.0201 11.4392 10.326 11.7006 9.93386C11.8477 9.71329 12.1457 9.65369 12.3663 9.80074C12.5869 9.94779 12.6465 10.2458 12.4994 10.4664C12.2262 10.8762 11.8041 11.2598 11.0181 11.5382ZM4.08049 7.01221C4.32412 6.74984 4.65476 6.60162 5.00007 6.59998C5.34538 6.60162 5.67603 6.74984 5.91966 7.01221C6.16329 7.27459 6.30007 7.62974 6.30007 7.99998C6.30007 8.37021 6.16329 8.72536 5.91966 8.98774C5.67603 9.25011 5.34538 9.39833 5.00007 9.39998C4.65476 9.39833 4.32412 9.25011 4.08049 8.98774C3.83685 8.72536 3.70007 8.37021 3.70007 7.99998C3.70007 7.62974 3.83685 7.27459 4.08049 7.01221ZM9.99885 6.59998C9.65354 6.60162 9.3229 6.74984 9.07926 7.01221C8.83563 7.27459 8.69885 7.62974 8.69885 7.99998C8.69885 8.37021 8.83563 8.72536 9.07926 8.98774C9.3229 9.25011 9.65354 9.39833 9.99885 9.39998C10.3442 9.39833 10.6748 9.25011 10.9184 8.98774C11.1621 8.72536 11.2989 8.37021 11.2989 7.99998C11.2989 7.62974 11.1621 7.27459 10.9184 7.01221C10.6748 6.74984 10.3442 6.60162 9.99885 6.59998Z",
+ )
+}
+
+// ABSTRACT --------------------------------------------------------------------
+
+pub fn hamburger(attrs: List(Attribute(msg))) -> Element(msg) {
+ icon(
+ attrs,
+ "M1.5 3C1.22386 3 1 3.22386 1 3.5C1 3.77614 1.22386 4 1.5 4H13.5C13.7761 4 14 3.77614 14 3.5C14 3.22386 13.7761 3 13.5 3H1.5ZM1 7.5C1 7.22386 1.22386 7 1.5 7H13.5C13.7761 7 14 7.22386 14 7.5C14 7.77614 13.7761 8 13.5 8H1.5C1.22386 8 1 7.77614 1 7.5ZM1 11.5C1 11.2239 1.22386 11 1.5 11H13.5C13.7761 11 14 11.2239 14 11.5C14 11.7761 13.7761 12 13.5 12H1.5C1.22386 12 1 11.7761 1 11.5Z",
+ )
+}
+
+pub fn cross(attrs: List(Attribute(msg))) -> Element(msg) {
+ icon(
+ attrs,
+ "M12.8536 2.85355C13.0488 2.65829 13.0488 2.34171 12.8536 2.14645C12.6583 1.95118 12.3417 1.95118 12.1464 2.14645L7.5 6.79289L2.85355 2.14645C2.65829 1.95118 2.34171 1.95118 2.14645 2.14645C1.95118 2.34171 1.95118 2.65829 2.14645 2.85355L6.79289 7.5L2.14645 12.1464C1.95118 12.3417 1.95118 12.6583 2.14645 12.8536C2.34171 13.0488 2.65829 13.0488 2.85355 12.8536L7.5 8.20711L12.1464 12.8536C12.3417 13.0488 12.6583 13.0488 12.8536 12.8536C13.0488 12.6583 13.0488 12.3417 12.8536 12.1464L8.20711 7.5L12.8536 2.85355Z",
+ )
+}
+
+// OBJECTS ---------------------------------------------------------------------
+
+pub fn sun(attrs: List(Attribute(msg))) -> Element(msg) {
+ icon(
+ attrs,
+ "M7.5 0C7.77614 0 8 0.223858 8 0.5V2.5C8 2.77614 7.77614 3 7.5 3C7.22386 3 7 2.77614 7 2.5V0.5C7 0.223858 7.22386 0 7.5 0ZM2.1967 2.1967C2.39196 2.00144 2.70854 2.00144 2.90381 2.1967L4.31802 3.61091C4.51328 3.80617 4.51328 4.12276 4.31802 4.31802C4.12276 4.51328 3.80617 4.51328 3.61091 4.31802L2.1967 2.90381C2.00144 2.70854 2.00144 2.39196 2.1967 2.1967ZM0.5 7C0.223858 7 0 7.22386 0 7.5C0 7.77614 0.223858 8 0.5 8H2.5C2.77614 8 3 7.77614 3 7.5C3 7.22386 2.77614 7 2.5 7H0.5ZM2.1967 12.8033C2.00144 12.608 2.00144 12.2915 2.1967 12.0962L3.61091 10.682C3.80617 10.4867 4.12276 10.4867 4.31802 10.682C4.51328 10.8772 4.51328 11.1938 4.31802 11.3891L2.90381 12.8033C2.70854 12.9986 2.39196 12.9986 2.1967 12.8033ZM12.5 7C12.2239 7 12 7.22386 12 7.5C12 7.77614 12.2239 8 12.5 8H14.5C14.7761 8 15 7.77614 15 7.5C15 7.22386 14.7761 7 14.5 7H12.5ZM10.682 4.31802C10.4867 4.12276 10.4867 3.80617 10.682 3.61091L12.0962 2.1967C12.2915 2.00144 12.608 2.00144 12.8033 2.1967C12.9986 2.39196 12.9986 2.70854 12.8033 2.90381L11.3891 4.31802C11.1938 4.51328 10.8772 4.51328 10.682 4.31802ZM8 12.5C8 12.2239 7.77614 12 7.5 12C7.22386 12 7 12.2239 7 12.5V14.5C7 14.7761 7.22386 15 7.5 15C7.77614 15 8 14.7761 8 14.5V12.5ZM10.682 10.682C10.8772 10.4867 11.1938 10.4867 11.3891 10.682L12.8033 12.0962C12.9986 12.2915 12.9986 12.608 12.8033 12.8033C12.608 12.9986 12.2915 12.9986 12.0962 12.8033L10.682 11.3891C10.4867 11.1938 10.4867 10.8772 10.682 10.682ZM5.5 7.5C5.5 6.39543 6.39543 5.5 7.5 5.5C8.60457 5.5 9.5 6.39543 9.5 7.5C9.5 8.60457 8.60457 9.5 7.5 9.5C6.39543 9.5 5.5 8.60457 5.5 7.5ZM7.5 4.5C5.84315 4.5 4.5 5.84315 4.5 7.5C4.5 9.15685 5.84315 10.5 7.5 10.5C9.15685 10.5 10.5 9.15685 10.5 7.5C10.5 5.84315 9.15685 4.5 7.5 4.5Z",
+ )
+}
+
+pub fn moon(attrs: List(Attribute(msg))) -> Element(msg) {
+ icon(
+ attrs,
+ "M2.89998 0.499976C2.89998 0.279062 2.72089 0.0999756 2.49998 0.0999756C2.27906 0.0999756 2.09998 0.279062 2.09998 0.499976V1.09998H1.49998C1.27906 1.09998 1.09998 1.27906 1.09998 1.49998C1.09998 1.72089 1.27906 1.89998 1.49998 1.89998H2.09998V2.49998C2.09998 2.72089 2.27906 2.89998 2.49998 2.89998C2.72089 2.89998 2.89998 2.72089 2.89998 2.49998V1.89998H3.49998C3.72089 1.89998 3.89998 1.72089 3.89998 1.49998C3.89998 1.27906 3.72089 1.09998 3.49998 1.09998H2.89998V0.499976ZM5.89998 3.49998C5.89998 3.27906 5.72089 3.09998 5.49998 3.09998C5.27906 3.09998 5.09998 3.27906 5.09998 3.49998V4.09998H4.49998C4.27906 4.09998 4.09998 4.27906 4.09998 4.49998C4.09998 4.72089 4.27906 4.89998 4.49998 4.89998H5.09998V5.49998C5.09998 5.72089 5.27906 5.89998 5.49998 5.89998C5.72089 5.89998 5.89998 5.72089 5.89998 5.49998V4.89998H6.49998C6.72089 4.89998 6.89998 4.72089 6.89998 4.49998C6.89998 4.27906 6.72089 4.09998 6.49998 4.09998H5.89998V3.49998ZM1.89998 6.49998C1.89998 6.27906 1.72089 6.09998 1.49998 6.09998C1.27906 6.09998 1.09998 6.27906 1.09998 6.49998V7.09998H0.499976C0.279062 7.09998 0.0999756 7.27906 0.0999756 7.49998C0.0999756 7.72089 0.279062 7.89998 0.499976 7.89998H1.09998V8.49998C1.09998 8.72089 1.27906 8.89997 1.49998 8.89997C1.72089 8.89997 1.89998 8.72089 1.89998 8.49998V7.89998H2.49998C2.72089 7.89998 2.89998 7.72089 2.89998 7.49998C2.89998 7.27906 2.72089 7.09998 2.49998 7.09998H1.89998V6.49998ZM8.54406 0.98184L8.24618 0.941586C8.03275 0.917676 7.90692 1.1655 8.02936 1.34194C8.17013 1.54479 8.29981 1.75592 8.41754 1.97445C8.91878 2.90485 9.20322 3.96932 9.20322 5.10022C9.20322 8.37201 6.82247 11.0878 3.69887 11.6097C3.45736 11.65 3.20988 11.6772 2.96008 11.6906C2.74563 11.702 2.62729 11.9535 2.77721 12.1072C2.84551 12.1773 2.91535 12.2458 2.98667 12.3128L3.05883 12.3795L3.31883 12.6045L3.50684 12.7532L3.62796 12.8433L3.81491 12.9742L3.99079 13.089C4.11175 13.1651 4.23536 13.2375 4.36157 13.3059L4.62496 13.4412L4.88553 13.5607L5.18837 13.6828L5.43169 13.7686C5.56564 13.8128 5.70149 13.8529 5.83857 13.8885C5.94262 13.9155 6.04767 13.9401 6.15405 13.9622C6.27993 13.9883 6.40713 14.0109 6.53544 14.0298L6.85241 14.0685L7.11934 14.0892C7.24637 14.0965 7.37436 14.1002 7.50322 14.1002C11.1483 14.1002 14.1032 11.1453 14.1032 7.50023C14.1032 7.25044 14.0893 7.00389 14.0623 6.76131L14.0255 6.48407C13.991 6.26083 13.9453 6.04129 13.8891 5.82642C13.8213 5.56709 13.7382 5.31398 13.6409 5.06881L13.5279 4.80132L13.4507 4.63542L13.3766 4.48666C13.2178 4.17773 13.0353 3.88295 12.8312 3.60423L12.6782 3.40352L12.4793 3.16432L12.3157 2.98361L12.1961 2.85951L12.0355 2.70246L11.8134 2.50184L11.4925 2.24191L11.2483 2.06498L10.9562 1.87446L10.6346 1.68894L10.3073 1.52378L10.1938 1.47176L9.95488 1.3706L9.67791 1.2669L9.42566 1.1846L9.10075 1.09489L8.83599 1.03486L8.54406 0.98184ZM10.4032 5.30023C10.4032 4.27588 10.2002 3.29829 9.83244 2.40604C11.7623 3.28995 13.1032 5.23862 13.1032 7.50023C13.1032 10.593 10.596 13.1002 7.50322 13.1002C6.63646 13.1002 5.81597 12.9036 5.08355 12.5522C6.5419 12.0941 7.81081 11.2082 8.74322 10.0416C8.87963 10.2284 9.10028 10.3497 9.34928 10.3497C9.76349 10.3497 10.0993 10.0139 10.0993 9.59971C10.0993 9.24256 9.84965 8.94373 9.51535 8.86816C9.57741 8.75165 9.63653 8.63334 9.6926 8.51332C9.88358 8.63163 10.1088 8.69993 10.35 8.69993C11.0403 8.69993 11.6 8.14028 11.6 7.44993C11.6 6.75976 11.0406 6.20024 10.3505 6.19993C10.3853 5.90487 10.4032 5.60464 10.4032 5.30023Z",
+ )
+}
diff --git a/docs/src/highlight.ffi.mjs b/docs/src/highlight.ffi.mjs
new file mode 100644
index 0000000..a67dfe2
--- /dev/null
+++ b/docs/src/highlight.ffi.mjs
@@ -0,0 +1,151 @@
+import "highlight.js/styles/github.css";
+
+let did_register_html = false;
+let did_register_js = false;
+let did_register_sh = false;
+let did_register_gleam = false;
+
+export async function highlight_element(el, lang) {
+ if (el.classList.contains("hljs")) return;
+
+ const { default: highlight } = await import("highlight.js/lib/core");
+
+ switch (lang) {
+ case !did_register_html && "html":
+ const { default: html } = await import("highlight.js/lib/languages/xml");
+ highlight.registerLanguage("html", html);
+ did_register_html = true;
+ break;
+
+ case !did_register_js && "javascript":
+ const { default: js } = await import(
+ "highlight.js/lib/languages/javascript"
+ );
+ highlight.registerLanguage("javascript", js);
+ did_register_js = true;
+ break;
+
+ case !did_register_sh && "shell":
+ const { default: sh } = await import("highlight.js/lib/languages/shell");
+ highlight.registerLanguage("sh", sh);
+ highlight.registerLanguage("shell", sh);
+ did_register_sh = true;
+ break;
+
+ case !did_register_gleam && "gleam":
+ highlight.registerLanguage("gleam", gleam);
+ did_register_gleam = true;
+ break;
+ }
+
+ highlight.highlightElement(el);
+}
+
+function gleam(hljs) {
+ const KEYWORDS =
+ "as assert case const fn if import let panic use opaque pub todo type";
+ const STRING = {
+ className: "string",
+ variants: [{ begin: /"/, end: /"/ }],
+ contains: [hljs.BACKSLASH_ESCAPE],
+ relevance: 0,
+ };
+ const NAME = {
+ className: "variable",
+ begin: "\\b[a-z][a-z0-9_]*\\b",
+ relevance: 0,
+ };
+ const DISCARD_NAME = {
+ className: "comment",
+ begin: "\\b_[a-z][a-z0-9_]*\\b",
+ relevance: 0,
+ };
+ const NUMBER = {
+ className: "number",
+ variants: [
+ {
+ // binary
+ begin: "\\b0[bB](?:_?[01]+)+",
+ },
+ {
+ // octal
+ begin: "\\b0[oO](?:_?[0-7]+)+",
+ },
+ {
+ // hex
+ begin: "\\b0[xX](?:_?[0-9a-fA-F]+)+",
+ },
+ {
+ // dec, float
+ begin: "\\b\\d(?:_?\\d+)*(?:\\.(?:\\d(?:_?\\d+)*)*)?",
+ },
+ ],
+ relevance: 0,
+ };
+
+ return {
+ name: "Gleam",
+ aliases: ["gleam"],
+ contains: [
+ hljs.C_LINE_COMMENT_MODE,
+ STRING,
+ {
+ // bit string
+ begin: "<<",
+ end: ">>",
+ contains: [
+ {
+ className: "keyword",
+ beginKeywords:
+ "binary bytes int float bit_string bits utf8 utf16 utf32 " +
+ "utf8_codepoint utf16_codepoint utf32_codepoint signed unsigned " +
+ "big little native unit size",
+ },
+ KEYWORDS,
+ STRING,
+ NAME,
+ DISCARD_NAME,
+ NUMBER,
+ ],
+ relevance: 10,
+ },
+ {
+ className: "function",
+ beginKeywords: "fn",
+ end: "\\(",
+ excludeEnd: true,
+ contains: [
+ {
+ className: "title",
+ begin: "[a-z][a-z0-9_]*\\w*",
+ relevance: 0,
+ },
+ ],
+ },
+ {
+ className: "attribute",
+ begin: "@",
+ end: "\\(",
+ excludeEnd: true,
+ },
+ {
+ className: "keyword",
+ beginKeywords: KEYWORDS,
+ },
+ {
+ // Type names and constructors
+ className: "title",
+ begin: "\\b[A-Z][A-Za-z0-9]*\\b",
+ relevance: 0,
+ },
+ {
+ className: "operator",
+ begin: "[+\\-*/%!=<>&|.]+",
+ relevance: 0,
+ },
+ NAME,
+ DISCARD_NAME,
+ NUMBER,
+ ],
+ };
+}
diff --git a/docs/src/markdown.ffi.mjs b/docs/src/markdown.ffi.mjs
new file mode 100644
index 0000000..7267c82
--- /dev/null
+++ b/docs/src/markdown.ffi.mjs
@@ -0,0 +1,164 @@
+import { attribute } from "../lustre/lustre/attribute.mjs";
+import { element, text } from "../lustre/lustre/element.mjs";
+import { List, Empty, NonEmpty } from "./gleam.mjs";
+import { fromMarkdown } from "mdast-util-from-markdown";
+import { highlight_element } from "./highlight.ffi.mjs";
+import * as Markdown from "./app/ui/markdown.mjs";
+
+const empty = new Empty();
+const singleton = (val) => new NonEmpty(val, empty);
+const fold_into_list = (arr, f) =>
+ arr.reduceRight((acc, val) => new NonEmpty(f(val), acc), empty);
+
+function compute_hash(str) {
+ let hash = 0;
+ for (let i = 0, len = str.length; i < len; i++) {
+ let chr = str.charCodeAt(i);
+ hash = (hash << 5) - hash + chr;
+ hash |= 0; // Convert to 32bit integer
+ }
+ return `${~hash}`;
+}
+
+function linkify(el) {
+ for (const [t, url] of Object.entries(links)) {
+ el.innerHTML = el.innerHTML.replace(
+ new RegExp(`\\b${t}\\b`, "g"),
+ `<a href="${url}" class="hover:underline">${t}</a>`
+ );
+ }
+}
+
+const base = import.meta.env.BASE_URL;
+const stdlib = "https://hexdocs.pm/gleam_stdlib/gleam/";
+const links = {
+ App: `${base}api/lustre#app-type`,
+ Attribute: `${base}api/lustre/attribute#attribute-type`,
+ Bool: `${stdlib}bool.html`,
+ Decoder: `${stdlib}dynamic.html#Decoder`,
+ Dynamic: `${stdlib}dynamic.html#Dynamic`,
+ Effect: `${base}api/lustre/effect#effect-type`,
+ Element: `${base}api/lustre/element#element-type`,
+ Error: `${base}api/lustre#error-type`,
+ Float: `${stdlib}float.html`,
+ Int: `${stdlib}int.html`,
+ List: `${stdlib}list.html`,
+ Map: `${stdlib}map.html#Map`,
+ Option: `${stdlib}option.html#Option`,
+ Result: `${stdlib}result.html`,
+ String: `${stdlib}string.html`,
+ StringBuilder: `${stdlib}string_builder.html#StringBuilder`,
+};
+
+const cashe = new Map();
+
+export function parse_markdown(md) {
+ window.requestAnimationFrame(() => {
+ const selector = `[data-hash]:not(.hljs)`;
+
+ for (const code of document.querySelectorAll(selector)) {
+ highlight_element(code, code.dataset.lang).then(() => {
+ linkify(code);
+ });
+ }
+ });
+
+ if (cashe.has(md)) return cashe.get(md);
+
+ const ast = fromMarkdown(md);
+ const summary = [];
+ const content = fold_into_list(
+ ast.children,
+ function to_lustre_element(node) {
+ switch (node.type) {
+ case "code":
+ return Markdown.code(node.value, compute_hash(node.value), node.lang);
+
+ case "emphasis":
+ return Markdown.emphasis(
+ fold_into_list(node.children, to_lustre_element)
+ );
+
+ case "heading": {
+ const [title, rest] = node.children[0].value.split("|");
+ const tags = List.fromArray(rest ? rest.trim().split(" ") : []);
+ const id =
+ /^[A-Z]/.test(title.trim()) &&
+ node.depth === 3 &&
+ window.location.pathname.includes("/api/")
+ ? `${title
+ .toLowerCase()
+ .trim()
+ .replace(/\s/g, "-")
+ .replace(/[^a-zA-Z0-9-_]/g, "")}-type`
+ : `${title
+ .toLowerCase()
+ .trim()
+ .replace(/\s/g, "-")
+ .replace(/[^a-zA-Z0-9-_]/g, "")}`;
+
+ if (node.depth > 1) {
+ summary.unshift(
+ element(
+ "a",
+ List.fromArray([
+ attribute("href", `#${id}`),
+ attribute("class", "text-sm text-gray-400 no-underline"),
+ attribute("class", "hover:text-gray-700 hover:underline"),
+ attribute(
+ "class",
+ node.depth === 2 ? `mt-4 first:mt-0` : `ml-2`
+ ),
+ ]),
+ singleton(text(title.trim()))
+ )
+ );
+ }
+
+ return Markdown.heading(node.depth, title.trim(), tags, id);
+ }
+
+ case "inlineCode":
+ return Markdown.inline_code(node.value);
+
+ case "link":
+ return Markdown.link(
+ node.url.startsWith("/")
+ ? import.meta.BASE_URL + node.url.slice(1)
+ : node.url,
+ fold_into_list(node.children, to_lustre_element)
+ );
+
+ case "list":
+ return Markdown.list(
+ !!node.ordered,
+ fold_into_list(node.children, to_lustre_element)
+ );
+
+ case "listItem":
+ return Markdown.list_item(
+ fold_into_list(node.children, to_lustre_element)
+ );
+
+ case "paragraph":
+ return Markdown.paragraph(
+ fold_into_list(node.children, to_lustre_element)
+ );
+
+ case "strong":
+ return Markdown.strong(
+ fold_into_list(node.children, to_lustre_element)
+ );
+
+ case "text":
+ return Markdown.text(node.value);
+
+ default:
+ return Markdown.text("");
+ }
+ }
+ );
+
+ cashe.set(md, [content, List.fromArray(summary)]);
+ return [content, List.fromArray(summary)];
+}