From 50ec5bd920e98ac52c5281bb8b8730fd483aa2e9 Mon Sep 17 00:00:00 2001 From: tommy Date: Fri, 7 Nov 2025 15:39:27 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ui/aspect_ratio.rs | 10 +----- src/components/ui/calendar.rs | 10 +----- src/components/ui/card.rs | 10 +----- src/components/ui/checkbox.rs | 10 +----- src/components/ui/collapsible.rs | 10 +----- src/components/ui/combobox.rs | 10 +----- src/components/ui/date_range_picker.rs | 10 +----- src/components/ui/file_drop_zone.rs | 10 +----- src/components/ui/form_field.rs | 10 +----- src/components/ui/input.rs | 10 +----- src/components/ui/label.rs | 10 +----- src/components/ui/mod.rs | 2 ++ src/components/ui/progress.rs | 10 +----- src/components/ui/radio_group.rs | 10 +----- src/components/ui/scroll_area.rs | 10 +----- src/components/ui/separator.rs | 10 +----- src/components/ui/sidebar.rs | 18 +--------- src/components/ui/skeleton.rs | 10 +----- src/components/ui/slider.rs | 10 +----- src/components/ui/switch.rs | 10 +----- src/components/ui/table.rs | 10 +----- src/components/ui/tabs.rs | 11 +----- src/components/ui/textarea.rs | 10 +----- src/components/ui/toggle.rs | 10 +----- src/components/ui/toggle_group.rs | 10 +----- src/components/ui/utils.rs | 48 ++++++++++++++++++++++++++ 26 files changed, 74 insertions(+), 225 deletions(-) create mode 100644 src/components/ui/utils.rs diff --git a/src/components/ui/aspect_ratio.rs b/src/components/ui/aspect_ratio.rs index 0e0c790..aff3765 100644 --- a/src/components/ui/aspect_ratio.rs +++ b/src/components/ui/aspect_ratio.rs @@ -1,13 +1,5 @@ use dioxus::prelude::*; - -fn merge_class(base: &str, extra: Option) -> String { - if let Some(extra) = extra.filter(|extra| !extra.trim().is_empty()) { - format!("{base} {}", extra.trim()) - } else { - base.to_string() - } -} - +use super::utils::merge_class; #[component] pub fn AspectRatio( #[props(default = 1.0f32)] ratio: f32, diff --git a/src/components/ui/calendar.rs b/src/components/ui/calendar.rs index 053f744..7454450 100644 --- a/src/components/ui/calendar.rs +++ b/src/components/ui/calendar.rs @@ -1,14 +1,6 @@ use chrono::{Datelike, Duration, NaiveDate}; use dioxus::prelude::*; - -fn merge_class(base: &str, extra: Option) -> String { - if let Some(extra) = extra.filter(|extra| !extra.trim().is_empty()) { - format!("{base} {}", extra.trim()) - } else { - base.to_string() - } -} - +use super::utils::merge_class; const WEEKDAY_LABELS: [&str; 7] = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]; fn first_day_of_month(date: NaiveDate) -> NaiveDate { diff --git a/src/components/ui/card.rs b/src/components/ui/card.rs index 4be5e56..00fcf78 100644 --- a/src/components/ui/card.rs +++ b/src/components/ui/card.rs @@ -1,13 +1,5 @@ use dioxus::prelude::*; - -fn merge_class(base: &str, extra: Option) -> String { - if let Some(extra) = extra.filter(|extra| !extra.trim().is_empty()) { - format!("{base} {}", extra.trim()) - } else { - base.to_string() - } -} - +use super::utils::merge_class; #[component] pub fn Card(#[props(into, default)] class: Option, children: Element) -> Element { let classes = merge_class("ui-card", class); diff --git a/src/components/ui/checkbox.rs b/src/components/ui/checkbox.rs index 9d60f20..26b2cb5 100644 --- a/src/components/ui/checkbox.rs +++ b/src/components/ui/checkbox.rs @@ -1,13 +1,5 @@ use dioxus::prelude::*; - -fn merge_class(base: &str, extra: Option) -> String { - if let Some(extra) = extra.filter(|extra| !extra.trim().is_empty()) { - format!("{base} {}", extra.trim()) - } else { - base.to_string() - } -} - +use super::utils::merge_class; #[component] pub fn Checkbox( #[props(default)] checked: bool, diff --git a/src/components/ui/collapsible.rs b/src/components/ui/collapsible.rs index 54eecd0..6d32b3a 100644 --- a/src/components/ui/collapsible.rs +++ b/src/components/ui/collapsible.rs @@ -1,13 +1,5 @@ use dioxus::prelude::*; - -fn merge_class(base: &str, extra: Option) -> String { - if let Some(extra) = extra.filter(|extra| !extra.trim().is_empty()) { - format!("{base} {}", extra.trim()) - } else { - base.to_string() - } -} - +use super::utils::merge_class; #[derive(Clone)] struct CollapsibleContext { open: Signal, diff --git a/src/components/ui/combobox.rs b/src/components/ui/combobox.rs index 4eea3f3..3cef7b2 100644 --- a/src/components/ui/combobox.rs +++ b/src/components/ui/combobox.rs @@ -1,13 +1,5 @@ use dioxus::prelude::*; - -fn merge_class(base: &str, extra: Option) -> String { - if let Some(extra) = extra.filter(|extra| !extra.trim().is_empty()) { - format!("{base} {}", extra.trim()) - } else { - base.to_string() - } -} - +use super::utils::merge_class; #[derive(Clone, PartialEq)] pub struct ComboboxOption { pub label: String, diff --git a/src/components/ui/date_range_picker.rs b/src/components/ui/date_range_picker.rs index e493bb2..48770f5 100644 --- a/src/components/ui/date_range_picker.rs +++ b/src/components/ui/date_range_picker.rs @@ -1,18 +1,10 @@ use super::button::{Button, ButtonSize, ButtonVariant}; use chrono::{Datelike, Duration, NaiveDate}; use dioxus::prelude::*; -#[cfg(not(target_arch = "wasm32"))] +use super::utils::merge_class;#[cfg(not(target_arch = "wasm32"))] use std::time::SystemTime; use crate::components::ui::PopoverHandle; -fn merge_class(base: &str, extra: Option) -> String { - if let Some(extra) = extra.filter(|extra| !extra.trim().is_empty()) { - format!("{base} {}", extra.trim()) - } else { - base.to_string() - } -} - #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct DateRange { pub start: NaiveDate, diff --git a/src/components/ui/file_drop_zone.rs b/src/components/ui/file_drop_zone.rs index b7fb8c4..9b75c7a 100644 --- a/src/components/ui/file_drop_zone.rs +++ b/src/components/ui/file_drop_zone.rs @@ -1,15 +1,7 @@ use dioxus::html::events::{DragEvent, FormEvent}; use dioxus::html::{FileData, HasFileData}; use dioxus::prelude::*; - -fn merge_class(base: &str, extra: Option) -> String { - if let Some(extra) = extra.filter(|extra| !extra.trim().is_empty()) { - format!("{base} {}", extra.trim()) - } else { - base.to_string() - } -} - +use super::utils::merge_class; #[derive(Clone, Debug, PartialEq)] pub struct FileMetadata { pub name: String, diff --git a/src/components/ui/form_field.rs b/src/components/ui/form_field.rs index f09acb3..4440ef6 100644 --- a/src/components/ui/form_field.rs +++ b/src/components/ui/form_field.rs @@ -1,14 +1,6 @@ use crate::components::ui::Label; use dioxus::prelude::*; - -fn merge_class(base: &str, extra: Option) -> String { - if let Some(extra) = extra.filter(|extra| !extra.trim().is_empty()) { - format!("{base} {}", extra.trim()) - } else { - base.to_string() - } -} - +use super::utils::merge_class; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum FormMessageVariant { Helper, diff --git a/src/components/ui/input.rs b/src/components/ui/input.rs index ff486c2..2db6ed3 100644 --- a/src/components/ui/input.rs +++ b/src/components/ui/input.rs @@ -1,13 +1,5 @@ use dioxus::prelude::*; - -fn merge_class(base: &str, extra: Option) -> String { - if let Some(extra) = extra.filter(|extra| !extra.trim().is_empty()) { - format!("{base} {}", extra.trim()) - } else { - base.to_string() - } -} - +use super::utils::merge_class; #[component] pub fn Input( #[props(into, default)] class: Option, diff --git a/src/components/ui/label.rs b/src/components/ui/label.rs index cf80ef2..22e15ad 100644 --- a/src/components/ui/label.rs +++ b/src/components/ui/label.rs @@ -1,13 +1,5 @@ use dioxus::prelude::*; - -fn merge_class(base: &str, extra: Option) -> String { - if let Some(extra) = extra.filter(|extra| !extra.trim().is_empty()) { - format!("{base} {}", extra.trim()) - } else { - base.to_string() - } -} - +use super::utils::merge_class; #[component] pub fn Label( #[props(into, default)] class: Option, diff --git a/src/components/ui/mod.rs b/src/components/ui/mod.rs index e2085e5..fe38574 100644 --- a/src/components/ui/mod.rs +++ b/src/components/ui/mod.rs @@ -2,6 +2,8 @@ //! Each component mirrors the styling and API conventions of the upstream React components while //! remaining idiomatic to Rust and Dioxus. +mod utils; + mod accordion; mod alert; mod aspect_ratio; diff --git a/src/components/ui/progress.rs b/src/components/ui/progress.rs index 775eb1a..ff25f92 100644 --- a/src/components/ui/progress.rs +++ b/src/components/ui/progress.rs @@ -1,13 +1,5 @@ use dioxus::prelude::*; - -fn merge_class(base: &str, extra: Option) -> String { - if let Some(extra) = extra.filter(|extra| !extra.trim().is_empty()) { - format!("{base} {}", extra.trim()) - } else { - base.to_string() - } -} - +use super::utils::merge_class; #[component] pub fn Progress( #[props(default = 0.0f32)] value: f32, diff --git a/src/components/ui/radio_group.rs b/src/components/ui/radio_group.rs index 675201f..8c387ca 100644 --- a/src/components/ui/radio_group.rs +++ b/src/components/ui/radio_group.rs @@ -1,15 +1,7 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use dioxus::prelude::*; - -fn merge_class(base: &str, extra: Option) -> String { - if let Some(extra) = extra.filter(|extra| !extra.trim().is_empty()) { - format!("{base} {}", extra.trim()) - } else { - base.to_string() - } -} - +use super::utils::merge_class; static RADIO_GROUP_IDS: AtomicUsize = AtomicUsize::new(0); fn next_radio_group_name() -> String { diff --git a/src/components/ui/scroll_area.rs b/src/components/ui/scroll_area.rs index f57ddea..537b2c0 100644 --- a/src/components/ui/scroll_area.rs +++ b/src/components/ui/scroll_area.rs @@ -1,13 +1,5 @@ use dioxus::prelude::*; - -fn merge_class(base: &str, extra: Option) -> String { - if let Some(extra) = extra.filter(|extra| !extra.trim().is_empty()) { - format!("{base} {}", extra.trim()) - } else { - base.to_string() - } -} - +use super::utils::merge_class; #[component] pub fn ScrollArea( #[props(into, default)] class: Option, diff --git a/src/components/ui/separator.rs b/src/components/ui/separator.rs index 5968625..45360a7 100644 --- a/src/components/ui/separator.rs +++ b/src/components/ui/separator.rs @@ -1,5 +1,5 @@ use dioxus::prelude::*; - +use super::utils::merge_class; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum SeparatorOrientation { Horizontal, @@ -21,14 +21,6 @@ impl Default for SeparatorOrientation { } } -fn merge_class(base: &str, extra: Option) -> String { - if let Some(extra) = extra.filter(|extra| !extra.trim().is_empty()) { - format!("{base} {}", extra.trim()) - } else { - base.to_string() - } -} - #[component] pub fn Separator( #[props(default)] orientation: SeparatorOrientation, diff --git a/src/components/ui/sidebar.rs b/src/components/ui/sidebar.rs index baf8f6c..d48a472 100644 --- a/src/components/ui/sidebar.rs +++ b/src/components/ui/sidebar.rs @@ -1,21 +1,5 @@ use dioxus::prelude::*; - -fn merge_class(base: &str, extra: Option) -> String { - if let Some(extra) = extra.filter(|extra| !extra.trim().is_empty()) { - format!("{base} {}", extra.trim()) - } else { - base.to_string() - } -} - -fn data_bool(value: bool) -> &'static str { - if value { - "true" - } else { - "false" - } -} - +use super::utils::{merge_class, data_bool}; #[component] pub fn Sidebar( #[props(default)] collapsed: bool, diff --git a/src/components/ui/skeleton.rs b/src/components/ui/skeleton.rs index a98909a..c446ff0 100644 --- a/src/components/ui/skeleton.rs +++ b/src/components/ui/skeleton.rs @@ -1,13 +1,5 @@ use dioxus::prelude::*; - -fn merge_class(base: &str, extra: Option) -> String { - if let Some(extra) = extra.filter(|extra| !extra.trim().is_empty()) { - format!("{base} {}", extra.trim()) - } else { - base.to_string() - } -} - +use super::utils::merge_class; #[component] pub fn Skeleton( #[props(into, default)] class: Option, diff --git a/src/components/ui/slider.rs b/src/components/ui/slider.rs index 6769375..820718b 100644 --- a/src/components/ui/slider.rs +++ b/src/components/ui/slider.rs @@ -1,13 +1,5 @@ use dioxus::prelude::*; - -fn merge_class(base: &str, extra: Option) -> String { - if let Some(extra) = extra.filter(|extra| !extra.trim().is_empty()) { - format!("{base} {}", extra.trim()) - } else { - base.to_string() - } -} - +use super::utils::merge_class; #[component] pub fn Slider( #[props(default = 0.0f32)] value: f32, diff --git a/src/components/ui/switch.rs b/src/components/ui/switch.rs index 2c5a981..e19fed1 100644 --- a/src/components/ui/switch.rs +++ b/src/components/ui/switch.rs @@ -1,13 +1,5 @@ use dioxus::prelude::*; - -fn merge_class(base: &str, extra: Option) -> String { - if let Some(extra) = extra.filter(|extra| !extra.trim().is_empty()) { - format!("{base} {}", extra.trim()) - } else { - base.to_string() - } -} - +use super::utils::merge_class; #[component] pub fn Switch( #[props(default)] checked: bool, diff --git a/src/components/ui/table.rs b/src/components/ui/table.rs index fc1ced3..68ecd3d 100644 --- a/src/components/ui/table.rs +++ b/src/components/ui/table.rs @@ -6,15 +6,7 @@ use std::{ use crate::components::ui::Checkbox; use dioxus::prelude::*; - -fn merge_class(base: &str, extra: Option) -> String { - if let Some(extra) = extra.filter(|extra| !extra.trim().is_empty()) { - format!("{base} {}", extra.trim()) - } else { - base.to_string() - } -} - +use super::utils::merge_class; #[component] pub fn Table(#[props(into, default)] class: Option, children: Element) -> Element { let classes = merge_class("ui-table", class); diff --git a/src/components/ui/tabs.rs b/src/components/ui/tabs.rs index 5255f20..bfb3e65 100644 --- a/src/components/ui/tabs.rs +++ b/src/components/ui/tabs.rs @@ -1,6 +1,5 @@ use dioxus::prelude::*; - -#[allow(dead_code)] +use super::utils::merge_class; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum TabsOrientation { Horizontal, @@ -28,14 +27,6 @@ struct TabsContext { on_change: Option>, } -fn merge_class(base: &str, extra: Option) -> String { - if let Some(extra) = extra.filter(|extra| !extra.trim().is_empty()) { - format!("{base} {}", extra.trim()) - } else { - base.to_string() - } -} - #[component] pub fn Tabs( #[props(into)] default_value: String, diff --git a/src/components/ui/textarea.rs b/src/components/ui/textarea.rs index 0c11ce6..151ebbe 100644 --- a/src/components/ui/textarea.rs +++ b/src/components/ui/textarea.rs @@ -1,13 +1,5 @@ use dioxus::prelude::*; - -fn merge_class(base: &str, extra: Option) -> String { - if let Some(extra) = extra.filter(|extra| !extra.trim().is_empty()) { - format!("{base} {}", extra.trim()) - } else { - base.to_string() - } -} - +use super::utils::merge_class; #[component] pub fn Textarea( #[props(into, default)] class: Option, diff --git a/src/components/ui/toggle.rs b/src/components/ui/toggle.rs index dc46ead..2a79664 100644 --- a/src/components/ui/toggle.rs +++ b/src/components/ui/toggle.rs @@ -1,13 +1,5 @@ use dioxus::prelude::*; - -fn merge_class(base: &str, extra: Option) -> String { - if let Some(extra) = extra.filter(|extra| !extra.trim().is_empty()) { - format!("{base} {}", extra.trim()) - } else { - base.to_string() - } -} - +use super::utils::merge_class; #[component] pub fn Toggle( #[props(default)] pressed: bool, diff --git a/src/components/ui/toggle_group.rs b/src/components/ui/toggle_group.rs index 03d6847..ee38218 100644 --- a/src/components/ui/toggle_group.rs +++ b/src/components/ui/toggle_group.rs @@ -1,14 +1,6 @@ use crate::components::ui::Toggle; use dioxus::prelude::*; - -fn merge_class(base: &str, extra: Option) -> String { - if let Some(extra) = extra.filter(|extra| !extra.trim().is_empty()) { - format!("{base} {}", extra.trim()) - } else { - base.to_string() - } -} - +use super::utils::merge_class; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum ToggleGroupMode { Single, diff --git a/src/components/ui/utils.rs b/src/components/ui/utils.rs new file mode 100644 index 0000000..ac6274e --- /dev/null +++ b/src/components/ui/utils.rs @@ -0,0 +1,48 @@ +/// Merge a base CSS class with an optional extra class string. +/// If the extra string is empty or only whitespace, returns just the base class. +pub fn merge_class(base: &str, extra: Option) -> String { + if let Some(extra) = extra.filter(|extra| !extra.trim().is_empty()) { + format!("{base} {}", extra.trim()) + } else { + base.to_string() + } +} + +/// Convert a boolean to "true" or "false" string for data attributes. +pub fn data_bool(value: bool) -> &'static str { + if value { + "true" + } else { + "false" + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_merge_class_with_extra() { + assert_eq!( + merge_class("base", Some("extra".to_string())), + "base extra" + ); + } + + #[test] + fn test_merge_class_without_extra() { + assert_eq!(merge_class("base", None), "base"); + } + + #[test] + fn test_merge_class_with_empty_extra() { + assert_eq!(merge_class("base", Some("".to_string())), "base"); + assert_eq!(merge_class("base", Some(" ".to_string())), "base"); + } + + #[test] + fn test_data_bool() { + assert_eq!(data_bool(true), "true"); + assert_eq!(data_bool(false), "false"); + } +}