1use crate::components::config_provider::{ComponentSize, use_config};
2use crate::theme::use_theme;
3use dioxus::prelude::*;
4
5#[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#[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#[derive(Clone, Debug, PartialEq)]
51pub struct ResponsiveColumn {
52 pub default: usize,
54 pub xs: Option<usize>,
56 pub sm: Option<usize>,
58 pub md: Option<usize>,
60 pub lg: Option<usize>,
62 pub xl: Option<usize>,
64 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#[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 fn get_columns(&self) -> usize {
99 match self {
100 ColumnConfig::Simple(n) => *n,
101 ColumnConfig::Responsive(r) => r.default,
102 }
103 }
104}
105
106#[derive(Clone, PartialEq)]
108pub struct DescriptionsItem {
109 pub key: String,
110 pub label: Element,
111 pub content: Element,
112 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#[derive(Props, Clone, PartialEq)]
134pub struct DescriptionsProps {
135 pub items: Vec<DescriptionsItem>,
137 #[props(optional)]
139 pub title: Option<Element>,
140 #[props(optional)]
142 pub extra: Option<Element>,
143 #[props(default)]
145 pub bordered: bool,
146 #[props(default)]
148 pub layout: DescriptionsLayout,
149 #[props(default)]
151 pub column: ColumnConfig,
152 #[props(optional)]
154 pub size: Option<DescriptionsSize>,
155 #[props(default = true)]
157 pub colon: bool,
158 #[props(optional)]
160 pub class: Option<String>,
161 #[props(optional)]
163 pub style: Option<String>,
164}
165
166#[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 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 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 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}