From 3ab89adf2d95270852ce7226b827593b51e818e4 Mon Sep 17 00:00:00 2001 From: slhx agent Date: Tue, 2 Jun 2026 07:24:10 +0200 Subject: [PATCH] feat(style): compose generated class states Add CssClass/CssClasses with/with_if helpers so nested partial view data can compose generated CSS constants without ad hoc class strings. req: style/003 req: component/003 --- REQUIREMENTS.md | 2 +- examples/techdemo/src/main.rs | 9 +-------- slhx-core/src/lib.rs | 24 ++++++++++++++++++++++++ slhx-core/tests/effect_batch.rs | 3 +++ 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md index fc09da9..2b223c7 100644 --- a/REQUIREMENTS.md +++ b/REQUIREMENTS.md @@ -860,7 +860,7 @@ All forms support returning `impl IntoEffect` and compose through tuples. 002 Generated class constants are ergonomic references only: they do not create a CSS framework, require a framework project structure, or make dynamic class expressions compile-time facts. Unknown Rust class references fail by normal Rust name resolution when the generated constant is absent. [north_star] ### req: style/003 -003 When a hemplate dynamic class attribute needs more than one class token, Rust passes a displayable list of generated `CssClass` values as view data. Rust should not assemble ad hoc class strings for known style tokens. [north_star] +003 When a hemplate dynamic class attribute needs more than one class token, Rust passes a displayable list of generated `CssClass` values as view data. Conditional state classes should compose from generated constants with boring Rust helpers such as `classes::card.with_if(selected, classes::is_selected)`. Rust should not assemble ad hoc class strings for known style tokens. [north_star] --- diff --git a/examples/techdemo/src/main.rs b/examples/techdemo/src/main.rs index 922e97c..314cfef 100644 --- a/examples/techdemo/src/main.rs +++ b/examples/techdemo/src/main.rs @@ -504,14 +504,7 @@ fn board_view(demo: &DemoState) -> BoardLanes { fn issue_card(item: &WorkItem, selected: bool) -> IssueCard { IssueCard { - class: if selected { - CssClasses::from([ - classes::work_card, - classes::is_selected, - ]) - } else { - CssClasses::from(classes::work_card) - }, + class: classes::work_card.with_if(selected, classes::is_selected), id: item.id, title: item.title.clone(), stage: item.stage.label(), diff --git a/slhx-core/src/lib.rs b/slhx-core/src/lib.rs index 5cdb496..7b27669 100644 --- a/slhx-core/src/lib.rs +++ b/slhx-core/src/lib.rs @@ -186,6 +186,14 @@ impl CssClass { pub const fn as_str(self) -> &'static str { self.name } + + pub fn with(self, class: CssClass) -> CssClasses { + CssClasses::from([self, class]) + } + + pub fn with_if(self, condition: bool, class: CssClass) -> CssClasses { + CssClasses::from(self).with_if(condition, class) + } } impl AsRef for CssClass { @@ -315,6 +323,22 @@ impl CssClasses { pub fn as_str(&self) -> &str { &self.names } + + pub fn with(mut self, class: CssClass) -> Self { + if !self.names.is_empty() { + self.names.push(' '); + } + self.names.push_str(class.as_str()); + self + } + + pub fn with_if(self, condition: bool, class: CssClass) -> Self { + if condition { + self.with(class) + } else { + self + } + } } impl From for CssClasses { diff --git a/slhx-core/tests/effect_batch.rs b/slhx-core/tests/effect_batch.rs index 3e386b9..b30a4f2 100644 --- a/slhx-core/tests/effect_batch.rs +++ b/slhx-core/tests/effect_batch.rs @@ -141,6 +141,9 @@ fn generated_css_classes_join_for_hemplate_dynamic_class_attrs() { assert_eq!(classes.as_str(), "work-card is-selected"); assert_eq!(classes.to_string(), "work-card is-selected"); + assert_eq!(CARD.with(SELECTED).as_str(), "work-card is-selected"); + assert_eq!(CARD.with_if(true, SELECTED).as_str(), "work-card is-selected"); + assert_eq!(CARD.with_if(false, SELECTED).as_str(), "work-card"); } #[test]