English

条件

Style::on はスタイルの一部を条件の下でだけ適用します。条件には擬似クラス、メディア特性、コンテナの幅、印を付けた祖先や兄弟の状態を使えます。

擬似クラス、メディア、コンテナ

use hypersig::style::value::{Color, Display, Opacity, pct, quoted, rem};
use hypersig::style::{Container, Media, PseudoClass, PseudoElement, Style};

let link = Style::new()
    .color(Color::hex(0x0025_63eb))
    // A pseudo-class.
    .on(PseudoClass::Hover, Style::new().color(Color::hex(0x001d_4ed8)))
    // Conditions nest: hovered and focused at once.
    .on(
        PseudoClass::Hover,
        Style::new().on(PseudoClass::FocusVisible, Style::new().opacity(Opacity::Number(0.8))),
    )
    // A pseudo-element.
    .pseudo(PseudoElement::After, Style::new().content(quoted(" →")));

let column = Style::new()
    .width(pct(100))
    // From 48rem wide, and from 64rem wide.
    .on(Media::Md, Style::new().width(pct(50)))
    .on(Media::Lg, Style::new().width(pct(33)))
    // The user's settings.
    .on(Media::ReducedMotion, Style::new().display(Display::Block))
    // The nearest query container, from 24rem wide.
    .on(Container::Sm, Style::new().padding(rem(2)));
  • PseudoClass と PseudoElement は MDN のデータから生成しています。pseudo は擬似要素に宣言を付けます。その中に書いた擬似クラスは元の要素に対して適用されるので、pseudo(Before, Style::new().on(Hover, ..)) は :hover::before になります。
  • ユーザー操作の擬似クラスは決まった順でスタイルシートに書かれます。そのため、ボタンを押している間は :active が :hover より優先され、:disabled はそのどれよりも優先されます。
  • Media にはブレークポイント Sm、Md、Lg、Xl、Xxl があり、それぞれ 40、48、64、80、96rem の min-width のメディアクエリです。幅は Tailwind CSS と Panda CSS が使うものと同じです。ほかに Dark、Light、ReducedMotion、Print、Landscape、Portrait、CanHover、CoarsePointer、ForcedColors、MoreContrast があります。
  • Container には、Panda CSS と同じ 16rem から 90rem までのコンテナの大きさがあります。祖先は container_type でクエリコンテナになります。そうした祖先がなければ、条件は一度も当てはまりません。
  • 条件は入れ子にできます。on に渡すスタイルの中でも on を使えます。その場合、内側の宣言は両方の条件が成り立つときに適用されます。

group と peer

Style::group は祖先に印を付け、Condition::Group(PseudoClass::Hover) はその祖先がホバーされている間だけスタイルを適用します。Style::peer は前にある兄弟に印を付け、Condition::Peer(PseudoClass::Checked) はその兄弟がチェックされている間だけスタイルを適用します。どちらも Tailwind CSS や Panda CSS の group と peer と同じように動きます。

use hypersig::html::{a, div, input, label, span};
use hypersig::style::value::{Color, Opacity};
use hypersig::style::{Condition, PseudoClass, Style};
use hypersig::{SafeUrl, Signals};
use serde::{Deserialize, Serialize};

#[derive(Signals, Serialize, Deserialize)]
struct Settings {
    agreed: bool,
}

// The arrow shows while the pointer is anywhere on the card.
let card = a::<Settings>()
    .href(SafeUrl::new("/docs"))
    .css(Style::new().group())
    .text("Read the docs")
    .child(
        span()
            .css(
                Style::new()
                    .opacity(Opacity::Number(0.0))
                    .on(Condition::Group(PseudoClass::Hover), Style::new().opacity(Opacity::Number(1.0))),
            )
            .text(" →"),
    );

// The text turns green while the checkbox before it is checked.
let agree = label::<Settings>()
    .child(input().attr("type", "checkbox").css(Style::new().peer()).bind(Settings::agreed()))
    .child(
        span()
            .css(Style::new().on(Condition::Peer(PseudoClass::Checked), Style::new().color(Color::GREEN)))
            .text("I agree"),
    );

実行結果

hover.rs
use hypersig::html::{a, div, input, label, p, span};
use hypersig::style::pattern::stack;
use hypersig::style::value::{
    BorderStyle, Color, FontWeight, Opacity, TextDecorationLine, px, rem,
};
use hypersig::style::{Condition, PseudoClass, Style};
use hypersig::{Page, SafeUrl, Signals};
use hypersig_ui::token;
use serde::{Deserialize, Serialize};

#[derive(Signals, Serialize, Deserialize)]
pub struct Settings {
    agreed: bool,
}

pub fn page() -> Page<Settings> {
    let card = a()
        .href(SafeUrl::new("#"))
        .css(
            stack(rem(0.25))
                .group()
                .padding(rem(1))
                .border_width(px(1))
                .border_style(BorderStyle::Solid)
                .border_color(token::border().var())
                .border_radius(px(8))
                .on(
                    PseudoClass::Hover,
                    Style::new().background_color(token::muted().var()),
                ),
        )
        .child(
            p().css(Style::new().font_weight(FontWeight::Number(600.0)))
                .text("ドキュメントを読む")
                .child(
                    span()
                        .css(Style::new().opacity(Opacity::Number(0.0)).on(
                            Condition::Group(PseudoClass::Hover),
                            Style::new().opacity(Opacity::Number(1.0)),
                        ))
                        .text(" →"),
                ),
        )
        .child(
            p().css(Style::new().color(token::muted_foreground().var()).on(
                Condition::Group(PseudoClass::Hover),
                Style::new().text_decoration_line(TextDecorationLine::Underline),
            ))
            .text("カードのどこかにポインタを乗せてください。"),
        );
    let agree = label()
        .child(
            input()
                .attr("type", "checkbox")
                .css(Style::new().peer())
                .bind(Settings::agreed()),
        )
        .child(
            span()
                .css(
                    Style::new().margin_left(rem(0.5)).on(
                        Condition::Peer(PseudoClass::Checked),
                        Style::new()
                            .color(Color::hex(0x0015_803d))
                            .font_weight(FontWeight::Number(600.0)),
                    ),
                )
                .text("利用規約に同意します"),
        );
    Page::new(
        "設定",
        Settings { agreed: false },
    )
    .child(
        div()
            .css(stack(rem(1.25)).max_width(rem(20)))
            .child(card)
            .child(agree),
    )
}