pre-styled-ui slot recipe API
1. 目的とトレーサビリティ
本ドキュメントは fandhe-frontend-pre-styled-ui に実装した slot recipe 相当の variant API(SlotRecipe/VariantValue)と静的 CSS 生成の仕様を 記録する。chakra-ui の recipe / slot recipe を参考に、複数 anatomy パーツ (slot)を横断する variant(size / variant / colorPalette 相当)を型安全な Rust API(enum ベース)で定義し、クラス名と静的 CSS を決定的に生成する基盤である。
- 実装:
crates/pre-styled-ui/src/css.rs(低レベル宣言・検証・シリアライズ)・crates/pre-styled-ui/src/recipe.rs(SlotRecipe/VariantValue/Size、 compoundVariants 相当はVariantCondition/when/SlotRecipe::compound_variant) - テスト:
crates/pre-styled-ui/tests/recipe_css.rs(golden・headless 接続・ fail-closed・compound variant)・crates/pre-styled-ui/tests/recipe_determinism.rs(決定性、compound variant を含む)
2. 公開 API
// fandhe_frontend_pre_styled_ui::css
pub struct Declaration { /* property, value: &'static str */ }
pub const fn decl(property: &'static str, value: &'static str) -> Declaration;
// fandhe_frontend_pre_styled_ui::recipe
pub trait VariantValue: Copy {
fn axis(self) -> &'static str; // 例: "size"
fn value(self) -> &'static str; // 例: "sm"
}
pub enum Size { Sm, Md, Lg } // axis = "size"
// 標準 colorPalette 軸。crate::theme のセマンティック色
// (accent/info/success/warning/danger)と 1:1 対応する。
pub enum ColorPalette { Accent, Info, Success, Warning, Danger } // axis = "color-palette"
pub fn palette_declarations(p: ColorPalette) -> Vec<Declaration>;
pub struct SlotRecipe { /* ... */ }
impl SlotRecipe {
pub const fn new(scope: &'static str, slots: &'static [&'static str]) -> Self;
pub fn base(self, slot: &'static str, declarations: Vec<Declaration>) -> Self;
pub fn variant<V: VariantValue>(self, v: V, slot: &'static str, declarations: Vec<Declaration>) -> Self;
pub fn default_variant<V: VariantValue>(self, v: V) -> Self;
pub fn compound_variant(self, conditions: Vec<VariantCondition>, slot: &'static str, declarations: Vec<Declaration>) -> Self;
pub fn css(&self) -> String;
pub fn variant_class<V: VariantValue>(&self, v: V) -> String;
pub fn variant_classes(&self, selection: &[(&str, &str)]) -> String;
}
// compoundVariants 相当
pub struct VariantCondition { /* axis, value: &'static str(型消去済み) */ }
pub fn when<V: VariantValue>(v: V) -> VariantCondition;SlotRecipe::new/base/variant/default_variant は自己消費の builder (chakra-ui の defineSlotRecipe({ base, variants, defaultVariants }) 相当の 宣言を、通常の Rust メソッドチェーンで表現する。マクロ DSL は採用しない、REQ-5)。
colorPalette 相当は独立の仕組みではなく通常の variant 軸として表現できる (docs/api 掲載の例・tests/recipe_css.rs の ColorPalette enum 参照)。
compoundVariants 相当(複数軸の組み合わせ条件スタイル)は [SlotRecipe::compound_variant] で表現する。条件部は [when()] で [VariantValue] 実装 enum から作った [VariantCondition] の Vec(AND 条件)として渡す:
recipe.compound_variant(
vec![when(Size::Sm), when(ColorPalette::Red)],
"trigger",
vec![decl("font-weight", "bold")],
)生の文字列ではなく when() を介した enum ベースの構築のみを許すことで、 variant() と同じ型安全性を条件部でも保つ(tests/recipe_css.rs の tabs_recipe() 参照)。
3. scope と headless 層との契約
SlotRecipe::new(scope, slots) の scope は、対応する fandhe-frontend-headless-ui の Anatomy::new(scope)(例: crates/headless-ui/src/tabs.rs の const ANATOMY: Anatomy = anatomy("tabs");) と同じ値を渡す契約とする。slots は同コンポーネントの anatomy part 名一覧 (Tabs であれば root/list/trigger/content)と一致させる。
この契約により、SlotRecipe::css() が生成するセレクタ [data-scope="<scope>"][data-part="<slot>"] が、headless 層が Anatomy::part() を通じて実際にレンダリングする属性と一致する (crates/pre-styled-ui/tests/recipe_css.rs::base_selectors_match_actual_headless_markup が fandhe_frontend_headless_ui::tabs::tabs() の実マークアップと照合して固定する)。
scope はセレクタ・クラス名へ slot/axis/value と同様にそのまま埋め込まれるため、 SlotRecipe::css()/variant_class()/variant_classes() はいずれも呼び出し時に scope を is_valid_identifier(css.rs)で検証し、不正な場合は空文字列を fail-closed で返す(slot/axis/value 側の検証だけでは scope 経由の セレクタ脱出・</style> 混入を防げないため、scope にも同じ検証を適用する)。
4. セレクタ・クラス命名規則・出力書式(凍結)
- base セレクタ:
[data-scope="<scope>"][data-part="<slot>"](詳細度 (0,2,0)) - variant セレクタ:
[data-scope="<scope>"][data-part="<slot>"].fd-<scope>--<axis>-<value>(詳細度 (0,3,0)。base に必ず勝つため、CSS 記述順に依存しない上書きを保証する) - compound variant セレクタ:
[data-scope="<scope>"][data-part="<slot>"].fd-<scope>--<a1>-<v1>.fd-<scope>--<a2>-<v2>...(conditionsの登録順に条件クラスを連結する。新しいクラス名は生成せず、variant_classes()が emit する既存の軸別クラスの共起にセレクタとして 反応するだけなので、HTML 側への影響はない) - クラス名形式:
fd-{scope}--{axis}-{value}(prefixfdはライブラリ固定。変更用 API は設けない) - 出力書式(golden テストの前提、変更しない):
- 規則単位:
<selector> {\n <property>: <value>;\n ...\n}\n(インデント 2 スペース、1 宣言 1 行) - 規則間は空行 1 つ
SlotRecipe::css()全体の出力順: base(slots宣言順)→ variants (登録順)→ compound variants(登録順)
- 規則単位:
4.1 compound variant の上書き保証(2 段)
- 条件 2 個以上: セレクタの詳細度が (0,4,0) 以上となり、単一 variant セレクタ (0,3,0) に記述順へ依存せず必ず勝つ
- 条件 1 個: 詳細度は単一 variant と同じ (0,3,0) だが、compound ブロックを variants ブロックより後に出力するため CSS カスケードの後勝ちで上書きされる
chakra-ui の「compoundVariants は variants を上書きする」という意味論に この 2 段の保証で対応する。
5. 順序規約・決定性
- 内部ストレージは
Vecのみ。HashMap/HashSetは使わない(反復順序がプロセスごとに 変わりうる型を持ち込まない) - 同一 slot・同一 axis/value への複数回登録は「後に登録された規則が CSS 中で後に 出力される」(CSS のカスケードにおいて後勝ちになる)という規約に従う。これより 複雑な優先順位判定は行わない
variant_classes(selection)はselectionで指定されなかった axis をdefault_variantで補完する。戻り値は axis の登録順(variant/default_variantで最初に現れた順)で連結したクラス文字列- 決定性は
crates/pre-styled-ui/tests/recipe_determinism.rsが固定する: 同一入力 から独立に構築した 2 インスタンスのcss()/variant_classes()が byte 一致する こと、同一インスタンスへの繰り返し呼び出しが安定していること(compound variant を含む場合も同様)
6. fail-closed 検証ポリシー
crates/core/src/lib.rs が不正なタグ名・属性名を「panic させず出力からスキップ」 する規約(.claude/rules/coding-rust.md の panic 回避方針)を踏襲する。
- 識別子(scope / slot / axis / value):
[a-z][a-z0-9-]*に一致しない場合、その 規則・クラスを出力からスキップする - プロパティ名: 通常のプロパティ名に加えカスタムプロパティ(
--fd-*プレフィックス) を許容する(テーマトークン参照var(--fd-color-primary)を見越した設計) - 宣言値:
{};<および制御文字を含む場合、その宣言をスキップする。<の拒否は、下流(styled 部品・examples 等)が生成 CSS を<style>へ インライン埋め込みした場合の</style>突破(HTML コンテキスト脱出)を防ぐ セキュリティ上の不変条件である slotsに宣言していない slot へのbase/variant/compound_variant登録は 出力から除外する- compound variant 固有の検証(
crates/pre-styled-ui/tests/recipe_css.rs::compound_variant_fail_closed_cases_are_skipped_not_panickingが固定する):conditionsが空の規則は base と同義になる無意味な規則として除外するconditions内に同一 axis が重複する規則は、variant_classes()が 1 軸 につき高々 1 クラスしか emit しないため決して同時に一致しない矛盾条件で あるとみなし、dead CSS の混入防止として除外する- 条件の
(axis, value)の組がvariant()/default_variant()のいずれにも 未登録の規則は、axis/value のタイポによる dead CSS の混入防止として除外する (検証はcss()呼び出し時に行うため builder の呼び出し順には依存しない)
- いずれも panic なし・スキップ動作。
crates/pre-styled-ui/tests/recipe_css.rs::invalid_identifiers_and_structural_chars_are_skipped_not_panickingが固定する
7. テーマトークンとの関係
宣言値は不透明な &'static str として扱うため、トークン参照は decl("color", "var(--fd-color-primary)") のような値として自然に載る。
colorPalette 軸実配線時、palette_declarations は crate::theme が生成する --fandhe-color-*(テーマ層の名前空間)とは別の --fandhe-palette-* 名前空間へ、 選択された palette に対応する accent/info/success/warning/danger の 3 役割(base/emphasized/fg)を var() 参照として束ねる。styled 部品 (Button/Badge/Spinner、crates/pre-styled-ui/src/button.rs 等)は var(--fandhe-palette) 等を参照するだけで、palette variant の選択に応じて 色が切り替わる。名前空間を分離しているため、ユーザーがカスタムテーマへ Theme::push_color("palette", ...) のような独自トークンを追加しても --fandhe-palette-* の生成とは衝突しない。既定トークンの上書きは push_* ではなく upsert_* を使う(詳細は pre-styled-ui-api.md §4l)。
crate::theme は加えて radii(--fandhe-radius-<name>)・shadow (--fandhe-shadow-<name>、light/dark 2 値)トークングループを持つ。 styled 部品は border-radius/box-shadow の値としてこれらを参照する (例: decl("border-radius", "var(--fandhe-radius-md)"))。
関連ドキュメント
docs/api/pre-styled-ui-api.md: 本 API の上層 (styled 部品・stylesheet::StyleSheet)docs/internal/pre-styled-recipe-implementation-notes.md: 実装経緯・ スコープ外事項・トレーサビリティの記録(docs サイト非掲載のためリンク化 しない)