English

スタイル

スタイルも Rust の値です。プロパティやキーワードの打ち間違いや、型の違う値はコンパイルエラーになります。

アトミックなスタイル

Style には、標準の CSS プロパティごとに 1 つずつメソッドがあり、MDN の CSS データから生成しています。El::css は、Panda CSS や Tailwind CSS のアトミック CSS と同じく、宣言ごとにその宣言にちなんだ名前のクラスを 1 つ付けます。ページは要素が使うルールを <head> に書き、新しい要素を含む差分には、その要素のルールも含まれます。

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, ...) や on(Media::Md, ...) は条件付きでスタイルを適用します。条件には、疑似クラスと疑似要素、メディア特性、コンテナのサイズ、印を付けた祖先や兄弟の状態(group と peer)があります。
  • css_when(style, cond) はシグナルの条件が成り立つ間だけスタイルを付け、css_match(value, cases) はシグナルの値ごとにスタイルを 1 つ選びます。
  • Token は型の付いた値を持つ CSS カスタムプロパティです。Page::theme は、たとえば Media::Dark のもとでのトークンをページに設定します。
  • Global はタグで指定する要素のルールを、小さなリセット、@font-face、@keyframes と一緒に持ちます。hypersig::style::pattern には stack や grid のようなレイアウトがあります。

下のバッジは、列挙型に対する css_match で色が決まります。緑はダークモード用の値を持つトークンの色です。

実行結果

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),
            "待機中",
            if_else(
                status.equals(&Status::Busy),
                "ビルド中",
                "完了",
            ),
        ));
    let set = |value: Status, label: &str| {
        button(ButtonVariant::Outline, ButtonSize::Sm)
            .on_click(status.set(&value))
            .text(label)
    };
    Page::new(
        "ビルド",
        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,
                    "待機中",
                ))
                .child(set(
                    Status::Busy,
                    "ビルド中",
                ))
                .child(set(
                    Status::Done,
                    "完了",
                )),
        ),
    )
}

型が検査しない値

margin: 0 auto、影、calc() のように複数の部分からなる値は raw(...) で書き、検査されないままブラウザに渡ります。負の幅のように、プロパティの範囲を外れた値も検査されません。既存のスタイルシートを読み込むページは、そのまま使い続けられます。Stylesheet::class("name") は、そのスタイルシートにクラスのセレクタがあることをコンパイル時に検査します。

リポジトリの style の例はそれぞれの機能を 1 つのページにまとめています。ui の例は、これらの機能を使って作った hypersig_ui のコンポーネントを示します。