日本語

Styling

Styles are Rust values too. A misspelled property or keyword, or a value of the wrong type, is a compile error.

Atomic styles

Style has one method per standard CSS property, generated from MDN's CSS data. El::css adds each declaration as one class named after the declaration, as in the atomic CSS of Panda CSS and Tailwind CSS. The page writes the rules its elements use into <head>, and a patch that contains new elements includes their rules.

use hypersig::html::button;
use hypersig::style::value::{Color, Cursor, rem};
use hypersig::style::{Media, PseudoClass, Style, Theme, Token};
use hypersig::{Page, Signals};
use serde::{Deserialize, Serialize};

#[derive(Signals, Serialize, Deserialize)]
struct Cart {
    busy: bool,
}

let accent = Token::new("accent", Color::hex(0xb83f45));
let page = Page::new("Cart", Cart { busy: false })
    .theme(Theme::new().on(Media::Dark, Theme::new().set(&accent, Color::hex(0xff6b6b))))
    .child(
        button()
            .css(
                Style::new()
                    .padding(rem(0.5))
                    .color(accent.var())
                    .cursor(Cursor::Pointer)
                    .on(PseudoClass::Hover, Style::new().color(Color::CRIMSON))
                    .on(Media::Md, Style::new().padding(rem(1))),
            )
            .css_when(Style::new().opacity(0.5), Cart::busy())
            .text("Buy"),
    );
assert!(page.render().contains("hover:color_crimson"));
  • on(PseudoClass::Hover, ...) and on(Media::Md, ...) apply a style under a condition. Conditions cover pseudo-classes and pseudo-elements, media features, container sizes, and the state of a marked ancestor or sibling (group and peer).
  • css_when(style, cond) adds a style while a signal condition holds, and css_match(value, cases) picks one style per value of a signal.
  • A Token is a CSS custom property with a typed value. Page::theme sets tokens for the page, for example under Media::Dark.
  • Global holds rules for elements by tag with a small reset, @font-face, and @keyframes. hypersig::style::pattern has layouts such as stack and grid.

The badge below takes its color from css_match over an enum, and the green comes from a token with a dark value.

Result

status.rs
use hypersig::html::{div, span};
use hypersig::style::pattern::{hstack, stack};
use hypersig::style::value::{Color, FontWeight, px, rem};
use hypersig::style::{Media, Style, Theme, Token};
use hypersig::{Page, Signals, if_else};
use hypersig_ui::{ButtonSize, ButtonVariant, button};
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Clone, Copy, PartialEq)]
pub enum Status {
    Idle,
    Busy,
    Done,
}

#[derive(Signals, Serialize, Deserialize)]
pub struct Build {
    status: Status,
}

pub fn page() -> Page<Build> {
    let status = Build::status();
    let done = Token::new("done", Color::hex(0x0015_803d));
    let badge = span()
        .css(
            Style::new()
                .padding_block(rem(0.125))
                .padding_inline(rem(0.625))
                .border_radius(px(999))
                .font_size(rem(0.75))
                .font_weight(FontWeight::Number(600.0))
                .color(Color::WHITE),
        )
        .css_match(
            status,
            [
                (
                    Status::Idle,
                    Style::new().background_color(Color::hex(0x0052_525b)),
                ),
                (
                    Status::Busy,
                    Style::new().background_color(Color::hex(0x00b4_5309)),
                ),
                (Status::Done, Style::new().background_color(done.var())),
            ],
        )
        .bind_text(if_else(
            status.equals(&Status::Idle),
            "Idle",
            if_else(
                status.equals(&Status::Busy),
                "Building",
                "Done",
            ),
        ));
    let set = |value: Status, label: &str| {
        button(ButtonVariant::Outline, ButtonSize::Sm)
            .on_click(status.set(&value))
            .text(label)
    };
    Page::new(
        "Build",
        Build {
            status: Status::Idle,
        },
    )
    .theme(Theme::new().on(
        Media::Dark,
        Theme::new().set(&done, Color::hex(0x0016_a34a)),
    ))
    .child(
        div().css(stack(rem(1))).child(badge).child(
            div()
                .css(hstack(rem(0.5)))
                .child(set(
                    Status::Idle,
                    "Idle",
                ))
                .child(set(
                    Status::Busy,
                    "Busy",
                ))
                .child(set(
                    Status::Done,
                    "Done",
                )),
        ),
    )
}

Values the types do not check

A value with several parts, such as margin: 0 auto, a shadow, or a calc(), goes through raw(...), which is passed to the browser unchecked, and so is a value outside a property's range, such as a negative width. A page that loads an existing stylesheet keeps it: Stylesheet::class("name") checks at compile time that the stylesheet has a selector for the class.

The style example in the repository puts each feature on one page, and the ui example shows the components of hypersig_ui, which are built with these features.