topcoat 0.9.0

A modular, batteries-included Rust web framework for server-rendered apps.
Documentation
Topcoat UI provides components you can copy into your project and edit. Run `topcoat ui add` to install a component, then change its source to suit your app.

Components use `#[component]`, `view!`, and Tailwind classes. Their colors and other shared styles come from a theme stylesheet installed by `topcoat ui init`.

# Setup

Set up the [`topcoat` CLI](https://github.com/tokio-rs/topcoat/blob/main/crates/topcoat/docs/getting_started.md#install-the-cli) and the [Tailwind integration](crate::tailwind), then enable the `ui` feature. This example also uses Fontsource to load the theme's font:

```toml
[dependencies]
topcoat = { version = "0.9.0", features = ["font-fontsource", "tailwind", "ui"] }

[build-dependencies]
topcoat = { version = "0.9.0", default-features = false, features = ["tailwind"] }
```

## Initialize the package

From the package directory, run:

```sh
topcoat ui init
```

The command creates `components.toml` to track installed components and writes the theme to `styles.css`. Check both files into version control. Run `init` before using the other UI commands.

If several themes are available, the command asks you to choose one. Pass `--theme <name>` to select it directly.

Components install into `src/components` by default. Use `--components-dir` to choose another directory. In a workspace, use `--package <name>` with any `topcoat ui` subcommand to select the package.

## Wire the theme into Tailwind

Use the installed `styles.css` as your Tailwind input. It imports Tailwind, defines the theme, and tells Tailwind to scan your Rust source for classes. Configure `build.rs` to use it:

```rust,no_run
fn main() {
    topcoat::tailwind::BuildConfig::new()
        .input("styles.css")
        .render()
        .unwrap();
}
```

## Load the stylesheet and the font

Load the generated stylesheet in your root layout. If the theme names a web font, load that font too. For example, use the [Fontsource integration](crate::font) to load Geist:

```rust,ignore
use topcoat::{
    Result,
    font::fontsource::fontsource_font,
    router::{Slot, layout},
    tailwind,
    view::{View, view},
};

#[layout]
async fn layout(slot: Slot<'_>) -> Result<impl View> {
    Ok(view! {
        <!DOCTYPE html>
        <html>
            <head>
                topcoat::font::link(font: fontsource_font!(GEIST))
                <link rel="stylesheet" href=(tailwind::stylesheet!())>
            </head>
            <body>
                (slot)
            </body>
        </html>
    })
}
```

The theme applies its base styles to `<body>` when the stylesheet loads.

# Adding components

Add components by name:

```sh
topcoat ui add button
```

The command copies the source into your components directory, adds its module declaration, and records the installation in `components.toml`. It also installs any components the requested component depends on.

Module declarations go in `src/components.rs`, or in `src/components/mod.rs` if that file already exists. If both files exist, resolve the conflict before adding components.

To see what is on offer, run:

```sh
topcoat ui list
```

The list shows available components and their install status. Use `--installed` to show only installed components.

# Using components

Installed components are ordinary modules in your crate. Import them and use them like any other component:

```rust,ignore
mod components;

use components::button::button;
use components::card::{
    card, card_content, card_description, card_footer, card_header, card_title,
};
use components::field::{field, field_description, field_group, field_label};
use components::input::input;
use topcoat::{Result, view::{View, attributes, component, view}};

#[component]
async fn sign_in() -> Result<impl View> {
    Ok(view! {
        card(
            card_header(
                card_title("Sign in")
                card_description("Use your work email to continue.")
            )
            card_content(
                <form id="sign-in" method="post" action="/login">
                    field_group(
                        field(
                            field_label(attrs: attributes! { for="email" }, "Email")
                            input(
                                attrs: attributes! {
                                    id="email"
                                    name="email"
                                    type="email"
                                    placeholder="you@example.com"
                                    aria-describedby="email-description"
                                }
                            )
                            field_description(
                                attrs: attributes! { id="email-description" },
                                "Use the email address associated with your account."
                            )
                        )
                    )
                </form>
            )
            card_footer(
                button(
                    attrs: attributes! { class="w-full" type="submit" form="sign-in" },
                    "Sign in"
                )
            )
        )
    })
}
```

The components share a few conventions:

- **Variants and sizes use enum props.** For example, pass `variant: ButtonVariant::Destructive` to style a destructive action.
- **Extra attributes go in `attrs`.** Build them with `attributes!`. Each component documents which element receives them. Classes are added to the existing classes without resolving conflicting Tailwind utilities. Use a more specific selector or edit the component to override a default reliably.
- **Pass content as children.** Children can contain text, HTML, and other components.
- **Reuse styles through class helpers.** For example, `button_variants` can give a link the appearance of a button:

  ```rust,ignore
  view! {
      <a href="/login" class=(button_variants(ButtonVariant::Outline, ButtonSize::Md))>
          "Sign in"
      </a>
  }
  ```

Read each component's source for its props and behavior. The [`ui` example](https://github.com/tokio-rs/topcoat/tree/main/examples/ui) provides a runnable showcase.

# Theming

A theme defines shared styles with CSS variables in `styles.css`. Components use classes such as `bg-primary` and `text-muted-foreground` to read those variables. Edit the values to change the theme across your app.

For example, `--background` sets the page color, while `--card` and `--card-foreground` set card colors. These values can be adjusted independently.

Add the `dark` class to `<html>` to enable dark mode for the page, or to another ancestor to enable it for a subtree. Edit the `.dark` variables in `styles.css` to change its colors.

The UI commands preserve `styles.css` after initialization, so you can customize it freely.

# Updating components

`topcoat ui list` reports an update when a component's registry source has changed since installation. It compares the current registry hash with the hash saved in `components.toml`. Edits to your local copy do not affect this comparison.

To pull the newer source, re-add the component:

```sh
topcoat ui add button --overwrite
```

This replaces your file with the registry's source. Save any local changes first, then review the diff and reapply the changes you want to keep.

# Removing components

Remove a component by name:

```sh
topcoat ui remove button
```

This deletes the component's file, removes its module declaration, and drops it from `components.toml`. It does not remove components that were installed as its dependencies.

# Custom registries

A custom registry is a Cargo crate that provides component sources and a `registry.toml` manifest. Point to the manifest's directory in the crate's metadata:

```toml
# Cargo.toml of the registry crate
[package.metadata.topcoat-ui]
registry = "registry"
```

```toml
# registry/registry.toml
version = 1

[themes.acme]
source = "themes/acme.css"

[components.button]
source = "src/components/button.rs"

[components.data_table]
source = "src/components/data_table.rs"
# Installed alongside data_table. An entry names a component in the same
# registry, or one in another registry by that registry's crate name.
dependencies = ["button", { registry = "other-registry-crate", name = "spinner" }]
```

Component versions are computed from their source. To distribute an update, publish a new version of the registry crate with the changed source.

To consume a registry, declare its crate as a direct dependency in `Cargo.toml`. Its components then show up in `topcoat ui list` and can be added with:

```sh
topcoat ui add data_table --registry my-registry-crate
```

Without `--registry`, the command prefers the built-in `topcoat` registry. It asks for confirmation if the component is available only from another registry. All registries share the same install directory. If a component would replace a file from another registry, the command asks before replacing it.

The built-in registry is available through Topcoat's `ui` feature and needs no separate dependency.