maud-extensions 0.6.7

Component, inline CSS/JS, and font helper macros for Maud views.
Documentation
# maud-extensions

[![crates.io](https://img.shields.io/crates/v/maud-extensions.svg)](https://crates.io/crates/maud-extensions)
[![docs.rs](https://img.shields.io/docsrs/maud-extensions)](https://docs.rs/maud-extensions)

Small, local superpowers for Maud.

## Install

Recommended: install the crate as `mx` so the macro and component surface reads
well at call sites:

```bash
cargo add maud-extensions --rename mx
```

If you want the experimental component system too, enable `components` on the
same dependency:

```toml
[dependencies]
mx = { package = "maud-extensions", version = "0.6.7", features = ["components"] }
```

## Core Story

Write plain `html!` and emit local CSS and JS where they belong:

```rust
use maud::html;
use maud_extensions::{css, js};

fn status_card(message: &str) -> maud::Markup {
    html! {
        article class="status-card" {
            h2 { "System status" }
            p class="message" { (message) }

            (css! {
                me {
                    border: 1px solid #ddd;
                    border-radius: 10px;
                    padding: 12px;
                }
                me.ready {
                    border-color: #16a34a;
                }
            })

            (js!(once, {
                me().class_add("ready");
            }))
        }
    }
}
```

This is still the intended center of gravity:

- no wrapper component macro
- no hidden CSS/JS injection
- no stringly helper names
- plain Maud remains the main language

## Experimental Components

The component system is opt-in behind the `components` feature.

Preferred authoring pattern:

```rust
use maud::Markup;
use mx::{Component, Slot};

#[derive(Component)]
struct Card {
    title: String,
    header: Slot<Markup>,
    #[mx(default)]
    body: Slot<Markup>,
    footer: Slot<Markup>,
    #[mx(each = action)]
    actions: Slot<Vec<Markup>>,
}

impl Card {
    fn css() -> Markup {
        mx::css! {
            me {
                padding: 1rem;
                border: 1px solid #ddd;
            }

            me .actions {
                display: flex;
                gap: 0.5rem;
            }
        }
    }

    fn js() -> Markup {
        mx::js!(once, {
            me().class_add("ready");
        })
    }
}

impl maud::Render for Card {
    fn render(&self) -> Markup {
        maud::html! {
            article.card {
                (Self::css())
                (Self::js())
                header class="header" { (self.header) }
                h2 { (self.title) }
                div.body { (self.body) }
                footer class="footer" { (self.footer) }
                div.actions { (self.actions) }
            }
        }
    }
}

fn view() -> Markup {
    Card::new()
        .title("Profile")
        .header(maud::html! { span { "Welcome" } })
        .child(maud::html! { p { "Body" } })
        .footer(maud::html! { button { "Save" } })
        .action(maud::html! { button { "Edit" } })
        .render()
}
```

What this gives you:

- Bon-backed typed builders
- `Slot<Markup>` and `Slot<Vec<Markup>>` as the slot declaration path
- `#[mx(default)]` for the single default slot
- explicit colocated `fn css() -> Markup` and `fn js() -> Markup` helpers
- a normal `impl Render` where you place `(Self::css())` / `(Self::js())` exactly where they belong
- builder `.render()` just renders the completed component value

Current constraints:

- use `Slot<T>` / `Slot<Vec<T>>` for slot declarations
- reserve `#[mx(default)]` for selecting the default slot only
- use Rust `Default` or `Option<T>` for non-slot defaults
- if there are multiple slot fields, mark exactly one `#[mx(default)]`
- repeated slots use `Slot<Vec<T>>` plus `#[mx(each = item_name)]`

Mental model:

- `#[derive(Component)]` owns fields, slots, and the Bon-backed builder
- inherent `css()` / `js()` helpers own component-local assets
- the `Render` impl stays explicit and decides where those helpers are emitted

This keeps the builder story ergonomic while leaving rendering explicit and easy to reason about.

The normal path is just `.render()` on the complete builder. Use `.build()`
only when you specifically want the concrete component value first.

```rust
let markup = Card::new()
    .title("Profile")
    .child(maud::html! { p { "Body" } })
    .render();
```

## Bundled browser-side building blocks

The current CSS/JS/component story is designed to layer on top of a few small
browser-side tools:

- [Surreal]https://github.com/gnat/surreal for DOM ergonomics around `me()` /
  `any()` style behavior
- [css-scope-inline]https://github.com/gnat/css-scope-inline for colocated
  scoped CSS transforms
- [Preact Signals]https://github.com/preactjs/signals for the signals runtime
  surface the crate builds on

`maud-extensions` is not trying to replace those pieces; it is trying to make
them feel coherent and component-local from Maud.

To bootstrap the browser-side runtime in a page, use `mx::Init` in `<head>`:

```rust
use maud::html;
use mx::Init;

fn page() -> maud::Markup {
    html! {
        head {
            (Init::all())
        }
        body {
            // component markup here
        }
    }
}
```

Recommended bootstrap entrypoints:

- `Init::all()`
- `Init::new().surrealjs().scoped_css().signals().build()`

If you want a more minimal component style, you can still stop at plain
`html!` + `css!` + `js!`. The component system is intentionally a second layer,
not the only way to use the crate.

## Named Helpers

When reuse helps, define local helper functions with Rust identifiers:

```rust
use maud::html;
use maud_extensions::{css, js};

css!(card_css, {
    me { gap: px!(12); }
});

js!(card_js, once, {
    me().class_add("ready");
});

fn card() -> maud::Markup {
    html! {
        article.card {
            (card_css())
            (card_js())
            "Hello"
        }
    }
}
```

Supported `css!` forms:

- `css! { ... }`
- `css!(name, { ... })`

Supported `js!` forms:

- `js! { ... }`
- `js!(once, { ... })`
- `js!(name, { ... })`
- `js!(name, once, { ... })`

## CSS Helper Macros

Inside `css!` token mode you can use:

- `raw!(r#"..."#)`
- `media!(prelude, { ... })`
- `container!(prelude, { ... })`
- `supports!(prelude, { ... })`
- `layer!(prelude, { ... })`
- `keyframes!(prelude, { ... })`
- unit helpers:
  - `rem!(...)`
  - `em!(...)`
  - `px!(...)`
  - `pct!(...)`
  - `vw!(...)`
  - `vh!(...)`
  - `ms!(...)`
  - `s!(...)`

Example:

```rust
use maud_extensions::css;

fn responsive_styles() -> maud::Markup {
    css! {
        media!("(min-width: 48rem)", {
            me { padding: rem!(2); }
        })
        supports!("(display: grid)", {
            me { gap: px!(12); }
        })
    }
}
```

## Limits

- `css!` and `js!` are placement-sensitive local emitters
- `js!(once, ...)` relies on a `data-mx-js-ran` marker on the parent element
- CSS token mode only sees Rust-tokenizable input; use `raw!(...)` for arbitrary
  CSS fragments
- JavaScript is validated with SWC before emission
- CSS is checked for lightweight syntax and raw-text safety before emission