1use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8
9use crate::action::Action;
10
11#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
13#[serde(rename_all = "snake_case")]
14pub enum Size {
15 Xs,
16 Sm,
17 #[default]
18 Default,
19 Lg,
20}
21
22#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
24#[serde(rename_all = "snake_case")]
25pub enum IconPosition {
26 #[default]
27 Left,
28 Right,
29}
30
31#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
33#[serde(rename_all = "snake_case")]
34pub enum SortDirection {
35 #[default]
36 Asc,
37 Desc,
38}
39
40#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
42#[serde(rename_all = "snake_case")]
43pub enum Orientation {
44 #[default]
45 Horizontal,
46 Vertical,
47}
48
49#[derive(
51 Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, strum::AsRefStr,
52)]
53#[serde(rename_all = "snake_case")]
54#[strum(serialize_all = "snake_case")]
55pub enum ButtonVariant {
56 #[default]
57 Default,
58 Secondary,
59 Destructive,
60 Outline,
61 Ghost,
62 Link,
63}
64
65#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
67#[serde(rename_all = "snake_case")]
68pub enum InputType {
69 #[default]
70 Text,
71 Email,
72 Password,
73 Number,
74 Textarea,
75 Hidden,
76 Date,
77 Time,
78 Url,
79 Tel,
80 Search,
81 File,
82}
83
84#[derive(
86 Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, strum::AsRefStr,
87)]
88#[serde(rename_all = "snake_case")]
89#[strum(serialize_all = "snake_case")]
90pub enum AlertVariant {
91 #[default]
92 Info,
93 Success,
94 Warning,
95 Error,
96}
97
98#[derive(
100 Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, strum::AsRefStr,
101)]
102#[serde(rename_all = "snake_case")]
103#[strum(serialize_all = "snake_case")]
104pub enum BadgeVariant {
105 #[default]
106 Default,
107 Secondary,
108 Destructive,
109 Outline,
110}
111
112#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
114#[serde(rename_all = "snake_case")]
115pub enum TextElement {
116 #[default]
117 P,
118 H1,
119 H2,
120 H3,
121 Span,
122 Div,
123 Section,
124}
125
126#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
131#[serde(rename_all = "snake_case")]
132pub enum ColumnFormat {
133 Date,
134 DateTime,
135 Currency,
136 Boolean,
137 Badge,
138}
139
140#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
142pub struct Column {
143 pub key: String,
144 pub label: String,
145 #[serde(default, skip_serializing_if = "Option::is_none")]
146 pub format: Option<ColumnFormat>,
147}
148
149#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
151pub struct SelectOption {
152 pub value: String,
153 pub label: String,
154}
155
156#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
163#[serde(rename_all = "snake_case")]
164pub enum CardVariant {
165 #[default]
166 Bordered,
167 Elevated,
168}
169
170#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
172pub struct CardProps {
173 pub title: String,
174 #[serde(default, skip_serializing_if = "Option::is_none")]
175 pub description: Option<String>,
176 #[serde(default, skip_serializing_if = "Option::is_none")]
180 pub subtitle: Option<String>,
181 #[serde(default, skip_serializing_if = "Option::is_none")]
185 pub badge: Option<String>,
186 #[serde(default, skip_serializing_if = "Option::is_none")]
187 pub max_width: Option<FormMaxWidth>,
188 #[serde(default, skip_serializing_if = "Vec::is_empty")]
190 pub footer: Vec<String>,
191 #[serde(default)]
192 pub variant: CardVariant,
193}
194
195#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
197pub struct TableProps {
198 pub columns: Vec<Column>,
199 pub data_path: String,
200 #[serde(default, skip_serializing_if = "Option::is_none")]
201 pub row_actions: Option<Vec<Action>>,
202 #[serde(default, skip_serializing_if = "Option::is_none")]
203 pub empty_message: Option<String>,
204 #[serde(default, skip_serializing_if = "Option::is_none")]
205 pub sortable: Option<bool>,
206 #[serde(default, skip_serializing_if = "Option::is_none")]
207 pub sort_column: Option<String>,
208 #[serde(default, skip_serializing_if = "Option::is_none")]
209 pub sort_direction: Option<SortDirection>,
210}
211
212#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
214#[serde(rename_all = "snake_case")]
215pub enum FormMaxWidth {
216 #[default]
217 Default,
218 Narrow,
219 Wide,
220}
221
222#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
224pub struct FormProps {
225 pub action: Action,
226 #[serde(default, skip_serializing_if = "Option::is_none")]
227 pub method: Option<crate::action::HttpMethod>,
228 #[serde(default, skip_serializing_if = "Option::is_none")]
232 pub guard: Option<String>,
233 #[serde(default, skip_serializing_if = "Option::is_none")]
235 pub max_width: Option<FormMaxWidth>,
236 #[serde(default, skip_serializing_if = "Option::is_none")]
240 pub id: Option<String>,
241 #[serde(default, skip_serializing_if = "Option::is_none")]
246 pub enctype: Option<String>,
247}
248
249#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
251#[serde(rename_all = "snake_case")]
252pub enum ButtonType {
253 #[default]
254 Button,
255 Submit,
256}
257
258#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
260pub struct ButtonProps {
261 pub label: String,
262 #[serde(default)]
263 pub variant: ButtonVariant,
264 #[serde(default)]
265 pub size: Size,
266 #[serde(default, skip_serializing_if = "Option::is_none")]
267 pub disabled: Option<bool>,
268 #[serde(default, skip_serializing_if = "Option::is_none")]
269 pub icon: Option<String>,
270 #[serde(default, skip_serializing_if = "Option::is_none")]
271 pub icon_position: Option<IconPosition>,
272 #[serde(default, skip_serializing_if = "Option::is_none")]
273 pub button_type: Option<ButtonType>,
274 #[serde(default, skip_serializing_if = "Option::is_none")]
278 pub form: Option<String>,
279}
280
281#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
283pub struct InputProps {
284 pub field: String,
286 pub label: String,
287 #[serde(default)]
288 pub input_type: InputType,
289 #[serde(default, skip_serializing_if = "Option::is_none")]
290 pub placeholder: Option<String>,
291 #[serde(default, skip_serializing_if = "Option::is_none")]
292 pub required: Option<bool>,
293 #[serde(default, skip_serializing_if = "Option::is_none")]
294 pub disabled: Option<bool>,
295 #[serde(default, skip_serializing_if = "Option::is_none")]
296 pub error: Option<String>,
297 #[serde(default, skip_serializing_if = "Option::is_none")]
298 pub description: Option<String>,
299 #[serde(default, skip_serializing_if = "Option::is_none")]
300 pub default_value: Option<String>,
301 #[serde(default, skip_serializing_if = "Option::is_none")]
303 pub data_path: Option<String>,
304 #[serde(default, skip_serializing_if = "Option::is_none")]
306 pub step: Option<String>,
307 #[serde(default, skip_serializing_if = "Option::is_none")]
311 pub list: Option<String>,
312 #[serde(default, skip_serializing_if = "Option::is_none")]
317 pub accept: Option<String>,
318}
319
320#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
331pub struct RichTextEditorProps {
332 pub field: String,
333 pub label: String,
334 #[serde(default, skip_serializing_if = "Option::is_none")]
335 pub placeholder: Option<String>,
336 #[serde(default, skip_serializing_if = "Option::is_none")]
337 pub default_value: Option<String>,
338 #[serde(default, skip_serializing_if = "Option::is_none")]
339 pub data_path: Option<String>,
340 #[serde(default, skip_serializing_if = "Option::is_none")]
341 pub error: Option<String>,
342}
343
344#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
346pub struct SelectProps {
347 pub field: String,
349 pub label: String,
350 pub options: Vec<SelectOption>,
351 #[serde(default, skip_serializing_if = "Option::is_none")]
352 pub placeholder: Option<String>,
353 #[serde(default, skip_serializing_if = "Option::is_none")]
354 pub required: Option<bool>,
355 #[serde(default, skip_serializing_if = "Option::is_none")]
356 pub disabled: Option<bool>,
357 #[serde(default, skip_serializing_if = "Option::is_none")]
358 pub error: Option<String>,
359 #[serde(default, skip_serializing_if = "Option::is_none")]
360 pub description: Option<String>,
361 #[serde(default, skip_serializing_if = "Option::is_none")]
362 pub default_value: Option<String>,
363 #[serde(default, skip_serializing_if = "Option::is_none")]
365 pub data_path: Option<String>,
366}
367
368#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
370pub struct AlertProps {
371 pub message: String,
372 #[serde(default)]
373 pub variant: AlertVariant,
374 #[serde(default, skip_serializing_if = "Option::is_none")]
375 pub title: Option<String>,
376}
377
378#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
380pub struct BadgeProps {
381 pub label: String,
382 #[serde(default)]
383 pub variant: BadgeVariant,
384}
385
386#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
388pub struct ModalProps {
389 pub id: String,
390 pub title: String,
391 #[serde(default, skip_serializing_if = "Option::is_none")]
392 pub description: Option<String>,
393 #[serde(default, skip_serializing_if = "Option::is_none")]
394 pub trigger_label: Option<String>,
395 #[serde(default, skip_serializing_if = "Vec::is_empty")]
397 pub footer: Vec<String>,
398}
399
400#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
402pub struct TextProps {
403 pub content: String,
404 #[serde(default)]
405 pub element: TextElement,
406}
407
408#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
410pub struct CheckboxProps {
411 pub field: String,
413 #[serde(default, skip_serializing_if = "Option::is_none")]
416 pub value: Option<String>,
417 pub label: String,
418 #[serde(default, skip_serializing_if = "Option::is_none")]
419 pub description: Option<String>,
420 #[serde(default, skip_serializing_if = "Option::is_none")]
421 pub checked: Option<bool>,
422 #[serde(default, skip_serializing_if = "Option::is_none")]
424 pub data_path: Option<String>,
425 #[serde(default, skip_serializing_if = "Option::is_none")]
426 pub required: Option<bool>,
427 #[serde(default, skip_serializing_if = "Option::is_none")]
428 pub disabled: Option<bool>,
429 #[serde(default, skip_serializing_if = "Option::is_none")]
430 pub error: Option<String>,
431}
432
433#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
439pub struct CheckboxListProps {
440 pub field: String,
442 #[serde(default, skip_serializing_if = "Vec::is_empty")]
445 pub options: Vec<SelectOption>,
446 #[serde(default, skip_serializing_if = "Option::is_none")]
448 pub options_path: Option<String>,
449 #[serde(default, skip_serializing_if = "Option::is_none")]
451 pub selected_path: Option<String>,
452 #[serde(default, skip_serializing_if = "Option::is_none")]
453 pub label: Option<String>,
454 #[serde(default, skip_serializing_if = "Option::is_none")]
455 pub description: Option<String>,
456 #[serde(default, skip_serializing_if = "Option::is_none")]
457 pub disabled: Option<bool>,
458 #[serde(default, skip_serializing_if = "Option::is_none")]
459 pub error: Option<String>,
460}
461
462#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
464pub struct SwitchProps {
465 pub field: String,
467 pub label: String,
468 #[serde(default, skip_serializing_if = "Option::is_none")]
469 pub description: Option<String>,
470 #[serde(default, skip_serializing_if = "Option::is_none")]
471 pub checked: Option<bool>,
472 #[serde(default, skip_serializing_if = "Option::is_none")]
474 pub data_path: Option<String>,
475 #[serde(default, skip_serializing_if = "Option::is_none")]
476 pub required: Option<bool>,
477 #[serde(default, skip_serializing_if = "Option::is_none")]
478 pub disabled: Option<bool>,
479 #[serde(default, skip_serializing_if = "Option::is_none")]
480 pub error: Option<String>,
481 #[serde(default, skip_serializing_if = "Option::is_none")]
484 pub action: Option<Action>,
485 #[serde(default, skip_serializing_if = "Option::is_none")]
488 pub compact: Option<bool>,
489}
490
491#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
493pub struct SeparatorProps {
494 #[serde(default, skip_serializing_if = "Option::is_none")]
495 pub orientation: Option<Orientation>,
496}
497
498#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
500pub struct DescriptionItem {
501 pub label: String,
502 pub value: String,
503 #[serde(default, skip_serializing_if = "Option::is_none")]
504 pub format: Option<ColumnFormat>,
505}
506
507#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
509pub struct DescriptionListProps {
510 #[serde(default, skip_serializing_if = "Vec::is_empty")]
511 pub items: Vec<DescriptionItem>,
512 #[serde(default, skip_serializing_if = "Option::is_none")]
513 pub columns: Option<u8>,
514 #[serde(default, skip_serializing_if = "Option::is_none")]
518 pub data_path: Option<String>,
519}
520
521#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
523pub struct Tab {
524 pub value: String,
525 pub label: String,
526 #[serde(default, skip_serializing_if = "Vec::is_empty")]
528 pub children: Vec<String>,
529}
530
531#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
533pub struct TabsProps {
534 pub default_tab: String,
535 pub tabs: Vec<Tab>,
536}
537
538#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
540pub struct BreadcrumbItem {
541 pub label: String,
542 #[serde(default, skip_serializing_if = "Option::is_none")]
543 pub url: Option<String>,
544}
545
546#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
548pub struct BreadcrumbProps {
549 pub items: Vec<BreadcrumbItem>,
550}
551
552#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
554pub struct PaginationProps {
555 pub current_page: u32,
556 pub per_page: u32,
557 pub total: u32,
558 #[serde(default, skip_serializing_if = "Option::is_none")]
559 pub base_url: Option<String>,
560}
561
562#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
564pub struct ProgressProps {
565 pub value: u8,
567 #[serde(default, skip_serializing_if = "Option::is_none")]
568 pub max: Option<u8>,
569 #[serde(default, skip_serializing_if = "Option::is_none")]
570 pub label: Option<String>,
571}
572
573#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
575pub struct ImageProps {
576 #[serde(default)]
577 pub src: String,
578 pub alt: String,
579 #[serde(default, skip_serializing_if = "Option::is_none")]
580 pub aspect_ratio: Option<String>,
581 #[serde(default, skip_serializing_if = "Option::is_none")]
586 pub placeholder_label: Option<String>,
587 #[serde(default, skip_serializing_if = "Option::is_none")]
595 pub inline_svg: Option<String>,
596 #[serde(default, skip_serializing_if = "Option::is_none")]
600 pub data_path: Option<String>,
601}
602
603impl ImageProps {
604 pub fn inline_svg(svg: impl Into<String>, alt: impl Into<String>) -> Self {
610 Self {
611 src: String::new(),
612 alt: alt.into(),
613 aspect_ratio: None,
614 placeholder_label: None,
615 inline_svg: Some(svg.into()),
616 data_path: None,
617 }
618 }
619}
620
621#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
623pub struct AvatarProps {
624 #[serde(default, skip_serializing_if = "Option::is_none")]
625 pub src: Option<String>,
626 pub alt: String,
627 #[serde(default, skip_serializing_if = "Option::is_none")]
628 pub fallback: Option<String>,
629 #[serde(default, skip_serializing_if = "Option::is_none")]
630 pub size: Option<Size>,
631}
632
633#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
635pub struct SkeletonProps {
636 #[serde(default, skip_serializing_if = "Option::is_none")]
637 pub width: Option<String>,
638 #[serde(default, skip_serializing_if = "Option::is_none")]
639 pub height: Option<String>,
640 #[serde(default, skip_serializing_if = "Option::is_none")]
641 pub rounded: Option<bool>,
642}
643
644#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
661pub struct RawHtmlProps {
662 #[serde(default)]
664 pub html: String,
665}
666
667#[derive(
669 Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, strum::AsRefStr,
670)]
671#[serde(rename_all = "snake_case")]
672#[strum(serialize_all = "snake_case")]
673pub enum ToastVariant {
674 #[default]
675 Info,
676 Success,
677 Warning,
678 Error,
679}
680
681#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
683pub struct ChecklistItem {
684 pub label: String,
685 #[serde(default)]
686 pub checked: bool,
687 #[serde(default, skip_serializing_if = "Option::is_none")]
688 pub href: Option<String>,
689}
690
691#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
693pub struct NotificationItem {
694 #[serde(default, skip_serializing_if = "Option::is_none")]
695 pub icon: Option<String>,
696 pub text: String,
697 #[serde(default, skip_serializing_if = "Option::is_none")]
698 pub timestamp: Option<String>,
699 #[serde(default)]
700 pub read: bool,
701 #[serde(default, skip_serializing_if = "Option::is_none")]
702 pub action_url: Option<String>,
703}
704
705#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
707pub struct SidebarNavItem {
708 pub label: String,
709 pub href: String,
710 #[serde(default, skip_serializing_if = "Option::is_none")]
711 pub icon: Option<String>,
712 #[serde(default)]
713 pub active: bool,
714 #[serde(default, skip_serializing_if = "Option::is_none")]
717 pub disabled: Option<bool>,
718}
719
720#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
722pub struct SidebarGroup {
723 pub label: String,
724 #[serde(default)]
725 pub collapsed: bool,
726 pub items: Vec<SidebarNavItem>,
727}
728
729#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
731pub struct StatCardProps {
732 pub label: String,
733 pub value: String,
734 #[serde(default, skip_serializing_if = "Option::is_none")]
735 pub icon: Option<String>,
736 #[serde(default, skip_serializing_if = "Option::is_none")]
737 pub subtitle: Option<String>,
738 #[serde(default, skip_serializing_if = "Option::is_none")]
740 pub sse_target: Option<String>,
741}
742
743#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
745pub struct ChecklistProps {
746 pub title: String,
747 pub items: Vec<ChecklistItem>,
748 #[serde(default = "default_true")]
749 pub dismissible: bool,
750 #[serde(default, skip_serializing_if = "Option::is_none")]
751 pub dismiss_label: Option<String>,
752 #[serde(default, skip_serializing_if = "Option::is_none")]
754 pub data_key: Option<String>,
755}
756
757fn default_true() -> bool {
758 true
759}
760
761#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
766pub struct ToastProps {
767 pub message: String,
768 #[serde(default)]
769 pub variant: ToastVariant,
770 #[serde(default, skip_serializing_if = "Option::is_none")]
772 pub timeout: Option<u32>,
773 #[serde(default = "default_true")]
774 pub dismissible: bool,
775}
776
777#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
779pub struct NotificationDropdownProps {
780 pub notifications: Vec<NotificationItem>,
781 #[serde(default, skip_serializing_if = "Option::is_none")]
782 pub empty_text: Option<String>,
783}
784
785#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
787pub struct SidebarProps {
788 #[serde(default, skip_serializing_if = "Vec::is_empty")]
789 pub fixed_top: Vec<SidebarNavItem>,
790 #[serde(default, skip_serializing_if = "Vec::is_empty")]
791 pub groups: Vec<SidebarGroup>,
792 #[serde(default, skip_serializing_if = "Vec::is_empty")]
793 pub fixed_bottom: Vec<SidebarNavItem>,
794}
795
796#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
798pub struct HeaderProps {
799 pub business_name: String,
800 #[serde(default, skip_serializing_if = "Option::is_none")]
802 pub notification_count: Option<u32>,
803 #[serde(default, skip_serializing_if = "Option::is_none")]
804 pub user_name: Option<String>,
805 #[serde(default, skip_serializing_if = "Option::is_none")]
806 pub user_avatar: Option<String>,
807 #[serde(default, skip_serializing_if = "Option::is_none")]
808 pub logout_url: Option<String>,
809}
810
811#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
813#[serde(rename_all = "snake_case")]
814pub enum GapSize {
815 None,
816 Sm,
817 #[default]
818 Md,
819 Lg,
820 Xl,
821}
822
823#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
825pub struct GridProps {
826 #[serde(default = "default_grid_columns")]
828 pub columns: u8,
829 #[serde(default, skip_serializing_if = "Option::is_none")]
831 pub md_columns: Option<u8>,
832 #[serde(default, skip_serializing_if = "Option::is_none")]
834 pub lg_columns: Option<u8>,
835 #[serde(default)]
837 pub gap: GapSize,
838 #[serde(default, skip_serializing_if = "Option::is_none")]
841 pub scrollable: Option<bool>,
842}
843
844fn default_grid_columns() -> u8 {
845 2
846}
847
848#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
850pub struct CollapsibleProps {
851 pub title: String,
852 #[serde(default)]
853 pub expanded: bool,
854}
855
856#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
858pub struct EmptyStateProps {
859 pub title: String,
860 #[serde(default, skip_serializing_if = "Option::is_none")]
861 pub description: Option<String>,
862 #[serde(default, skip_serializing_if = "Option::is_none")]
863 pub action: Option<Action>,
864 #[serde(default, skip_serializing_if = "Option::is_none")]
865 pub action_label: Option<String>,
866}
867
868#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
870#[serde(rename_all = "snake_case")]
871pub enum FormSectionLayout {
872 #[default]
873 Stacked,
874 TwoColumn,
875}
876
877#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
879pub struct FormSectionProps {
880 pub title: String,
881 #[serde(default, skip_serializing_if = "Option::is_none")]
882 pub description: Option<String>,
883 #[serde(default, skip_serializing_if = "Option::is_none")]
885 pub layout: Option<FormSectionLayout>,
886}
887
888#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
890pub struct PageHeaderProps {
891 pub title: String,
892 #[serde(default, skip_serializing_if = "Vec::is_empty")]
893 pub breadcrumb: Vec<BreadcrumbItem>,
894 #[serde(
896 default,
897 deserialize_with = "deserialize_actions_lax",
898 skip_serializing_if = "Vec::is_empty"
899 )]
900 pub actions: Vec<String>,
901}
902
903#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Default)]
905pub struct ButtonGroupProps {
906 #[serde(default)]
908 pub gap: GapSize,
909}
910
911#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
920pub struct DetailPageProps {
921 pub title: String,
922 #[serde(default, skip_serializing_if = "Vec::is_empty")]
923 pub breadcrumb: Vec<BreadcrumbItem>,
924 #[serde(
926 default,
927 deserialize_with = "deserialize_actions_lax",
928 skip_serializing_if = "Vec::is_empty"
929 )]
930 pub actions: Vec<String>,
931 #[serde(default, skip_serializing_if = "Vec::is_empty")]
934 pub info: Vec<String>,
935}
936
937#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
939pub struct DropdownMenuAction {
940 pub label: String,
941 pub action: Action,
942 #[serde(default)]
943 pub destructive: bool,
944 #[serde(default, skip_serializing_if = "Option::is_none")]
951 pub visible_if: Option<String>,
952}
953
954#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
956pub struct DropdownMenuProps {
957 pub menu_id: String,
958 pub trigger_label: String,
959 pub items: Vec<DropdownMenuAction>,
960 #[serde(default, skip_serializing_if = "Option::is_none")]
961 pub trigger_variant: Option<ButtonVariant>,
962}
963
964#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
967pub struct DataTableProps {
968 pub columns: Vec<Column>,
969 pub data_path: String,
970 #[serde(default, skip_serializing_if = "Option::is_none")]
971 pub row_actions: Option<Vec<DropdownMenuAction>>,
972 #[serde(default, skip_serializing_if = "Option::is_none")]
973 pub empty_message: Option<String>,
974 #[serde(default, skip_serializing_if = "Option::is_none")]
975 pub row_key: Option<String>,
976 #[serde(default, skip_serializing_if = "Option::is_none")]
978 pub row_href: Option<String>,
979}
980
981#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
985pub struct MediaCardGridProps {
986 pub data_path: String,
987 pub title_key: String,
989 #[serde(default, skip_serializing_if = "Option::is_none")]
991 pub description_key: Option<String>,
992 #[serde(default, skip_serializing_if = "Option::is_none")]
994 pub image_key: Option<String>,
995 #[serde(default, skip_serializing_if = "Option::is_none")]
997 pub image_href_key: Option<String>,
998 #[serde(default, skip_serializing_if = "Option::is_none")]
1000 pub image_aspect_ratio: Option<String>,
1001 #[serde(default, skip_serializing_if = "Option::is_none")]
1004 pub image_position: Option<String>,
1005 #[serde(default, skip_serializing_if = "Option::is_none")]
1007 pub badge_key: Option<String>,
1008 #[serde(default, skip_serializing_if = "Option::is_none")]
1010 pub badge_variant_key: Option<String>,
1011 #[serde(default, skip_serializing_if = "Option::is_none")]
1013 pub row_key: Option<String>,
1014 #[serde(default, skip_serializing_if = "Option::is_none")]
1015 pub row_actions: Option<Vec<DropdownMenuAction>>,
1016 #[serde(default, skip_serializing_if = "Option::is_none")]
1017 pub empty_message: Option<String>,
1018 #[serde(default, skip_serializing_if = "Option::is_none")]
1020 pub columns: Option<u8>,
1021}
1022
1023#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1025pub struct KanbanColumnProps {
1026 pub id: String,
1027 pub title: String,
1028 pub count: u32,
1029 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1031 pub children: Vec<String>,
1032}
1033
1034#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1036pub struct KanbanBoardProps {
1037 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1039 pub columns: Vec<KanbanColumnProps>,
1040 #[serde(default, skip_serializing_if = "Option::is_none")]
1045 pub data_path: Option<String>,
1046 #[serde(default, skip_serializing_if = "Option::is_none")]
1047 pub mobile_default_column: Option<String>,
1048 #[serde(default, skip_serializing_if = "Option::is_none")]
1052 pub empty_label: Option<String>,
1053}
1054
1055#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1060pub struct CalendarCellProps {
1061 pub day: u8,
1062 #[serde(default)]
1063 pub is_today: bool,
1064 #[serde(default)]
1065 pub is_current_month: bool,
1066 #[serde(default)]
1067 pub event_count: u32,
1068 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1071 pub dot_colors: Vec<String>,
1072}
1073
1074#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1076#[serde(rename_all = "snake_case")]
1077pub enum ActionCardVariant {
1078 #[default]
1079 Default,
1080 Setup,
1081 Danger,
1082}
1083
1084#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1089pub struct ActionCardProps {
1090 pub title: String,
1091 pub description: String,
1092 #[serde(default, skip_serializing_if = "Option::is_none")]
1093 pub icon: Option<String>,
1094 #[serde(default)]
1095 pub variant: ActionCardVariant,
1096 #[serde(default, skip_serializing_if = "Option::is_none")]
1098 pub href: Option<String>,
1099}
1100
1101#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1106pub struct ProductTileProps {
1107 pub product_id: String,
1108 pub name: String,
1109 pub price: String,
1110 pub field: String,
1111 #[serde(default, skip_serializing_if = "Option::is_none")]
1112 pub default_quantity: Option<u32>,
1113}
1114
1115fn deserialize_actions_lax<'de, D: serde::Deserializer<'de>>(
1121 d: D,
1122) -> Result<Vec<String>, D::Error> {
1123 use serde::de::Error;
1124 let v = serde_json::Value::deserialize(d)?;
1125 match v {
1126 serde_json::Value::Null => Ok(Vec::new()),
1127 serde_json::Value::String(s) if s.is_empty() => Ok(Vec::new()),
1128 serde_json::Value::Array(arr) => arr
1129 .into_iter()
1130 .map(|item| {
1131 item.as_str()
1132 .map(String::from)
1133 .ok_or_else(|| D::Error::custom("PageHeader.actions: expected string in array"))
1134 })
1135 .collect(),
1136 other => Err(D::Error::custom(format!(
1137 "PageHeader.actions: expected null, empty string, or array of strings; got {other:?}"
1138 ))),
1139 }
1140}
1141
1142#[cfg(test)]
1143mod schema_smoke_tests {
1144 use super::*;
1155
1156 fn assert_schema_nonempty_object<T: schemars::JsonSchema>(type_label: &str) {
1157 let schema = schemars::schema_for!(T);
1158 let value = serde_json::to_value(&schema).expect("schema serializes to JSON");
1159 assert!(
1160 value.is_object(),
1161 "{type_label}: schema must be a JSON object"
1162 );
1163 let props = value
1164 .get("properties")
1165 .and_then(|p| p.as_object())
1166 .map(|o| !o.is_empty())
1167 .unwrap_or(false);
1168 assert!(
1169 props,
1170 "{type_label}: schema must have a non-empty `properties` field"
1171 );
1172 }
1173
1174 #[test]
1175 fn schema_for_card_props_generates() {
1176 assert_schema_nonempty_object::<CardProps>("CardProps");
1177 }
1178
1179 #[test]
1180 fn schema_for_table_props_generates() {
1181 assert_schema_nonempty_object::<TableProps>("TableProps");
1182 }
1183
1184 #[test]
1185 fn schema_for_form_props_generates() {
1186 assert_schema_nonempty_object::<FormProps>("FormProps");
1187 }
1188
1189 #[test]
1190 fn schema_for_button_props_generates() {
1191 assert_schema_nonempty_object::<ButtonProps>("ButtonProps");
1192 }
1193
1194 #[test]
1195 fn schema_for_input_props_generates() {
1196 assert_schema_nonempty_object::<InputProps>("InputProps");
1197 }
1198
1199 #[test]
1200 fn schema_for_select_props_generates() {
1201 assert_schema_nonempty_object::<SelectProps>("SelectProps");
1202 }
1203
1204 #[test]
1205 fn schema_for_alert_props_generates() {
1206 assert_schema_nonempty_object::<AlertProps>("AlertProps");
1207 }
1208
1209 #[test]
1210 fn schema_for_badge_props_generates() {
1211 assert_schema_nonempty_object::<BadgeProps>("BadgeProps");
1212 }
1213
1214 #[test]
1215 fn schema_for_modal_props_generates() {
1216 assert_schema_nonempty_object::<ModalProps>("ModalProps");
1217 }
1218
1219 #[test]
1220 fn schema_for_text_props_generates() {
1221 assert_schema_nonempty_object::<TextProps>("TextProps");
1222 }
1223
1224 #[test]
1225 fn schema_for_checkbox_props_generates() {
1226 assert_schema_nonempty_object::<CheckboxProps>("CheckboxProps");
1227 }
1228
1229 #[test]
1230 fn schema_for_switch_props_generates() {
1231 assert_schema_nonempty_object::<SwitchProps>("SwitchProps");
1232 }
1233
1234 #[test]
1235 fn schema_for_separator_props_generates() {
1236 assert_schema_nonempty_object::<SeparatorProps>("SeparatorProps");
1237 }
1238
1239 #[test]
1240 fn schema_for_description_list_props_generates() {
1241 assert_schema_nonempty_object::<DescriptionListProps>("DescriptionListProps");
1242 }
1243
1244 #[test]
1245 fn schema_for_tab_generates() {
1246 assert_schema_nonempty_object::<Tab>("Tab");
1247 }
1248
1249 #[test]
1250 fn schema_for_tabs_props_generates() {
1251 assert_schema_nonempty_object::<TabsProps>("TabsProps");
1252 }
1253
1254 #[test]
1255 fn schema_for_breadcrumb_props_generates() {
1256 assert_schema_nonempty_object::<BreadcrumbProps>("BreadcrumbProps");
1257 }
1258
1259 #[test]
1260 fn schema_for_pagination_props_generates() {
1261 assert_schema_nonempty_object::<PaginationProps>("PaginationProps");
1262 }
1263
1264 #[test]
1265 fn schema_for_progress_props_generates() {
1266 assert_schema_nonempty_object::<ProgressProps>("ProgressProps");
1267 }
1268
1269 #[test]
1270 fn schema_for_image_props_generates() {
1271 assert_schema_nonempty_object::<ImageProps>("ImageProps");
1272 }
1273
1274 #[test]
1275 fn image_inline_svg_factory_roundtrips_via_serde() {
1276 let p = ImageProps::inline_svg("<svg/>", "alt");
1277 let json = serde_json::to_value(&p).expect("serialization must not fail");
1278 let parsed: ImageProps =
1279 serde_json::from_value(json).expect("deserialization must not fail");
1280 assert_eq!(parsed.inline_svg, Some("<svg/>".to_string()));
1281 assert_eq!(parsed.alt, "alt");
1282 assert_eq!(parsed.src, "");
1283 }
1284
1285 #[test]
1286 fn schema_for_avatar_props_generates() {
1287 assert_schema_nonempty_object::<AvatarProps>("AvatarProps");
1288 }
1289
1290 #[test]
1291 fn schema_for_skeleton_props_generates() {
1292 assert_schema_nonempty_object::<SkeletonProps>("SkeletonProps");
1293 }
1294
1295 #[test]
1296 fn schema_for_stat_card_props_generates() {
1297 assert_schema_nonempty_object::<StatCardProps>("StatCardProps");
1298 }
1299
1300 #[test]
1301 fn schema_for_checklist_props_generates() {
1302 assert_schema_nonempty_object::<ChecklistProps>("ChecklistProps");
1303 }
1304
1305 #[test]
1306 fn schema_for_toast_props_generates() {
1307 assert_schema_nonempty_object::<ToastProps>("ToastProps");
1308 }
1309
1310 #[test]
1311 fn schema_for_notification_dropdown_props_generates() {
1312 assert_schema_nonempty_object::<NotificationDropdownProps>("NotificationDropdownProps");
1313 }
1314
1315 #[test]
1316 fn schema_for_sidebar_props_generates() {
1317 assert_schema_nonempty_object::<SidebarProps>("SidebarProps");
1318 }
1319
1320 #[test]
1321 fn schema_for_header_props_generates() {
1322 assert_schema_nonempty_object::<HeaderProps>("HeaderProps");
1323 }
1324
1325 #[test]
1326 fn schema_for_grid_props_generates() {
1327 assert_schema_nonempty_object::<GridProps>("GridProps");
1328 }
1329
1330 #[test]
1331 fn schema_for_collapsible_props_generates() {
1332 assert_schema_nonempty_object::<CollapsibleProps>("CollapsibleProps");
1333 }
1334
1335 #[test]
1336 fn schema_for_empty_state_props_generates() {
1337 assert_schema_nonempty_object::<EmptyStateProps>("EmptyStateProps");
1338 }
1339
1340 #[test]
1341 fn schema_for_form_section_props_generates() {
1342 assert_schema_nonempty_object::<FormSectionProps>("FormSectionProps");
1343 }
1344
1345 #[test]
1346 fn schema_for_page_header_props_generates() {
1347 assert_schema_nonempty_object::<PageHeaderProps>("PageHeaderProps");
1348 }
1349
1350 #[test]
1351 fn schema_for_button_group_props_generates() {
1352 assert_schema_nonempty_object::<ButtonGroupProps>("ButtonGroupProps");
1353 }
1354
1355 #[test]
1356 fn schema_for_dropdown_menu_action_generates() {
1357 assert_schema_nonempty_object::<DropdownMenuAction>("DropdownMenuAction");
1358 }
1359
1360 #[test]
1361 fn schema_for_dropdown_menu_props_generates() {
1362 assert_schema_nonempty_object::<DropdownMenuProps>("DropdownMenuProps");
1363 }
1364
1365 #[test]
1366 fn schema_for_data_table_props_generates() {
1367 assert_schema_nonempty_object::<DataTableProps>("DataTableProps");
1368 }
1369
1370 #[test]
1371 fn schema_for_kanban_column_props_generates() {
1372 assert_schema_nonempty_object::<KanbanColumnProps>("KanbanColumnProps");
1373 }
1374
1375 #[test]
1376 fn schema_for_kanban_board_props_generates() {
1377 assert_schema_nonempty_object::<KanbanBoardProps>("KanbanBoardProps");
1378 }
1379
1380 #[test]
1381 fn schema_for_calendar_cell_props_generates() {
1382 assert_schema_nonempty_object::<CalendarCellProps>("CalendarCellProps");
1383 }
1384
1385 #[test]
1386 fn schema_for_action_card_props_generates() {
1387 assert_schema_nonempty_object::<ActionCardProps>("ActionCardProps");
1388 }
1389
1390 #[test]
1391 fn schema_for_product_tile_props_generates() {
1392 assert_schema_nonempty_object::<ProductTileProps>("ProductTileProps");
1393 }
1394
1395 #[test]
1396 fn card_props_round_trips_footer() {
1397 let original = CardProps {
1398 title: "Hero".to_string(),
1399 description: None,
1400 subtitle: None,
1401 badge: None,
1402 max_width: None,
1403 footer: vec!["btn1".to_string(), "btn2".to_string()],
1404 variant: CardVariant::Bordered,
1405 };
1406 let json = serde_json::to_string(&original).unwrap();
1407 let parsed: CardProps = serde_json::from_str(&json).unwrap();
1408 assert_eq!(original.footer, parsed.footer);
1409 }
1410
1411 #[test]
1412 fn tab_round_trips_children() {
1413 let original = Tab {
1414 value: "overview".to_string(),
1415 label: "Overview".to_string(),
1416 children: vec!["panel1".to_string()],
1417 };
1418 let json = serde_json::to_string(&original).unwrap();
1419 let parsed: Tab = serde_json::from_str(&json).unwrap();
1420 assert_eq!(original.children, parsed.children);
1421 }
1422
1423 #[test]
1424 fn card_props_omits_empty_footer_in_json() {
1425 let card = CardProps {
1426 title: "Card".to_string(),
1427 description: None,
1428 subtitle: None,
1429 badge: None,
1430 max_width: None,
1431 footer: Vec::new(),
1432 variant: CardVariant::Bordered,
1433 };
1434 let json = serde_json::to_string(&card).unwrap();
1435 assert!(
1436 !json.contains("\"footer\""),
1437 "empty footer must be skipped, got: {json}"
1438 );
1439 }
1440
1441 #[test]
1442 fn card_props_round_trips_badge() {
1443 let original = CardProps {
1444 title: "Hero".to_string(),
1445 description: None,
1446 subtitle: None,
1447 badge: Some("Scade tra 9m".to_string()),
1448 max_width: None,
1449 footer: Vec::new(),
1450 variant: CardVariant::Bordered,
1451 };
1452 let json = serde_json::to_string(&original).unwrap();
1453 let parsed: CardProps = serde_json::from_str(&json).unwrap();
1454 assert_eq!(original.badge, parsed.badge);
1455 }
1456
1457 #[test]
1458 fn card_props_omits_empty_badge_in_json() {
1459 let card = CardProps {
1460 title: "Card".to_string(),
1461 description: None,
1462 subtitle: None,
1463 badge: None,
1464 max_width: None,
1465 footer: Vec::new(),
1466 variant: CardVariant::Bordered,
1467 };
1468 let json = serde_json::to_string(&card).unwrap();
1469 assert!(
1470 !json.contains("\"badge\""),
1471 "empty badge must be skipped, got: {json}"
1472 );
1473 }
1474
1475 #[test]
1476 fn card_props_round_trips_subtitle() {
1477 let original = CardProps {
1478 title: "Hero".to_string(),
1479 description: None,
1480 subtitle: Some("Marco Rossi".to_string()),
1481 badge: None,
1482 max_width: None,
1483 footer: Vec::new(),
1484 variant: CardVariant::Bordered,
1485 };
1486 let json = serde_json::to_string(&original).unwrap();
1487 let parsed: CardProps = serde_json::from_str(&json).unwrap();
1488 assert_eq!(original.subtitle, parsed.subtitle);
1489 }
1490
1491 #[test]
1492 fn card_props_omits_empty_subtitle_in_json() {
1493 let card = CardProps {
1494 title: "Card".to_string(),
1495 description: None,
1496 subtitle: None,
1497 badge: None,
1498 max_width: None,
1499 footer: Vec::new(),
1500 variant: CardVariant::Bordered,
1501 };
1502 let json = serde_json::to_string(&card).unwrap();
1503 assert!(
1504 !json.contains("\"subtitle\""),
1505 "empty subtitle must be skipped, got: {json}"
1506 );
1507 }
1508
1509 #[test]
1510 fn card_props_schema_includes_badge() {
1511 let schema = schemars::schema_for!(CardProps);
1512 let value = serde_json::to_value(&schema).expect("schema serializes to JSON");
1513 let props = value
1514 .get("properties")
1515 .and_then(|p| p.as_object())
1516 .expect("schema has a properties object");
1517 assert!(
1518 props.contains_key("badge"),
1519 "CardProps schema must expose a `badge` property; got keys: {:?}",
1520 props.keys().collect::<Vec<_>>()
1521 );
1522 let badge_schema = props.get("badge").expect("badge entry");
1527 let badge_json = badge_schema.to_string();
1528 assert!(
1529 badge_json.contains("\"string\""),
1530 "badge schema entry must mention string type; got: {badge_json}"
1531 );
1532 }
1533
1534 #[test]
1535 fn card_props_schema_includes_subtitle() {
1536 let schema = schemars::schema_for!(CardProps);
1537 let value = serde_json::to_value(&schema).expect("schema serializes to JSON");
1538 let props = value
1539 .get("properties")
1540 .and_then(|p| p.as_object())
1541 .expect("schema has a properties object");
1542 assert!(
1543 props.contains_key("subtitle"),
1544 "CardProps schema must expose a `subtitle` property; got keys: {:?}",
1545 props.keys().collect::<Vec<_>>()
1546 );
1547 let subtitle_schema = props.get("subtitle").expect("subtitle entry");
1552 let subtitle_json = subtitle_schema.to_string();
1553 assert!(
1554 subtitle_json.contains("\"string\""),
1555 "subtitle schema entry must mention string type; got: {subtitle_json}"
1556 );
1557 }
1558
1559 #[test]
1560 fn schema_for_checkbox_list_props_generates() {
1561 assert_schema_nonempty_object::<CheckboxListProps>("CheckboxListProps");
1562 }
1563
1564 #[test]
1565 fn checkbox_list_props_serde_roundtrip() {
1566 let json = serde_json::json!({
1567 "field": "services",
1568 "options": [{"value": "a", "label": "Alpha"}, {"value": "b", "label": "Beta"}],
1569 "selected_path": "/preselected"
1570 });
1571 let parsed: CheckboxListProps = serde_json::from_value(json.clone()).expect("decode");
1572 assert_eq!(parsed.field, "services");
1573 assert_eq!(parsed.options.len(), 2);
1574 assert_eq!(parsed.selected_path.as_deref(), Some("/preselected"));
1575 let reserialized = serde_json::to_value(&parsed).expect("encode");
1576 assert!(reserialized.get("label").is_none());
1578 assert!(reserialized.get("disabled").is_none());
1579 }
1580
1581 #[test]
1582 fn schema_for_rich_text_editor_props_generates() {
1583 assert_schema_nonempty_object::<RichTextEditorProps>("RichTextEditorProps");
1584 }
1585
1586 #[test]
1587 fn rich_text_editor_props_serde_roundtrip() {
1588 let json = serde_json::json!({
1589 "field": "body",
1590 "label": "Body"
1591 });
1592 let parsed: RichTextEditorProps = serde_json::from_value(json).expect("decode");
1593 assert_eq!(parsed.field, "body");
1594 assert_eq!(parsed.label, "Body");
1595 assert!(parsed.placeholder.is_none());
1596 assert!(parsed.default_value.is_none());
1597 assert!(parsed.data_path.is_none());
1598 assert!(parsed.error.is_none());
1599 let reserialized = serde_json::to_value(&parsed).expect("encode");
1600 assert!(reserialized.get("placeholder").is_none());
1602 assert!(reserialized.get("error").is_none());
1603 }
1604}
1605
1606#[cfg(test)]
1607mod strum_tests {
1608 use super::*;
1609
1610 #[test]
1614 fn variant_enums_strum_matches_serde_wire_format() {
1615 fn check<T: AsRef<str> + serde::Serialize>(variants: &[T], label: &str) {
1616 for v in variants {
1617 let json = serde_json::to_string(v).expect("serialize");
1618 let json_stripped = json.trim_matches('"');
1619 assert_eq!(
1620 v.as_ref(),
1621 json_stripped,
1622 "strum AsRefStr drifted from serde for {label} variant"
1623 );
1624 }
1625 }
1626 check(
1627 &[
1628 AlertVariant::Info,
1629 AlertVariant::Success,
1630 AlertVariant::Warning,
1631 AlertVariant::Error,
1632 ],
1633 "AlertVariant",
1634 );
1635 check(
1636 &[
1637 BadgeVariant::Default,
1638 BadgeVariant::Secondary,
1639 BadgeVariant::Destructive,
1640 BadgeVariant::Outline,
1641 ],
1642 "BadgeVariant",
1643 );
1644 check(
1645 &[
1646 ButtonVariant::Default,
1647 ButtonVariant::Secondary,
1648 ButtonVariant::Destructive,
1649 ButtonVariant::Outline,
1650 ButtonVariant::Ghost,
1651 ButtonVariant::Link,
1652 ],
1653 "ButtonVariant",
1654 );
1655 check(
1656 &[
1657 ToastVariant::Info,
1658 ToastVariant::Success,
1659 ToastVariant::Warning,
1660 ToastVariant::Error,
1661 ],
1662 "ToastVariant",
1663 );
1664 }
1665
1666 #[test]
1667 fn alert_variant_as_ref_str_matches_wire_format() {
1668 assert_eq!(AlertVariant::Success.as_ref(), "success");
1669 assert_eq!(AlertVariant::Warning.as_ref(), "warning");
1670 assert_eq!(AlertVariant::Info.as_ref(), "info");
1671 assert_eq!(AlertVariant::Error.as_ref(), "error");
1672 }
1673}
1674
1675#[cfg(test)]
1676mod card_variant_tests {
1677 use super::*;
1678
1679 #[test]
1680 fn card_variant_default_is_bordered() {
1681 assert_eq!(CardVariant::default(), CardVariant::Bordered);
1682 }
1683
1684 #[test]
1685 fn card_variant_serializes_snake_case() {
1686 assert_eq!(
1687 serde_json::to_value(CardVariant::Bordered).unwrap(),
1688 serde_json::json!("bordered")
1689 );
1690 assert_eq!(
1691 serde_json::to_value(CardVariant::Elevated).unwrap(),
1692 serde_json::json!("elevated")
1693 );
1694 }
1695
1696 #[test]
1697 fn card_variant_deserializes_snake_case() {
1698 assert_eq!(
1699 serde_json::from_value::<CardVariant>(serde_json::json!("bordered")).unwrap(),
1700 CardVariant::Bordered
1701 );
1702 assert_eq!(
1703 serde_json::from_value::<CardVariant>(serde_json::json!("elevated")).unwrap(),
1704 CardVariant::Elevated
1705 );
1706 }
1707
1708 #[test]
1709 fn card_props_without_variant_defaults_to_bordered() {
1710 let v = serde_json::json!({"title": "x"});
1711 let p: CardProps = serde_json::from_value(v).unwrap();
1712 assert_eq!(p.variant, CardVariant::Bordered);
1713 }
1714
1715 #[test]
1716 fn card_props_with_elevated_variant() {
1717 let v = serde_json::json!({"title": "x", "variant": "elevated"});
1718 let p: CardProps = serde_json::from_value(v).unwrap();
1719 assert_eq!(p.variant, CardVariant::Elevated);
1720 }
1721
1722 #[test]
1723 fn card_props_roundtrip_preserves_variant() {
1724 let p = CardProps {
1725 title: "x".into(),
1726 description: None,
1727 subtitle: None,
1728 badge: None,
1729 max_width: None,
1730 footer: vec![],
1731 variant: CardVariant::Elevated,
1732 };
1733 let j = serde_json::to_value(&p).unwrap();
1734 let back: CardProps = serde_json::from_value(j).unwrap();
1735 assert_eq!(back.variant, CardVariant::Elevated);
1736 }
1737}
1738
1739#[cfg(test)]
1740mod kanban_board_props_tests {
1741 use super::*;
1742
1743 #[test]
1744 fn kanban_board_props_serde_static_columns() {
1745 let v = serde_json::json!({
1746 "columns": [{"title": "To Do", "items": [], "id": "todo", "count": 0}]
1747 });
1748 let p: KanbanBoardProps = serde_json::from_value(v).unwrap();
1749 assert_eq!(p.columns.len(), 1);
1750 assert!(p.data_path.is_none());
1751 }
1752
1753 #[test]
1754 fn kanban_board_props_serde_data_path() {
1755 let v = serde_json::json!({"data_path": "/columns"});
1756 let p: KanbanBoardProps = serde_json::from_value(v).unwrap();
1757 assert!(p.columns.is_empty());
1758 assert_eq!(p.data_path.as_deref(), Some("/columns"));
1759 }
1760
1761 #[test]
1762 fn kanban_board_props_serde_neither() {
1763 let v = serde_json::json!({});
1764 let p: KanbanBoardProps = serde_json::from_value(v).unwrap();
1765 assert!(p.columns.is_empty());
1766 assert!(p.data_path.is_none());
1767 }
1768
1769 #[test]
1770 fn kanban_board_props_empty_columns_skipped_on_serialize() {
1771 let p = KanbanBoardProps {
1772 columns: vec![],
1773 data_path: Some("/x".into()),
1774 mobile_default_column: None,
1775 empty_label: None,
1776 };
1777 let j = serde_json::to_value(&p).unwrap();
1778 assert!(
1779 j.get("columns").is_none(),
1780 "empty columns must be skipped, got: {j}"
1781 );
1782 assert_eq!(j.get("data_path").and_then(|v| v.as_str()), Some("/x"));
1783 }
1784}
1785
1786#[cfg(test)]
1787mod page_header_actions_tests {
1788 use super::*;
1789
1790 #[test]
1791 fn page_header_actions_missing_field() {
1792 let v = serde_json::json!({"title": "X"});
1793 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
1794 assert!(p.actions.is_empty());
1795 }
1796
1797 #[test]
1798 fn page_header_actions_null() {
1799 let v = serde_json::json!({"title": "X", "actions": null});
1800 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
1801 assert!(p.actions.is_empty());
1802 }
1803
1804 #[test]
1805 fn page_header_actions_empty_string() {
1806 let v = serde_json::json!({"title": "X", "actions": ""});
1807 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
1808 assert!(p.actions.is_empty());
1809 }
1810
1811 #[test]
1812 fn page_header_actions_empty_array() {
1813 let v = serde_json::json!({"title": "X", "actions": []});
1814 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
1815 assert!(p.actions.is_empty());
1816 }
1817
1818 #[test]
1819 fn page_header_actions_non_empty_array() {
1820 let v = serde_json::json!({"title": "X", "actions": ["a", "b"]});
1821 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
1822 assert_eq!(p.actions, vec!["a".to_string(), "b".to_string()]);
1823 }
1824
1825 #[test]
1826 fn page_header_actions_non_empty_string_rejected() {
1827 let v = serde_json::json!({"title": "X", "actions": "not-empty"});
1828 let result: Result<PageHeaderProps, _> = serde_json::from_value(v);
1829 assert!(result.is_err(), "non-empty string must be rejected");
1830 }
1831
1832 #[test]
1833 fn page_header_actions_non_string_array_rejected() {
1834 let v = serde_json::json!({"title": "X", "actions": [1, 2, 3]});
1835 let result: Result<PageHeaderProps, _> = serde_json::from_value(v);
1836 assert!(result.is_err(), "array of non-strings must be rejected");
1837 }
1838}