adui_dioxus/components/
descriptions.rs

1use crate::components::config_provider::{ComponentSize, use_config};
2use crate::theme::use_theme;
3use dioxus::prelude::*;
4
5/// Layout direction for descriptions.
6#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
7pub enum DescriptionsLayout {
8    #[default]
9    Horizontal,
10    Vertical,
11}
12
13impl DescriptionsLayout {
14    fn as_class(&self) -> &'static str {
15        match self {
16            DescriptionsLayout::Horizontal => "adui-descriptions-horizontal",
17            DescriptionsLayout::Vertical => "adui-descriptions-vertical",
18        }
19    }
20}
21
22/// Size variants for Descriptions.
23#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
24pub enum DescriptionsSize {
25    Small,
26    #[default]
27    Middle,
28    Large,
29}
30
31impl DescriptionsSize {
32    fn from_global(size: ComponentSize) -> Self {
33        match size {
34            ComponentSize::Small => DescriptionsSize::Small,
35            ComponentSize::Large => DescriptionsSize::Large,
36            ComponentSize::Middle => DescriptionsSize::Middle,
37        }
38    }
39
40    fn as_class(&self) -> &'static str {
41        match self {
42            DescriptionsSize::Small => "adui-descriptions-sm",
43            DescriptionsSize::Middle => "adui-descriptions-md",
44            DescriptionsSize::Large => "adui-descriptions-lg",
45        }
46    }
47}
48
49/// Responsive column configuration for different screen sizes.
50#[derive(Clone, Debug, PartialEq)]
51pub struct ResponsiveColumn {
52    /// Default column count.
53    pub default: usize,
54    /// Column count for xs screens (< 576px).
55    pub xs: Option<usize>,
56    /// Column count for sm screens (≥ 576px).
57    pub sm: Option<usize>,
58    /// Column count for md screens (≥ 768px).
59    pub md: Option<usize>,
60    /// Column count for lg screens (≥ 992px).
61    pub lg: Option<usize>,
62    /// Column count for xl screens (≥ 1200px).
63    pub xl: Option<usize>,
64    /// Column count for xxl screens (≥ 1600px).
65    pub xxl: Option<usize>,
66}
67
68impl ResponsiveColumn {
69    pub fn new(default: usize) -> Self {
70        Self {
71            default,
72            xs: None,
73            sm: None,
74            md: None,
75            lg: None,
76            xl: None,
77            xxl: None,
78        }
79    }
80}
81
82/// Column configuration (simple or responsive).
83#[derive(Clone, Debug, PartialEq)]
84pub enum ColumnConfig {
85    Simple(usize),
86    Responsive(ResponsiveColumn),
87}
88
89impl Default for ColumnConfig {
90    fn default() -> Self {
91        ColumnConfig::Simple(3)
92    }
93}
94
95impl ColumnConfig {
96    /// Get the effective column count (for now, just return default/simple value).
97    /// In a full implementation, this would check window width.
98    fn get_columns(&self) -> usize {
99        match self {
100            ColumnConfig::Simple(n) => *n,
101            ColumnConfig::Responsive(r) => r.default,
102        }
103    }
104}
105
106/// Data model for a single description item.
107#[derive(Clone, PartialEq)]
108pub struct DescriptionsItem {
109    pub key: String,
110    pub label: Element,
111    pub content: Element,
112    /// How many columns this item spans.
113    pub span: usize,
114}
115
116impl DescriptionsItem {
117    pub fn new(key: impl Into<String>, label: Element, content: Element) -> Self {
118        Self {
119            key: key.into(),
120            label,
121            content,
122            span: 1,
123        }
124    }
125
126    pub fn span(mut self, span: usize) -> Self {
127        self.span = span.max(1);
128        self
129    }
130}
131
132/// Props for the Descriptions component.
133#[derive(Props, Clone, PartialEq)]
134pub struct DescriptionsProps {
135    /// Description items to display.
136    pub items: Vec<DescriptionsItem>,
137    /// Optional title for the descriptions.
138    #[props(optional)]
139    pub title: Option<Element>,
140    /// Optional extra content in the header.
141    #[props(optional)]
142    pub extra: Option<Element>,
143    /// Whether to show border.
144    #[props(default)]
145    pub bordered: bool,
146    /// Layout direction.
147    #[props(default)]
148    pub layout: DescriptionsLayout,
149    /// Column configuration.
150    #[props(default)]
151    pub column: ColumnConfig,
152    /// Size variant.
153    #[props(optional)]
154    pub size: Option<DescriptionsSize>,
155    /// Whether to show colon after label.
156    #[props(default = true)]
157    pub colon: bool,
158    /// Extra class name.
159    #[props(optional)]
160    pub class: Option<String>,
161    /// Inline style.
162    #[props(optional)]
163    pub style: Option<String>,
164}
165
166/// Ant Design flavored Descriptions component.
167#[component]
168pub fn Descriptions(props: DescriptionsProps) -> Element {
169    let DescriptionsProps {
170        items,
171        title,
172        extra,
173        bordered,
174        layout,
175        column,
176        size,
177        colon,
178        class,
179        style,
180    } = props;
181
182    let config = use_config();
183    let theme = use_theme();
184    let tokens = theme.tokens();
185
186    // Resolve size
187    let resolved_size = if let Some(s) = size {
188        s
189    } else {
190        DescriptionsSize::from_global(config.size)
191    };
192
193    let columns = column.get_columns();
194
195    // Build root classes
196    let mut class_list = vec!["adui-descriptions".to_string()];
197    class_list.push(resolved_size.as_class().to_string());
198    class_list.push(layout.as_class().to_string());
199    if bordered {
200        class_list.push("adui-descriptions-bordered".into());
201    }
202    if let Some(extra) = class {
203        class_list.push(extra);
204    }
205    let class_attr = class_list.join(" ");
206
207    let style_attr = format!(
208        "border-color:{};{}",
209        tokens.color_border,
210        style.unwrap_or_default()
211    );
212
213    // Split items into rows based on column count and span
214    let mut rows: Vec<Vec<&DescriptionsItem>> = Vec::new();
215    let mut current_row: Vec<&DescriptionsItem> = Vec::new();
216    let mut current_span = 0;
217
218    for item in &items {
219        let item_span = item.span.min(columns);
220        if current_span + item_span > columns && !current_row.is_empty() {
221            rows.push(current_row);
222            current_row = Vec::new();
223            current_span = 0;
224        }
225        current_row.push(item);
226        current_span += item_span;
227        if current_span >= columns {
228            rows.push(current_row);
229            current_row = Vec::new();
230            current_span = 0;
231        }
232    }
233    if !current_row.is_empty() {
234        rows.push(current_row);
235    }
236
237    rsx! {
238        div {
239            class: "{class_attr}",
240            style: "{style_attr}",
241            {(title.is_some() || extra.is_some()).then(|| rsx! {
242                div { class: "adui-descriptions-header",
243                    {title.map(|t| rsx! {
244                        div { class: "adui-descriptions-title",
245                            {t}
246                        }
247                    })},
248                    {extra.map(|e| rsx! {
249                        div { class: "adui-descriptions-extra",
250                            {e}
251                        }
252                    })},
253                }
254            })},
255            div { class: "adui-descriptions-view",
256                {if bordered {
257                    rsx! {
258                        table { class: "adui-descriptions-table",
259                            tbody {
260                                {rows.iter().map(|row| {
261                                    if layout == DescriptionsLayout::Horizontal {
262                                        rsx! {
263                                            tr { class: "adui-descriptions-row",
264                                                {row.iter().map(|item| {
265                                                    let label = item.label.clone();
266                                                    let content = item.content.clone();
267                                                    let span = item.span;
268                                                    rsx! {
269                                                        th {
270                                                            class: "adui-descriptions-item-label",
271                                                            colspan: "{span}",
272                                                            {label}
273                                                            {colon.then(|| rsx! { span { class: "adui-descriptions-colon", ":" } })}
274                                                        },
275                                                        td {
276                                                            class: "adui-descriptions-item-content",
277                                                            colspan: "{span}",
278                                                            {content}
279                                                        }
280                                                    }
281                                                })}
282                                            }
283                                        }
284                                    } else {
285                                        rsx! {
286                                            {row.iter().map(|item| {
287                                                let label = item.label.clone();
288                                                let content = item.content.clone();
289                                                let span = item.span;
290                                                rsx! {
291                                                    tr { class: "adui-descriptions-row",
292                                                        th {
293                                                            class: "adui-descriptions-item-label",
294                                                            colspan: "{span * 2}",
295                                                            {label}
296                                                            {colon.then(|| rsx! { span { class: "adui-descriptions-colon", ":" } })}
297                                                        }
298                                                    },
299                                                    tr { class: "adui-descriptions-row",
300                                                        td {
301                                                            class: "adui-descriptions-item-content",
302                                                            colspan: "{span * 2}",
303                                                            {content}
304                                                        }
305                                                    }
306                                                }
307                                            })}
308                                        }
309                                    }
310                                })}
311                            }
312                        }
313                    }
314                } else {
315                    rsx! {
316                        div { class: "adui-descriptions-list",
317                            {rows.iter().map(|row| {
318                                rsx! {
319                                    div { class: "adui-descriptions-row",
320                                        {row.iter().map(|item| {
321                                            let label = item.label.clone();
322                                            let content = item.content.clone();
323                                            let span = item.span;
324                                            let width_percent = (span as f32 / columns as f32 * 100.0) as usize;
325                                            if layout == DescriptionsLayout::Horizontal {
326                                                rsx! {
327                                                    div {
328                                                        class: "adui-descriptions-item",
329                                                        style: "width: {width_percent}%",
330                                                        div {
331                                                            class: "adui-descriptions-item-label",
332                                                            {label}
333                                                            {colon.then(|| rsx! { span { class: "adui-descriptions-colon", ":" } })}
334                                                        },
335                                                        div { class: "adui-descriptions-item-content",
336                                                            {content}
337                                                        }
338                                                    }
339                                                }
340                                            } else {
341                                                rsx! {
342                                                    div {
343                                                        class: "adui-descriptions-item adui-descriptions-item-vertical",
344                                                        style: "width: {width_percent}%",
345                                                        div {
346                                                            class: "adui-descriptions-item-label",
347                                                            {label}
348                                                            {colon.then(|| rsx! { span { class: "adui-descriptions-colon", ":" } })}
349                                                        },
350                                                        div { class: "adui-descriptions-item-content",
351                                                            {content}
352                                                        }
353                                                    }
354                                                }
355                                            }
356                                        })}
357                                    }
358                                }
359                            })}
360                        }
361                    }
362                }}
363            }
364        }
365    }
366}
367
368#[cfg(test)]
369mod tests {
370    use super::*;
371
372    #[test]
373    fn descriptions_size_class_mapping_is_stable() {
374        assert_eq!(DescriptionsSize::Small.as_class(), "adui-descriptions-sm");
375        assert_eq!(DescriptionsSize::Middle.as_class(), "adui-descriptions-md");
376        assert_eq!(DescriptionsSize::Large.as_class(), "adui-descriptions-lg");
377    }
378
379    #[test]
380    fn descriptions_layout_class_mapping_is_stable() {
381        assert_eq!(
382            DescriptionsLayout::Horizontal.as_class(),
383            "adui-descriptions-horizontal"
384        );
385        assert_eq!(
386            DescriptionsLayout::Vertical.as_class(),
387            "adui-descriptions-vertical"
388        );
389    }
390
391    #[test]
392    fn column_config_returns_correct_count() {
393        let simple = ColumnConfig::Simple(4);
394        assert_eq!(simple.get_columns(), 4);
395
396        let responsive = ColumnConfig::Responsive(ResponsiveColumn::new(3));
397        assert_eq!(responsive.get_columns(), 3);
398    }
399}