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")]
1003 pub badge_key: Option<String>,
1004 #[serde(default, skip_serializing_if = "Option::is_none")]
1006 pub badge_variant_key: Option<String>,
1007 #[serde(default, skip_serializing_if = "Option::is_none")]
1009 pub row_key: Option<String>,
1010 #[serde(default, skip_serializing_if = "Option::is_none")]
1011 pub row_actions: Option<Vec<DropdownMenuAction>>,
1012 #[serde(default, skip_serializing_if = "Option::is_none")]
1013 pub empty_message: Option<String>,
1014 #[serde(default, skip_serializing_if = "Option::is_none")]
1016 pub columns: Option<u8>,
1017}
1018
1019#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1021pub struct KanbanColumnProps {
1022 pub id: String,
1023 pub title: String,
1024 pub count: u32,
1025 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1027 pub children: Vec<String>,
1028}
1029
1030#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1032pub struct KanbanBoardProps {
1033 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1035 pub columns: Vec<KanbanColumnProps>,
1036 #[serde(default, skip_serializing_if = "Option::is_none")]
1041 pub data_path: Option<String>,
1042 #[serde(default, skip_serializing_if = "Option::is_none")]
1043 pub mobile_default_column: Option<String>,
1044 #[serde(default, skip_serializing_if = "Option::is_none")]
1048 pub empty_label: Option<String>,
1049}
1050
1051#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1056pub struct CalendarCellProps {
1057 pub day: u8,
1058 #[serde(default)]
1059 pub is_today: bool,
1060 #[serde(default)]
1061 pub is_current_month: bool,
1062 #[serde(default)]
1063 pub event_count: u32,
1064 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1067 pub dot_colors: Vec<String>,
1068}
1069
1070#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1072#[serde(rename_all = "snake_case")]
1073pub enum ActionCardVariant {
1074 #[default]
1075 Default,
1076 Setup,
1077 Danger,
1078}
1079
1080#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1085pub struct ActionCardProps {
1086 pub title: String,
1087 pub description: String,
1088 #[serde(default, skip_serializing_if = "Option::is_none")]
1089 pub icon: Option<String>,
1090 #[serde(default)]
1091 pub variant: ActionCardVariant,
1092 #[serde(default, skip_serializing_if = "Option::is_none")]
1094 pub href: Option<String>,
1095}
1096
1097#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1102pub struct ProductTileProps {
1103 pub product_id: String,
1104 pub name: String,
1105 pub price: String,
1106 pub field: String,
1107 #[serde(default, skip_serializing_if = "Option::is_none")]
1108 pub default_quantity: Option<u32>,
1109}
1110
1111fn deserialize_actions_lax<'de, D: serde::Deserializer<'de>>(
1117 d: D,
1118) -> Result<Vec<String>, D::Error> {
1119 use serde::de::Error;
1120 let v = serde_json::Value::deserialize(d)?;
1121 match v {
1122 serde_json::Value::Null => Ok(Vec::new()),
1123 serde_json::Value::String(s) if s.is_empty() => Ok(Vec::new()),
1124 serde_json::Value::Array(arr) => arr
1125 .into_iter()
1126 .map(|item| {
1127 item.as_str()
1128 .map(String::from)
1129 .ok_or_else(|| D::Error::custom("PageHeader.actions: expected string in array"))
1130 })
1131 .collect(),
1132 other => Err(D::Error::custom(format!(
1133 "PageHeader.actions: expected null, empty string, or array of strings; got {other:?}"
1134 ))),
1135 }
1136}
1137
1138#[cfg(test)]
1139mod schema_smoke_tests {
1140 use super::*;
1151
1152 fn assert_schema_nonempty_object<T: schemars::JsonSchema>(type_label: &str) {
1153 let schema = schemars::schema_for!(T);
1154 let value = serde_json::to_value(&schema).expect("schema serializes to JSON");
1155 assert!(
1156 value.is_object(),
1157 "{type_label}: schema must be a JSON object"
1158 );
1159 let props = value
1160 .get("properties")
1161 .and_then(|p| p.as_object())
1162 .map(|o| !o.is_empty())
1163 .unwrap_or(false);
1164 assert!(
1165 props,
1166 "{type_label}: schema must have a non-empty `properties` field"
1167 );
1168 }
1169
1170 #[test]
1171 fn schema_for_card_props_generates() {
1172 assert_schema_nonempty_object::<CardProps>("CardProps");
1173 }
1174
1175 #[test]
1176 fn schema_for_table_props_generates() {
1177 assert_schema_nonempty_object::<TableProps>("TableProps");
1178 }
1179
1180 #[test]
1181 fn schema_for_form_props_generates() {
1182 assert_schema_nonempty_object::<FormProps>("FormProps");
1183 }
1184
1185 #[test]
1186 fn schema_for_button_props_generates() {
1187 assert_schema_nonempty_object::<ButtonProps>("ButtonProps");
1188 }
1189
1190 #[test]
1191 fn schema_for_input_props_generates() {
1192 assert_schema_nonempty_object::<InputProps>("InputProps");
1193 }
1194
1195 #[test]
1196 fn schema_for_select_props_generates() {
1197 assert_schema_nonempty_object::<SelectProps>("SelectProps");
1198 }
1199
1200 #[test]
1201 fn schema_for_alert_props_generates() {
1202 assert_schema_nonempty_object::<AlertProps>("AlertProps");
1203 }
1204
1205 #[test]
1206 fn schema_for_badge_props_generates() {
1207 assert_schema_nonempty_object::<BadgeProps>("BadgeProps");
1208 }
1209
1210 #[test]
1211 fn schema_for_modal_props_generates() {
1212 assert_schema_nonempty_object::<ModalProps>("ModalProps");
1213 }
1214
1215 #[test]
1216 fn schema_for_text_props_generates() {
1217 assert_schema_nonempty_object::<TextProps>("TextProps");
1218 }
1219
1220 #[test]
1221 fn schema_for_checkbox_props_generates() {
1222 assert_schema_nonempty_object::<CheckboxProps>("CheckboxProps");
1223 }
1224
1225 #[test]
1226 fn schema_for_switch_props_generates() {
1227 assert_schema_nonempty_object::<SwitchProps>("SwitchProps");
1228 }
1229
1230 #[test]
1231 fn schema_for_separator_props_generates() {
1232 assert_schema_nonempty_object::<SeparatorProps>("SeparatorProps");
1233 }
1234
1235 #[test]
1236 fn schema_for_description_list_props_generates() {
1237 assert_schema_nonempty_object::<DescriptionListProps>("DescriptionListProps");
1238 }
1239
1240 #[test]
1241 fn schema_for_tab_generates() {
1242 assert_schema_nonempty_object::<Tab>("Tab");
1243 }
1244
1245 #[test]
1246 fn schema_for_tabs_props_generates() {
1247 assert_schema_nonempty_object::<TabsProps>("TabsProps");
1248 }
1249
1250 #[test]
1251 fn schema_for_breadcrumb_props_generates() {
1252 assert_schema_nonempty_object::<BreadcrumbProps>("BreadcrumbProps");
1253 }
1254
1255 #[test]
1256 fn schema_for_pagination_props_generates() {
1257 assert_schema_nonempty_object::<PaginationProps>("PaginationProps");
1258 }
1259
1260 #[test]
1261 fn schema_for_progress_props_generates() {
1262 assert_schema_nonempty_object::<ProgressProps>("ProgressProps");
1263 }
1264
1265 #[test]
1266 fn schema_for_image_props_generates() {
1267 assert_schema_nonempty_object::<ImageProps>("ImageProps");
1268 }
1269
1270 #[test]
1271 fn image_inline_svg_factory_roundtrips_via_serde() {
1272 let p = ImageProps::inline_svg("<svg/>", "alt");
1273 let json = serde_json::to_value(&p).expect("serialization must not fail");
1274 let parsed: ImageProps =
1275 serde_json::from_value(json).expect("deserialization must not fail");
1276 assert_eq!(parsed.inline_svg, Some("<svg/>".to_string()));
1277 assert_eq!(parsed.alt, "alt");
1278 assert_eq!(parsed.src, "");
1279 }
1280
1281 #[test]
1282 fn schema_for_avatar_props_generates() {
1283 assert_schema_nonempty_object::<AvatarProps>("AvatarProps");
1284 }
1285
1286 #[test]
1287 fn schema_for_skeleton_props_generates() {
1288 assert_schema_nonempty_object::<SkeletonProps>("SkeletonProps");
1289 }
1290
1291 #[test]
1292 fn schema_for_stat_card_props_generates() {
1293 assert_schema_nonempty_object::<StatCardProps>("StatCardProps");
1294 }
1295
1296 #[test]
1297 fn schema_for_checklist_props_generates() {
1298 assert_schema_nonempty_object::<ChecklistProps>("ChecklistProps");
1299 }
1300
1301 #[test]
1302 fn schema_for_toast_props_generates() {
1303 assert_schema_nonempty_object::<ToastProps>("ToastProps");
1304 }
1305
1306 #[test]
1307 fn schema_for_notification_dropdown_props_generates() {
1308 assert_schema_nonempty_object::<NotificationDropdownProps>("NotificationDropdownProps");
1309 }
1310
1311 #[test]
1312 fn schema_for_sidebar_props_generates() {
1313 assert_schema_nonempty_object::<SidebarProps>("SidebarProps");
1314 }
1315
1316 #[test]
1317 fn schema_for_header_props_generates() {
1318 assert_schema_nonempty_object::<HeaderProps>("HeaderProps");
1319 }
1320
1321 #[test]
1322 fn schema_for_grid_props_generates() {
1323 assert_schema_nonempty_object::<GridProps>("GridProps");
1324 }
1325
1326 #[test]
1327 fn schema_for_collapsible_props_generates() {
1328 assert_schema_nonempty_object::<CollapsibleProps>("CollapsibleProps");
1329 }
1330
1331 #[test]
1332 fn schema_for_empty_state_props_generates() {
1333 assert_schema_nonempty_object::<EmptyStateProps>("EmptyStateProps");
1334 }
1335
1336 #[test]
1337 fn schema_for_form_section_props_generates() {
1338 assert_schema_nonempty_object::<FormSectionProps>("FormSectionProps");
1339 }
1340
1341 #[test]
1342 fn schema_for_page_header_props_generates() {
1343 assert_schema_nonempty_object::<PageHeaderProps>("PageHeaderProps");
1344 }
1345
1346 #[test]
1347 fn schema_for_button_group_props_generates() {
1348 assert_schema_nonempty_object::<ButtonGroupProps>("ButtonGroupProps");
1349 }
1350
1351 #[test]
1352 fn schema_for_dropdown_menu_action_generates() {
1353 assert_schema_nonempty_object::<DropdownMenuAction>("DropdownMenuAction");
1354 }
1355
1356 #[test]
1357 fn schema_for_dropdown_menu_props_generates() {
1358 assert_schema_nonempty_object::<DropdownMenuProps>("DropdownMenuProps");
1359 }
1360
1361 #[test]
1362 fn schema_for_data_table_props_generates() {
1363 assert_schema_nonempty_object::<DataTableProps>("DataTableProps");
1364 }
1365
1366 #[test]
1367 fn schema_for_kanban_column_props_generates() {
1368 assert_schema_nonempty_object::<KanbanColumnProps>("KanbanColumnProps");
1369 }
1370
1371 #[test]
1372 fn schema_for_kanban_board_props_generates() {
1373 assert_schema_nonempty_object::<KanbanBoardProps>("KanbanBoardProps");
1374 }
1375
1376 #[test]
1377 fn schema_for_calendar_cell_props_generates() {
1378 assert_schema_nonempty_object::<CalendarCellProps>("CalendarCellProps");
1379 }
1380
1381 #[test]
1382 fn schema_for_action_card_props_generates() {
1383 assert_schema_nonempty_object::<ActionCardProps>("ActionCardProps");
1384 }
1385
1386 #[test]
1387 fn schema_for_product_tile_props_generates() {
1388 assert_schema_nonempty_object::<ProductTileProps>("ProductTileProps");
1389 }
1390
1391 #[test]
1392 fn card_props_round_trips_footer() {
1393 let original = CardProps {
1394 title: "Hero".to_string(),
1395 description: None,
1396 subtitle: None,
1397 badge: None,
1398 max_width: None,
1399 footer: vec!["btn1".to_string(), "btn2".to_string()],
1400 variant: CardVariant::Bordered,
1401 };
1402 let json = serde_json::to_string(&original).unwrap();
1403 let parsed: CardProps = serde_json::from_str(&json).unwrap();
1404 assert_eq!(original.footer, parsed.footer);
1405 }
1406
1407 #[test]
1408 fn tab_round_trips_children() {
1409 let original = Tab {
1410 value: "overview".to_string(),
1411 label: "Overview".to_string(),
1412 children: vec!["panel1".to_string()],
1413 };
1414 let json = serde_json::to_string(&original).unwrap();
1415 let parsed: Tab = serde_json::from_str(&json).unwrap();
1416 assert_eq!(original.children, parsed.children);
1417 }
1418
1419 #[test]
1420 fn card_props_omits_empty_footer_in_json() {
1421 let card = CardProps {
1422 title: "Card".to_string(),
1423 description: None,
1424 subtitle: None,
1425 badge: None,
1426 max_width: None,
1427 footer: Vec::new(),
1428 variant: CardVariant::Bordered,
1429 };
1430 let json = serde_json::to_string(&card).unwrap();
1431 assert!(
1432 !json.contains("\"footer\""),
1433 "empty footer must be skipped, got: {json}"
1434 );
1435 }
1436
1437 #[test]
1438 fn card_props_round_trips_badge() {
1439 let original = CardProps {
1440 title: "Hero".to_string(),
1441 description: None,
1442 subtitle: None,
1443 badge: Some("Scade tra 9m".to_string()),
1444 max_width: None,
1445 footer: Vec::new(),
1446 variant: CardVariant::Bordered,
1447 };
1448 let json = serde_json::to_string(&original).unwrap();
1449 let parsed: CardProps = serde_json::from_str(&json).unwrap();
1450 assert_eq!(original.badge, parsed.badge);
1451 }
1452
1453 #[test]
1454 fn card_props_omits_empty_badge_in_json() {
1455 let card = CardProps {
1456 title: "Card".to_string(),
1457 description: None,
1458 subtitle: None,
1459 badge: None,
1460 max_width: None,
1461 footer: Vec::new(),
1462 variant: CardVariant::Bordered,
1463 };
1464 let json = serde_json::to_string(&card).unwrap();
1465 assert!(
1466 !json.contains("\"badge\""),
1467 "empty badge must be skipped, got: {json}"
1468 );
1469 }
1470
1471 #[test]
1472 fn card_props_round_trips_subtitle() {
1473 let original = CardProps {
1474 title: "Hero".to_string(),
1475 description: None,
1476 subtitle: Some("Marco Rossi".to_string()),
1477 badge: None,
1478 max_width: None,
1479 footer: Vec::new(),
1480 variant: CardVariant::Bordered,
1481 };
1482 let json = serde_json::to_string(&original).unwrap();
1483 let parsed: CardProps = serde_json::from_str(&json).unwrap();
1484 assert_eq!(original.subtitle, parsed.subtitle);
1485 }
1486
1487 #[test]
1488 fn card_props_omits_empty_subtitle_in_json() {
1489 let card = CardProps {
1490 title: "Card".to_string(),
1491 description: None,
1492 subtitle: None,
1493 badge: None,
1494 max_width: None,
1495 footer: Vec::new(),
1496 variant: CardVariant::Bordered,
1497 };
1498 let json = serde_json::to_string(&card).unwrap();
1499 assert!(
1500 !json.contains("\"subtitle\""),
1501 "empty subtitle must be skipped, got: {json}"
1502 );
1503 }
1504
1505 #[test]
1506 fn card_props_schema_includes_badge() {
1507 let schema = schemars::schema_for!(CardProps);
1508 let value = serde_json::to_value(&schema).expect("schema serializes to JSON");
1509 let props = value
1510 .get("properties")
1511 .and_then(|p| p.as_object())
1512 .expect("schema has a properties object");
1513 assert!(
1514 props.contains_key("badge"),
1515 "CardProps schema must expose a `badge` property; got keys: {:?}",
1516 props.keys().collect::<Vec<_>>()
1517 );
1518 let badge_schema = props.get("badge").expect("badge entry");
1523 let badge_json = badge_schema.to_string();
1524 assert!(
1525 badge_json.contains("\"string\""),
1526 "badge schema entry must mention string type; got: {badge_json}"
1527 );
1528 }
1529
1530 #[test]
1531 fn card_props_schema_includes_subtitle() {
1532 let schema = schemars::schema_for!(CardProps);
1533 let value = serde_json::to_value(&schema).expect("schema serializes to JSON");
1534 let props = value
1535 .get("properties")
1536 .and_then(|p| p.as_object())
1537 .expect("schema has a properties object");
1538 assert!(
1539 props.contains_key("subtitle"),
1540 "CardProps schema must expose a `subtitle` property; got keys: {:?}",
1541 props.keys().collect::<Vec<_>>()
1542 );
1543 let subtitle_schema = props.get("subtitle").expect("subtitle entry");
1548 let subtitle_json = subtitle_schema.to_string();
1549 assert!(
1550 subtitle_json.contains("\"string\""),
1551 "subtitle schema entry must mention string type; got: {subtitle_json}"
1552 );
1553 }
1554
1555 #[test]
1556 fn schema_for_checkbox_list_props_generates() {
1557 assert_schema_nonempty_object::<CheckboxListProps>("CheckboxListProps");
1558 }
1559
1560 #[test]
1561 fn checkbox_list_props_serde_roundtrip() {
1562 let json = serde_json::json!({
1563 "field": "services",
1564 "options": [{"value": "a", "label": "Alpha"}, {"value": "b", "label": "Beta"}],
1565 "selected_path": "/preselected"
1566 });
1567 let parsed: CheckboxListProps = serde_json::from_value(json.clone()).expect("decode");
1568 assert_eq!(parsed.field, "services");
1569 assert_eq!(parsed.options.len(), 2);
1570 assert_eq!(parsed.selected_path.as_deref(), Some("/preselected"));
1571 let reserialized = serde_json::to_value(&parsed).expect("encode");
1572 assert!(reserialized.get("label").is_none());
1574 assert!(reserialized.get("disabled").is_none());
1575 }
1576
1577 #[test]
1578 fn schema_for_rich_text_editor_props_generates() {
1579 assert_schema_nonempty_object::<RichTextEditorProps>("RichTextEditorProps");
1580 }
1581
1582 #[test]
1583 fn rich_text_editor_props_serde_roundtrip() {
1584 let json = serde_json::json!({
1585 "field": "body",
1586 "label": "Body"
1587 });
1588 let parsed: RichTextEditorProps = serde_json::from_value(json).expect("decode");
1589 assert_eq!(parsed.field, "body");
1590 assert_eq!(parsed.label, "Body");
1591 assert!(parsed.placeholder.is_none());
1592 assert!(parsed.default_value.is_none());
1593 assert!(parsed.data_path.is_none());
1594 assert!(parsed.error.is_none());
1595 let reserialized = serde_json::to_value(&parsed).expect("encode");
1596 assert!(reserialized.get("placeholder").is_none());
1598 assert!(reserialized.get("error").is_none());
1599 }
1600}
1601
1602#[cfg(test)]
1603mod strum_tests {
1604 use super::*;
1605
1606 #[test]
1610 fn variant_enums_strum_matches_serde_wire_format() {
1611 fn check<T: AsRef<str> + serde::Serialize>(variants: &[T], label: &str) {
1612 for v in variants {
1613 let json = serde_json::to_string(v).expect("serialize");
1614 let json_stripped = json.trim_matches('"');
1615 assert_eq!(
1616 v.as_ref(),
1617 json_stripped,
1618 "strum AsRefStr drifted from serde for {label} variant"
1619 );
1620 }
1621 }
1622 check(
1623 &[
1624 AlertVariant::Info,
1625 AlertVariant::Success,
1626 AlertVariant::Warning,
1627 AlertVariant::Error,
1628 ],
1629 "AlertVariant",
1630 );
1631 check(
1632 &[
1633 BadgeVariant::Default,
1634 BadgeVariant::Secondary,
1635 BadgeVariant::Destructive,
1636 BadgeVariant::Outline,
1637 ],
1638 "BadgeVariant",
1639 );
1640 check(
1641 &[
1642 ButtonVariant::Default,
1643 ButtonVariant::Secondary,
1644 ButtonVariant::Destructive,
1645 ButtonVariant::Outline,
1646 ButtonVariant::Ghost,
1647 ButtonVariant::Link,
1648 ],
1649 "ButtonVariant",
1650 );
1651 check(
1652 &[
1653 ToastVariant::Info,
1654 ToastVariant::Success,
1655 ToastVariant::Warning,
1656 ToastVariant::Error,
1657 ],
1658 "ToastVariant",
1659 );
1660 }
1661
1662 #[test]
1663 fn alert_variant_as_ref_str_matches_wire_format() {
1664 assert_eq!(AlertVariant::Success.as_ref(), "success");
1665 assert_eq!(AlertVariant::Warning.as_ref(), "warning");
1666 assert_eq!(AlertVariant::Info.as_ref(), "info");
1667 assert_eq!(AlertVariant::Error.as_ref(), "error");
1668 }
1669}
1670
1671#[cfg(test)]
1672mod card_variant_tests {
1673 use super::*;
1674
1675 #[test]
1676 fn card_variant_default_is_bordered() {
1677 assert_eq!(CardVariant::default(), CardVariant::Bordered);
1678 }
1679
1680 #[test]
1681 fn card_variant_serializes_snake_case() {
1682 assert_eq!(
1683 serde_json::to_value(CardVariant::Bordered).unwrap(),
1684 serde_json::json!("bordered")
1685 );
1686 assert_eq!(
1687 serde_json::to_value(CardVariant::Elevated).unwrap(),
1688 serde_json::json!("elevated")
1689 );
1690 }
1691
1692 #[test]
1693 fn card_variant_deserializes_snake_case() {
1694 assert_eq!(
1695 serde_json::from_value::<CardVariant>(serde_json::json!("bordered")).unwrap(),
1696 CardVariant::Bordered
1697 );
1698 assert_eq!(
1699 serde_json::from_value::<CardVariant>(serde_json::json!("elevated")).unwrap(),
1700 CardVariant::Elevated
1701 );
1702 }
1703
1704 #[test]
1705 fn card_props_without_variant_defaults_to_bordered() {
1706 let v = serde_json::json!({"title": "x"});
1707 let p: CardProps = serde_json::from_value(v).unwrap();
1708 assert_eq!(p.variant, CardVariant::Bordered);
1709 }
1710
1711 #[test]
1712 fn card_props_with_elevated_variant() {
1713 let v = serde_json::json!({"title": "x", "variant": "elevated"});
1714 let p: CardProps = serde_json::from_value(v).unwrap();
1715 assert_eq!(p.variant, CardVariant::Elevated);
1716 }
1717
1718 #[test]
1719 fn card_props_roundtrip_preserves_variant() {
1720 let p = CardProps {
1721 title: "x".into(),
1722 description: None,
1723 subtitle: None,
1724 badge: None,
1725 max_width: None,
1726 footer: vec![],
1727 variant: CardVariant::Elevated,
1728 };
1729 let j = serde_json::to_value(&p).unwrap();
1730 let back: CardProps = serde_json::from_value(j).unwrap();
1731 assert_eq!(back.variant, CardVariant::Elevated);
1732 }
1733}
1734
1735#[cfg(test)]
1736mod kanban_board_props_tests {
1737 use super::*;
1738
1739 #[test]
1740 fn kanban_board_props_serde_static_columns() {
1741 let v = serde_json::json!({
1742 "columns": [{"title": "To Do", "items": [], "id": "todo", "count": 0}]
1743 });
1744 let p: KanbanBoardProps = serde_json::from_value(v).unwrap();
1745 assert_eq!(p.columns.len(), 1);
1746 assert!(p.data_path.is_none());
1747 }
1748
1749 #[test]
1750 fn kanban_board_props_serde_data_path() {
1751 let v = serde_json::json!({"data_path": "/columns"});
1752 let p: KanbanBoardProps = serde_json::from_value(v).unwrap();
1753 assert!(p.columns.is_empty());
1754 assert_eq!(p.data_path.as_deref(), Some("/columns"));
1755 }
1756
1757 #[test]
1758 fn kanban_board_props_serde_neither() {
1759 let v = serde_json::json!({});
1760 let p: KanbanBoardProps = serde_json::from_value(v).unwrap();
1761 assert!(p.columns.is_empty());
1762 assert!(p.data_path.is_none());
1763 }
1764
1765 #[test]
1766 fn kanban_board_props_empty_columns_skipped_on_serialize() {
1767 let p = KanbanBoardProps {
1768 columns: vec![],
1769 data_path: Some("/x".into()),
1770 mobile_default_column: None,
1771 empty_label: None,
1772 };
1773 let j = serde_json::to_value(&p).unwrap();
1774 assert!(
1775 j.get("columns").is_none(),
1776 "empty columns must be skipped, got: {j}"
1777 );
1778 assert_eq!(j.get("data_path").and_then(|v| v.as_str()), Some("/x"));
1779 }
1780}
1781
1782#[cfg(test)]
1783mod page_header_actions_tests {
1784 use super::*;
1785
1786 #[test]
1787 fn page_header_actions_missing_field() {
1788 let v = serde_json::json!({"title": "X"});
1789 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
1790 assert!(p.actions.is_empty());
1791 }
1792
1793 #[test]
1794 fn page_header_actions_null() {
1795 let v = serde_json::json!({"title": "X", "actions": null});
1796 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
1797 assert!(p.actions.is_empty());
1798 }
1799
1800 #[test]
1801 fn page_header_actions_empty_string() {
1802 let v = serde_json::json!({"title": "X", "actions": ""});
1803 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
1804 assert!(p.actions.is_empty());
1805 }
1806
1807 #[test]
1808 fn page_header_actions_empty_array() {
1809 let v = serde_json::json!({"title": "X", "actions": []});
1810 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
1811 assert!(p.actions.is_empty());
1812 }
1813
1814 #[test]
1815 fn page_header_actions_non_empty_array() {
1816 let v = serde_json::json!({"title": "X", "actions": ["a", "b"]});
1817 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
1818 assert_eq!(p.actions, vec!["a".to_string(), "b".to_string()]);
1819 }
1820
1821 #[test]
1822 fn page_header_actions_non_empty_string_rejected() {
1823 let v = serde_json::json!({"title": "X", "actions": "not-empty"});
1824 let result: Result<PageHeaderProps, _> = serde_json::from_value(v);
1825 assert!(result.is_err(), "non-empty string must be rejected");
1826 }
1827
1828 #[test]
1829 fn page_header_actions_non_string_array_rejected() {
1830 let v = serde_json::json!({"title": "X", "actions": [1, 2, 3]});
1831 let result: Result<PageHeaderProps, _> = serde_json::from_value(v);
1832 assert!(result.is_err(), "array of non-strings must be rejected");
1833 }
1834}