スタイル
スタイルも 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 のコンポーネントを示します。