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 Image,
140}
141
142#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
144pub struct Column {
145 pub key: String,
146 pub label: String,
147 #[serde(default, skip_serializing_if = "Option::is_none")]
148 pub format: Option<ColumnFormat>,
149}
150
151#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
153pub struct SelectOption {
154 pub value: String,
155 pub label: String,
156}
157
158#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
165#[serde(rename_all = "snake_case")]
166pub enum CardVariant {
167 #[default]
168 Bordered,
169 Elevated,
170}
171
172#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
174pub struct CardProps {
175 pub title: String,
176 #[serde(default, skip_serializing_if = "Option::is_none")]
177 pub description: Option<String>,
178 #[serde(default, skip_serializing_if = "Option::is_none")]
182 pub subtitle: Option<String>,
183 #[serde(default, skip_serializing_if = "Option::is_none")]
187 pub badge: Option<String>,
188 #[serde(default, skip_serializing_if = "Option::is_none")]
189 pub max_width: Option<FormMaxWidth>,
190 #[serde(default, skip_serializing_if = "Vec::is_empty")]
192 pub footer: Vec<String>,
193 #[serde(default)]
194 pub variant: CardVariant,
195}
196
197#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
199pub struct TableProps {
200 pub columns: Vec<Column>,
201 pub data_path: String,
202 #[serde(default, skip_serializing_if = "Option::is_none")]
203 pub row_actions: Option<Vec<Action>>,
204 #[serde(default, skip_serializing_if = "Option::is_none")]
205 pub empty_message: Option<String>,
206 #[serde(default, skip_serializing_if = "Option::is_none")]
207 pub sortable: Option<bool>,
208 #[serde(default, skip_serializing_if = "Option::is_none")]
209 pub sort_column: Option<String>,
210 #[serde(default, skip_serializing_if = "Option::is_none")]
211 pub sort_direction: Option<SortDirection>,
212}
213
214#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
216#[serde(rename_all = "snake_case")]
217pub enum FormMaxWidth {
218 #[default]
219 Default,
220 Narrow,
221 Wide,
222}
223
224#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
226pub struct FormProps {
227 pub action: Action,
228 #[serde(default, skip_serializing_if = "Option::is_none")]
229 pub method: Option<crate::action::HttpMethod>,
230 #[serde(default, skip_serializing_if = "Option::is_none")]
234 pub guard: Option<String>,
235 #[serde(default, skip_serializing_if = "Option::is_none")]
237 pub max_width: Option<FormMaxWidth>,
238 #[serde(default, skip_serializing_if = "Option::is_none")]
242 pub id: Option<String>,
243 #[serde(default, skip_serializing_if = "Option::is_none")]
248 pub enctype: Option<String>,
249}
250
251#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
253#[serde(rename_all = "snake_case")]
254pub enum ButtonType {
255 #[default]
256 Button,
257 Submit,
258}
259
260#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
262pub struct ButtonProps {
263 pub label: String,
264 #[serde(default)]
265 pub variant: ButtonVariant,
266 #[serde(default)]
267 pub size: Size,
268 #[serde(default, skip_serializing_if = "Option::is_none")]
269 pub disabled: Option<bool>,
270 #[serde(default, skip_serializing_if = "Option::is_none")]
271 pub icon: Option<String>,
272 #[serde(default, skip_serializing_if = "Option::is_none")]
273 pub icon_position: Option<IconPosition>,
274 #[serde(default, skip_serializing_if = "Option::is_none")]
275 pub button_type: Option<ButtonType>,
276 #[serde(default, skip_serializing_if = "Option::is_none")]
280 pub form: Option<String>,
281}
282
283#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
285pub struct InputProps {
286 pub field: String,
288 pub label: String,
289 #[serde(default)]
290 pub input_type: InputType,
291 #[serde(default, skip_serializing_if = "Option::is_none")]
292 pub placeholder: Option<String>,
293 #[serde(default, skip_serializing_if = "Option::is_none")]
294 pub required: Option<bool>,
295 #[serde(default, skip_serializing_if = "Option::is_none")]
296 pub disabled: Option<bool>,
297 #[serde(default, skip_serializing_if = "Option::is_none")]
298 pub error: Option<String>,
299 #[serde(default, skip_serializing_if = "Option::is_none")]
300 pub description: Option<String>,
301 #[serde(default, skip_serializing_if = "Option::is_none")]
302 pub default_value: Option<String>,
303 #[serde(default, skip_serializing_if = "Option::is_none")]
305 pub data_path: Option<String>,
306 #[serde(default, skip_serializing_if = "Option::is_none")]
308 pub step: Option<String>,
309 #[serde(default, skip_serializing_if = "Option::is_none")]
313 pub list: Option<String>,
314 #[serde(default, skip_serializing_if = "Option::is_none")]
319 pub accept: Option<String>,
320}
321
322#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
333pub struct RichTextEditorProps {
334 pub field: String,
335 pub label: String,
336 #[serde(default, skip_serializing_if = "Option::is_none")]
337 pub placeholder: Option<String>,
338 #[serde(default, skip_serializing_if = "Option::is_none")]
339 pub default_value: Option<String>,
340 #[serde(default, skip_serializing_if = "Option::is_none")]
341 pub data_path: Option<String>,
342 #[serde(default, skip_serializing_if = "Option::is_none")]
343 pub error: Option<String>,
344}
345
346#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
348pub struct SelectProps {
349 pub field: String,
351 pub label: String,
352 pub options: Vec<SelectOption>,
353 #[serde(default, skip_serializing_if = "Option::is_none")]
354 pub placeholder: Option<String>,
355 #[serde(default, skip_serializing_if = "Option::is_none")]
356 pub required: Option<bool>,
357 #[serde(default, skip_serializing_if = "Option::is_none")]
358 pub disabled: Option<bool>,
359 #[serde(default, skip_serializing_if = "Option::is_none")]
360 pub error: Option<String>,
361 #[serde(default, skip_serializing_if = "Option::is_none")]
362 pub description: Option<String>,
363 #[serde(default, skip_serializing_if = "Option::is_none")]
364 pub default_value: Option<String>,
365 #[serde(default, skip_serializing_if = "Option::is_none")]
367 pub data_path: Option<String>,
368}
369
370#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
372pub struct AlertProps {
373 pub message: String,
374 #[serde(default)]
375 pub variant: AlertVariant,
376 #[serde(default, skip_serializing_if = "Option::is_none")]
377 pub title: Option<String>,
378}
379
380#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
382pub struct BadgeProps {
383 pub label: String,
384 #[serde(default)]
385 pub variant: BadgeVariant,
386}
387
388#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
390pub struct ModalProps {
391 pub id: String,
392 pub title: String,
393 #[serde(default, skip_serializing_if = "Option::is_none")]
394 pub description: Option<String>,
395 #[serde(default, skip_serializing_if = "Option::is_none")]
396 pub trigger_label: Option<String>,
397 #[serde(default, skip_serializing_if = "Vec::is_empty")]
399 pub footer: Vec<String>,
400}
401
402#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
404pub struct TextProps {
405 pub content: String,
406 #[serde(default)]
407 pub element: TextElement,
408}
409
410#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
412pub struct CheckboxProps {
413 pub field: String,
415 #[serde(default, skip_serializing_if = "Option::is_none")]
418 pub value: Option<String>,
419 pub label: String,
420 #[serde(default, skip_serializing_if = "Option::is_none")]
421 pub description: Option<String>,
422 #[serde(default, skip_serializing_if = "Option::is_none")]
423 pub checked: Option<bool>,
424 #[serde(default, skip_serializing_if = "Option::is_none")]
426 pub data_path: Option<String>,
427 #[serde(default, skip_serializing_if = "Option::is_none")]
428 pub required: Option<bool>,
429 #[serde(default, skip_serializing_if = "Option::is_none")]
430 pub disabled: Option<bool>,
431 #[serde(default, skip_serializing_if = "Option::is_none")]
432 pub error: Option<String>,
433}
434
435#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
441pub struct CheckboxListProps {
442 pub field: String,
444 #[serde(default, skip_serializing_if = "Vec::is_empty")]
447 pub options: Vec<SelectOption>,
448 #[serde(default, skip_serializing_if = "Option::is_none")]
450 pub options_path: Option<String>,
451 #[serde(default, skip_serializing_if = "Option::is_none")]
453 pub selected_path: Option<String>,
454 #[serde(default, skip_serializing_if = "Option::is_none")]
455 pub label: Option<String>,
456 #[serde(default, skip_serializing_if = "Option::is_none")]
457 pub description: Option<String>,
458 #[serde(default, skip_serializing_if = "Option::is_none")]
459 pub disabled: Option<bool>,
460 #[serde(default, skip_serializing_if = "Option::is_none")]
461 pub error: Option<String>,
462}
463
464#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
466pub struct SwitchProps {
467 pub field: String,
469 pub label: String,
470 #[serde(default, skip_serializing_if = "Option::is_none")]
471 pub description: Option<String>,
472 #[serde(default, skip_serializing_if = "Option::is_none")]
473 pub checked: Option<bool>,
474 #[serde(default, skip_serializing_if = "Option::is_none")]
476 pub data_path: Option<String>,
477 #[serde(default, skip_serializing_if = "Option::is_none")]
478 pub required: Option<bool>,
479 #[serde(default, skip_serializing_if = "Option::is_none")]
480 pub disabled: Option<bool>,
481 #[serde(default, skip_serializing_if = "Option::is_none")]
482 pub error: Option<String>,
483 #[serde(default, skip_serializing_if = "Option::is_none")]
486 pub action: Option<Action>,
487 #[serde(default, skip_serializing_if = "Option::is_none")]
490 pub compact: Option<bool>,
491}
492
493#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
495pub struct SeparatorProps {
496 #[serde(default, skip_serializing_if = "Option::is_none")]
497 pub orientation: Option<Orientation>,
498}
499
500#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
502pub struct DescriptionItem {
503 pub label: String,
504 pub value: String,
505 #[serde(default, skip_serializing_if = "Option::is_none")]
506 pub format: Option<ColumnFormat>,
507}
508
509#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
511pub struct DescriptionListProps {
512 #[serde(default, skip_serializing_if = "Vec::is_empty")]
513 pub items: Vec<DescriptionItem>,
514 #[serde(default, skip_serializing_if = "Option::is_none")]
515 pub columns: Option<u8>,
516 #[serde(default, skip_serializing_if = "Option::is_none")]
520 pub data_path: Option<String>,
521}
522
523#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
525pub struct Tab {
526 pub value: String,
527 pub label: String,
528 #[serde(default, skip_serializing_if = "Vec::is_empty")]
530 pub children: Vec<String>,
531}
532
533#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
535pub struct TabsProps {
536 pub default_tab: String,
537 pub tabs: Vec<Tab>,
538}
539
540#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
542pub struct BreadcrumbItem {
543 pub label: String,
544 #[serde(default, skip_serializing_if = "Option::is_none")]
545 pub url: Option<String>,
546}
547
548#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
550pub struct BreadcrumbProps {
551 pub items: Vec<BreadcrumbItem>,
552}
553
554#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
556pub struct PaginationProps {
557 pub current_page: u32,
558 pub per_page: u32,
559 pub total: u32,
560 #[serde(default, skip_serializing_if = "Option::is_none")]
561 pub base_url: Option<String>,
562}
563
564#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
566pub struct ProgressProps {
567 pub value: u8,
569 #[serde(default, skip_serializing_if = "Option::is_none")]
570 pub max: Option<u8>,
571 #[serde(default, skip_serializing_if = "Option::is_none")]
572 pub label: Option<String>,
573}
574
575#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
577pub struct ImageProps {
578 #[serde(default)]
579 pub src: String,
580 pub alt: String,
581 #[serde(default, skip_serializing_if = "Option::is_none")]
582 pub aspect_ratio: Option<String>,
583 #[serde(default, skip_serializing_if = "Option::is_none")]
588 pub placeholder_label: Option<String>,
589 #[serde(default, skip_serializing_if = "Option::is_none")]
597 pub inline_svg: Option<String>,
598 #[serde(default, skip_serializing_if = "Option::is_none")]
602 pub data_path: Option<String>,
603}
604
605impl ImageProps {
606 pub fn inline_svg(svg: impl Into<String>, alt: impl Into<String>) -> Self {
612 Self {
613 src: String::new(),
614 alt: alt.into(),
615 aspect_ratio: None,
616 placeholder_label: None,
617 inline_svg: Some(svg.into()),
618 data_path: None,
619 }
620 }
621}
622
623#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
625pub struct AvatarProps {
626 #[serde(default, skip_serializing_if = "Option::is_none")]
627 pub src: Option<String>,
628 pub alt: String,
629 #[serde(default, skip_serializing_if = "Option::is_none")]
630 pub fallback: Option<String>,
631 #[serde(default, skip_serializing_if = "Option::is_none")]
632 pub size: Option<Size>,
633}
634
635#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
637pub struct SkeletonProps {
638 #[serde(default, skip_serializing_if = "Option::is_none")]
639 pub width: Option<String>,
640 #[serde(default, skip_serializing_if = "Option::is_none")]
641 pub height: Option<String>,
642 #[serde(default, skip_serializing_if = "Option::is_none")]
643 pub rounded: Option<bool>,
644}
645
646#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
663pub struct RawHtmlProps {
664 #[serde(default)]
666 pub html: String,
667}
668
669#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
675pub struct StreamTextProps {
676 #[serde(default)]
679 pub sse_url: String,
680 #[serde(default, skip_serializing_if = "Option::is_none")]
682 pub placeholder: Option<String>,
683 #[serde(default, skip_serializing_if = "Option::is_none")]
685 pub loading_text: Option<String>,
686}
687
688#[derive(
690 Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, strum::AsRefStr,
691)]
692#[serde(rename_all = "snake_case")]
693#[strum(serialize_all = "snake_case")]
694pub enum ToastVariant {
695 #[default]
696 Info,
697 Success,
698 Warning,
699 Error,
700}
701
702#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
704pub struct ChecklistItem {
705 pub label: String,
706 #[serde(default)]
707 pub checked: bool,
708 #[serde(default, skip_serializing_if = "Option::is_none")]
709 pub href: Option<String>,
710}
711
712#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
714pub struct NotificationItem {
715 #[serde(default, skip_serializing_if = "Option::is_none")]
716 pub icon: Option<String>,
717 pub text: String,
718 #[serde(default, skip_serializing_if = "Option::is_none")]
719 pub timestamp: Option<String>,
720 #[serde(default)]
721 pub read: bool,
722 #[serde(default, skip_serializing_if = "Option::is_none")]
723 pub action_url: Option<String>,
724}
725
726#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
728pub struct SidebarNavItem {
729 pub label: String,
730 pub href: String,
731 #[serde(default, skip_serializing_if = "Option::is_none")]
732 pub icon: Option<String>,
733 #[serde(default)]
734 pub active: bool,
735 #[serde(default, skip_serializing_if = "Option::is_none")]
738 pub disabled: Option<bool>,
739}
740
741#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
743pub struct SidebarGroup {
744 pub label: String,
745 #[serde(default)]
746 pub collapsed: bool,
747 pub items: Vec<SidebarNavItem>,
748}
749
750#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
752pub struct StatCardProps {
753 pub label: String,
754 pub value: String,
755 #[serde(default, skip_serializing_if = "Option::is_none")]
756 pub icon: Option<String>,
757 #[serde(default, skip_serializing_if = "Option::is_none")]
758 pub subtitle: Option<String>,
759 #[serde(default, skip_serializing_if = "Option::is_none")]
761 pub sse_target: Option<String>,
762 #[serde(default, skip_serializing_if = "Option::is_none")]
767 pub value_path: Option<String>,
768}
769
770#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
772pub struct ChecklistProps {
773 pub title: String,
774 pub items: Vec<ChecklistItem>,
775 #[serde(default = "default_true")]
776 pub dismissible: bool,
777 #[serde(default, skip_serializing_if = "Option::is_none")]
778 pub dismiss_label: Option<String>,
779 #[serde(default, skip_serializing_if = "Option::is_none")]
781 pub data_key: Option<String>,
782}
783
784fn default_true() -> bool {
785 true
786}
787
788#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
793pub struct ToastProps {
794 pub message: String,
795 #[serde(default)]
796 pub variant: ToastVariant,
797 #[serde(default, skip_serializing_if = "Option::is_none")]
799 pub timeout: Option<u32>,
800 #[serde(default = "default_true")]
801 pub dismissible: bool,
802}
803
804#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
806pub struct NotificationDropdownProps {
807 pub notifications: Vec<NotificationItem>,
808 #[serde(default, skip_serializing_if = "Option::is_none")]
809 pub empty_text: Option<String>,
810}
811
812#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
814pub struct SidebarProps {
815 #[serde(default, skip_serializing_if = "Vec::is_empty")]
816 pub fixed_top: Vec<SidebarNavItem>,
817 #[serde(default, skip_serializing_if = "Vec::is_empty")]
818 pub groups: Vec<SidebarGroup>,
819 #[serde(default, skip_serializing_if = "Vec::is_empty")]
820 pub fixed_bottom: Vec<SidebarNavItem>,
821}
822
823#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
825pub struct HeaderProps {
826 pub business_name: String,
827 #[serde(default, skip_serializing_if = "Option::is_none")]
829 pub notification_count: Option<u32>,
830 #[serde(default, skip_serializing_if = "Option::is_none")]
831 pub user_name: Option<String>,
832 #[serde(default, skip_serializing_if = "Option::is_none")]
833 pub user_avatar: Option<String>,
834 #[serde(default, skip_serializing_if = "Option::is_none")]
835 pub logout_url: Option<String>,
836}
837
838#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
840#[serde(rename_all = "snake_case")]
841pub enum GapSize {
842 None,
843 Sm,
844 #[default]
845 Md,
846 Lg,
847 Xl,
848}
849
850#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
852pub struct GridProps {
853 #[serde(default = "default_grid_columns")]
855 pub columns: u8,
856 #[serde(default, skip_serializing_if = "Option::is_none")]
858 pub md_columns: Option<u8>,
859 #[serde(default, skip_serializing_if = "Option::is_none")]
861 pub lg_columns: Option<u8>,
862 #[serde(default)]
864 pub gap: GapSize,
865 #[serde(default, skip_serializing_if = "Option::is_none")]
868 pub scrollable: Option<bool>,
869}
870
871fn default_grid_columns() -> u8 {
872 2
873}
874
875#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
877pub struct CollapsibleProps {
878 pub title: String,
879 #[serde(default)]
880 pub expanded: bool,
881}
882
883#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
885pub struct EmptyStateProps {
886 pub title: String,
887 #[serde(default, skip_serializing_if = "Option::is_none")]
888 pub description: Option<String>,
889 #[serde(default, skip_serializing_if = "Option::is_none")]
890 pub action: Option<Action>,
891 #[serde(default, skip_serializing_if = "Option::is_none")]
892 pub action_label: Option<String>,
893}
894
895#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
897#[serde(rename_all = "snake_case")]
898pub enum FormSectionLayout {
899 #[default]
900 Stacked,
901 TwoColumn,
902}
903
904#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
906pub struct FormSectionProps {
907 pub title: String,
908 #[serde(default, skip_serializing_if = "Option::is_none")]
909 pub description: Option<String>,
910 #[serde(default, skip_serializing_if = "Option::is_none")]
912 pub layout: Option<FormSectionLayout>,
913}
914
915#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
917pub struct PageHeaderProps {
918 pub title: String,
919 #[serde(default, skip_serializing_if = "Vec::is_empty")]
920 pub breadcrumb: Vec<BreadcrumbItem>,
921 #[serde(
923 default,
924 deserialize_with = "deserialize_actions_lax",
925 skip_serializing_if = "Vec::is_empty"
926 )]
927 pub actions: Vec<String>,
928}
929
930#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Default)]
932pub struct ButtonGroupProps {
933 #[serde(default)]
935 pub gap: GapSize,
936}
937
938#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
947pub struct DetailPageProps {
948 pub title: String,
949 #[serde(default, skip_serializing_if = "Vec::is_empty")]
950 pub breadcrumb: Vec<BreadcrumbItem>,
951 #[serde(
953 default,
954 deserialize_with = "deserialize_actions_lax",
955 skip_serializing_if = "Vec::is_empty"
956 )]
957 pub actions: Vec<String>,
958 #[serde(default, skip_serializing_if = "Vec::is_empty")]
961 pub info: Vec<String>,
962}
963
964#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
966pub struct DropdownMenuAction {
967 pub label: String,
968 pub action: Action,
969 #[serde(default)]
970 pub destructive: bool,
971 #[serde(default, skip_serializing_if = "Option::is_none")]
978 pub visible_if: Option<String>,
979}
980
981#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
983pub struct DropdownMenuProps {
984 pub menu_id: String,
985 pub trigger_label: String,
986 pub items: Vec<DropdownMenuAction>,
987 #[serde(default, skip_serializing_if = "Option::is_none")]
988 pub trigger_variant: Option<ButtonVariant>,
989}
990
991#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
994pub struct DataTableProps {
995 pub columns: Vec<Column>,
996 pub data_path: String,
997 #[serde(default, skip_serializing_if = "Option::is_none")]
998 pub row_actions: Option<Vec<DropdownMenuAction>>,
999 #[serde(default, skip_serializing_if = "Option::is_none")]
1000 pub empty_message: Option<String>,
1001 #[serde(default, skip_serializing_if = "Option::is_none")]
1002 pub row_key: Option<String>,
1003 #[serde(default, skip_serializing_if = "Option::is_none")]
1005 pub row_href: Option<String>,
1006}
1007
1008#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1012pub struct MediaCardGridProps {
1013 pub data_path: String,
1014 pub title_key: String,
1016 #[serde(default, skip_serializing_if = "Option::is_none")]
1018 pub description_key: Option<String>,
1019 #[serde(default, skip_serializing_if = "Option::is_none")]
1021 pub image_key: Option<String>,
1022 #[serde(default, skip_serializing_if = "Option::is_none")]
1024 pub image_href_key: Option<String>,
1025 #[serde(default, skip_serializing_if = "Option::is_none")]
1027 pub image_aspect_ratio: Option<String>,
1028 #[serde(default, skip_serializing_if = "Option::is_none")]
1031 pub image_position: Option<String>,
1032 #[serde(default, skip_serializing_if = "Option::is_none")]
1034 pub badge_key: Option<String>,
1035 #[serde(default, skip_serializing_if = "Option::is_none")]
1037 pub badge_variant_key: Option<String>,
1038 #[serde(default, skip_serializing_if = "Option::is_none")]
1040 pub row_key: Option<String>,
1041 #[serde(default, skip_serializing_if = "Option::is_none")]
1042 pub row_actions: Option<Vec<DropdownMenuAction>>,
1043 #[serde(default, skip_serializing_if = "Option::is_none")]
1044 pub empty_message: Option<String>,
1045 #[serde(default, skip_serializing_if = "Option::is_none")]
1047 pub columns: Option<u8>,
1048}
1049
1050#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1058pub struct KanbanColumnProps {
1059 pub id: String,
1060 pub title: String,
1061 #[serde(default)]
1062 pub count: u32,
1063 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1065 pub children: Vec<String>,
1066}
1067
1068#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1081pub struct KanbanBoardProps {
1082 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1084 pub columns: Vec<KanbanColumnProps>,
1085 #[serde(default, skip_serializing_if = "Option::is_none")]
1088 pub items_path: Option<String>,
1089 #[serde(default, skip_serializing_if = "Option::is_none")]
1091 pub group_by: Option<String>,
1092 #[serde(default, skip_serializing_if = "Option::is_none")]
1094 pub card_title_key: Option<String>,
1095 #[serde(default, skip_serializing_if = "Option::is_none")]
1097 pub card_description_key: Option<String>,
1098 #[serde(default, skip_serializing_if = "Option::is_none")]
1101 pub row_actions: Option<Vec<DropdownMenuAction>>,
1102 #[serde(default, skip_serializing_if = "Option::is_none")]
1105 pub row_key: Option<String>,
1106 #[serde(default, skip_serializing_if = "Option::is_none")]
1107 pub mobile_default_column: Option<String>,
1108 #[serde(default, skip_serializing_if = "Option::is_none")]
1112 pub empty_label: Option<String>,
1113}
1114
1115#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1120pub struct CalendarCellProps {
1121 pub day: u8,
1122 #[serde(default)]
1123 pub is_today: bool,
1124 #[serde(default)]
1125 pub is_current_month: bool,
1126 #[serde(default)]
1127 pub event_count: u32,
1128 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1131 pub dot_colors: Vec<String>,
1132}
1133
1134#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1136#[serde(rename_all = "snake_case")]
1137pub enum ActionCardVariant {
1138 #[default]
1139 Default,
1140 Setup,
1141 Danger,
1142}
1143
1144#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1149pub struct ActionCardProps {
1150 pub title: String,
1151 pub description: String,
1152 #[serde(default, skip_serializing_if = "Option::is_none")]
1153 pub icon: Option<String>,
1154 #[serde(default)]
1155 pub variant: ActionCardVariant,
1156 #[serde(default, skip_serializing_if = "Option::is_none")]
1158 pub href: Option<String>,
1159}
1160
1161#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1166pub struct ProductTileProps {
1167 pub product_id: String,
1168 pub name: String,
1169 pub price: String,
1170 pub field: String,
1171 #[serde(default, skip_serializing_if = "Option::is_none")]
1172 pub default_quantity: Option<u32>,
1173}
1174
1175fn deserialize_actions_lax<'de, D: serde::Deserializer<'de>>(
1181 d: D,
1182) -> Result<Vec<String>, D::Error> {
1183 use serde::de::Error;
1184 let v = serde_json::Value::deserialize(d)?;
1185 match v {
1186 serde_json::Value::Null => Ok(Vec::new()),
1187 serde_json::Value::String(s) if s.is_empty() => Ok(Vec::new()),
1188 serde_json::Value::Array(arr) => arr
1189 .into_iter()
1190 .map(|item| {
1191 item.as_str()
1192 .map(String::from)
1193 .ok_or_else(|| D::Error::custom("PageHeader.actions: expected string in array"))
1194 })
1195 .collect(),
1196 other => Err(D::Error::custom(format!(
1197 "PageHeader.actions: expected null, empty string, or array of strings; got {other:?}"
1198 ))),
1199 }
1200}
1201
1202#[cfg(test)]
1203mod schema_smoke_tests {
1204 use super::*;
1215
1216 fn assert_schema_nonempty_object<T: schemars::JsonSchema>(type_label: &str) {
1217 let schema = schemars::schema_for!(T);
1218 let value = serde_json::to_value(&schema).expect("schema serializes to JSON");
1219 assert!(
1220 value.is_object(),
1221 "{type_label}: schema must be a JSON object"
1222 );
1223 let props = value
1224 .get("properties")
1225 .and_then(|p| p.as_object())
1226 .map(|o| !o.is_empty())
1227 .unwrap_or(false);
1228 assert!(
1229 props,
1230 "{type_label}: schema must have a non-empty `properties` field"
1231 );
1232 }
1233
1234 #[test]
1235 fn schema_for_card_props_generates() {
1236 assert_schema_nonempty_object::<CardProps>("CardProps");
1237 }
1238
1239 #[test]
1240 fn schema_for_table_props_generates() {
1241 assert_schema_nonempty_object::<TableProps>("TableProps");
1242 }
1243
1244 #[test]
1245 fn schema_for_form_props_generates() {
1246 assert_schema_nonempty_object::<FormProps>("FormProps");
1247 }
1248
1249 #[test]
1250 fn schema_for_button_props_generates() {
1251 assert_schema_nonempty_object::<ButtonProps>("ButtonProps");
1252 }
1253
1254 #[test]
1255 fn schema_for_input_props_generates() {
1256 assert_schema_nonempty_object::<InputProps>("InputProps");
1257 }
1258
1259 #[test]
1260 fn schema_for_select_props_generates() {
1261 assert_schema_nonempty_object::<SelectProps>("SelectProps");
1262 }
1263
1264 #[test]
1265 fn schema_for_alert_props_generates() {
1266 assert_schema_nonempty_object::<AlertProps>("AlertProps");
1267 }
1268
1269 #[test]
1270 fn schema_for_badge_props_generates() {
1271 assert_schema_nonempty_object::<BadgeProps>("BadgeProps");
1272 }
1273
1274 #[test]
1275 fn schema_for_modal_props_generates() {
1276 assert_schema_nonempty_object::<ModalProps>("ModalProps");
1277 }
1278
1279 #[test]
1280 fn schema_for_text_props_generates() {
1281 assert_schema_nonempty_object::<TextProps>("TextProps");
1282 }
1283
1284 #[test]
1285 fn schema_for_checkbox_props_generates() {
1286 assert_schema_nonempty_object::<CheckboxProps>("CheckboxProps");
1287 }
1288
1289 #[test]
1290 fn schema_for_switch_props_generates() {
1291 assert_schema_nonempty_object::<SwitchProps>("SwitchProps");
1292 }
1293
1294 #[test]
1295 fn schema_for_separator_props_generates() {
1296 assert_schema_nonempty_object::<SeparatorProps>("SeparatorProps");
1297 }
1298
1299 #[test]
1300 fn schema_for_description_list_props_generates() {
1301 assert_schema_nonempty_object::<DescriptionListProps>("DescriptionListProps");
1302 }
1303
1304 #[test]
1305 fn schema_for_tab_generates() {
1306 assert_schema_nonempty_object::<Tab>("Tab");
1307 }
1308
1309 #[test]
1310 fn schema_for_tabs_props_generates() {
1311 assert_schema_nonempty_object::<TabsProps>("TabsProps");
1312 }
1313
1314 #[test]
1315 fn schema_for_breadcrumb_props_generates() {
1316 assert_schema_nonempty_object::<BreadcrumbProps>("BreadcrumbProps");
1317 }
1318
1319 #[test]
1320 fn schema_for_pagination_props_generates() {
1321 assert_schema_nonempty_object::<PaginationProps>("PaginationProps");
1322 }
1323
1324 #[test]
1325 fn schema_for_progress_props_generates() {
1326 assert_schema_nonempty_object::<ProgressProps>("ProgressProps");
1327 }
1328
1329 #[test]
1330 fn schema_for_image_props_generates() {
1331 assert_schema_nonempty_object::<ImageProps>("ImageProps");
1332 }
1333
1334 #[test]
1335 fn image_inline_svg_factory_roundtrips_via_serde() {
1336 let p = ImageProps::inline_svg("<svg/>", "alt");
1337 let json = serde_json::to_value(&p).expect("serialization must not fail");
1338 let parsed: ImageProps =
1339 serde_json::from_value(json).expect("deserialization must not fail");
1340 assert_eq!(parsed.inline_svg, Some("<svg/>".to_string()));
1341 assert_eq!(parsed.alt, "alt");
1342 assert_eq!(parsed.src, "");
1343 }
1344
1345 #[test]
1346 fn schema_for_avatar_props_generates() {
1347 assert_schema_nonempty_object::<AvatarProps>("AvatarProps");
1348 }
1349
1350 #[test]
1351 fn schema_for_skeleton_props_generates() {
1352 assert_schema_nonempty_object::<SkeletonProps>("SkeletonProps");
1353 }
1354
1355 #[test]
1356 fn schema_for_stat_card_props_generates() {
1357 assert_schema_nonempty_object::<StatCardProps>("StatCardProps");
1358 }
1359
1360 #[test]
1361 fn schema_for_checklist_props_generates() {
1362 assert_schema_nonempty_object::<ChecklistProps>("ChecklistProps");
1363 }
1364
1365 #[test]
1366 fn schema_for_toast_props_generates() {
1367 assert_schema_nonempty_object::<ToastProps>("ToastProps");
1368 }
1369
1370 #[test]
1371 fn schema_for_notification_dropdown_props_generates() {
1372 assert_schema_nonempty_object::<NotificationDropdownProps>("NotificationDropdownProps");
1373 }
1374
1375 #[test]
1376 fn schema_for_sidebar_props_generates() {
1377 assert_schema_nonempty_object::<SidebarProps>("SidebarProps");
1378 }
1379
1380 #[test]
1381 fn schema_for_header_props_generates() {
1382 assert_schema_nonempty_object::<HeaderProps>("HeaderProps");
1383 }
1384
1385 #[test]
1386 fn schema_for_grid_props_generates() {
1387 assert_schema_nonempty_object::<GridProps>("GridProps");
1388 }
1389
1390 #[test]
1391 fn schema_for_collapsible_props_generates() {
1392 assert_schema_nonempty_object::<CollapsibleProps>("CollapsibleProps");
1393 }
1394
1395 #[test]
1396 fn schema_for_empty_state_props_generates() {
1397 assert_schema_nonempty_object::<EmptyStateProps>("EmptyStateProps");
1398 }
1399
1400 #[test]
1401 fn schema_for_form_section_props_generates() {
1402 assert_schema_nonempty_object::<FormSectionProps>("FormSectionProps");
1403 }
1404
1405 #[test]
1406 fn schema_for_page_header_props_generates() {
1407 assert_schema_nonempty_object::<PageHeaderProps>("PageHeaderProps");
1408 }
1409
1410 #[test]
1411 fn schema_for_button_group_props_generates() {
1412 assert_schema_nonempty_object::<ButtonGroupProps>("ButtonGroupProps");
1413 }
1414
1415 #[test]
1416 fn schema_for_dropdown_menu_action_generates() {
1417 assert_schema_nonempty_object::<DropdownMenuAction>("DropdownMenuAction");
1418 }
1419
1420 #[test]
1421 fn schema_for_dropdown_menu_props_generates() {
1422 assert_schema_nonempty_object::<DropdownMenuProps>("DropdownMenuProps");
1423 }
1424
1425 #[test]
1426 fn schema_for_data_table_props_generates() {
1427 assert_schema_nonempty_object::<DataTableProps>("DataTableProps");
1428 }
1429
1430 #[test]
1431 fn schema_for_kanban_column_props_generates() {
1432 assert_schema_nonempty_object::<KanbanColumnProps>("KanbanColumnProps");
1433 }
1434
1435 #[test]
1436 fn schema_for_kanban_board_props_generates() {
1437 assert_schema_nonempty_object::<KanbanBoardProps>("KanbanBoardProps");
1438 }
1439
1440 #[test]
1441 fn schema_for_calendar_cell_props_generates() {
1442 assert_schema_nonempty_object::<CalendarCellProps>("CalendarCellProps");
1443 }
1444
1445 #[test]
1446 fn schema_for_action_card_props_generates() {
1447 assert_schema_nonempty_object::<ActionCardProps>("ActionCardProps");
1448 }
1449
1450 #[test]
1451 fn schema_for_product_tile_props_generates() {
1452 assert_schema_nonempty_object::<ProductTileProps>("ProductTileProps");
1453 }
1454
1455 #[test]
1456 fn card_props_round_trips_footer() {
1457 let original = CardProps {
1458 title: "Hero".to_string(),
1459 description: None,
1460 subtitle: None,
1461 badge: None,
1462 max_width: None,
1463 footer: vec!["btn1".to_string(), "btn2".to_string()],
1464 variant: CardVariant::Bordered,
1465 };
1466 let json = serde_json::to_string(&original).unwrap();
1467 let parsed: CardProps = serde_json::from_str(&json).unwrap();
1468 assert_eq!(original.footer, parsed.footer);
1469 }
1470
1471 #[test]
1472 fn tab_round_trips_children() {
1473 let original = Tab {
1474 value: "overview".to_string(),
1475 label: "Overview".to_string(),
1476 children: vec!["panel1".to_string()],
1477 };
1478 let json = serde_json::to_string(&original).unwrap();
1479 let parsed: Tab = serde_json::from_str(&json).unwrap();
1480 assert_eq!(original.children, parsed.children);
1481 }
1482
1483 #[test]
1484 fn card_props_omits_empty_footer_in_json() {
1485 let card = CardProps {
1486 title: "Card".to_string(),
1487 description: None,
1488 subtitle: None,
1489 badge: None,
1490 max_width: None,
1491 footer: Vec::new(),
1492 variant: CardVariant::Bordered,
1493 };
1494 let json = serde_json::to_string(&card).unwrap();
1495 assert!(
1496 !json.contains("\"footer\""),
1497 "empty footer must be skipped, got: {json}"
1498 );
1499 }
1500
1501 #[test]
1502 fn card_props_round_trips_badge() {
1503 let original = CardProps {
1504 title: "Hero".to_string(),
1505 description: None,
1506 subtitle: None,
1507 badge: Some("Scade tra 9m".to_string()),
1508 max_width: None,
1509 footer: Vec::new(),
1510 variant: CardVariant::Bordered,
1511 };
1512 let json = serde_json::to_string(&original).unwrap();
1513 let parsed: CardProps = serde_json::from_str(&json).unwrap();
1514 assert_eq!(original.badge, parsed.badge);
1515 }
1516
1517 #[test]
1518 fn card_props_omits_empty_badge_in_json() {
1519 let card = CardProps {
1520 title: "Card".to_string(),
1521 description: None,
1522 subtitle: None,
1523 badge: None,
1524 max_width: None,
1525 footer: Vec::new(),
1526 variant: CardVariant::Bordered,
1527 };
1528 let json = serde_json::to_string(&card).unwrap();
1529 assert!(
1530 !json.contains("\"badge\""),
1531 "empty badge must be skipped, got: {json}"
1532 );
1533 }
1534
1535 #[test]
1536 fn card_props_round_trips_subtitle() {
1537 let original = CardProps {
1538 title: "Hero".to_string(),
1539 description: None,
1540 subtitle: Some("Marco Rossi".to_string()),
1541 badge: None,
1542 max_width: None,
1543 footer: Vec::new(),
1544 variant: CardVariant::Bordered,
1545 };
1546 let json = serde_json::to_string(&original).unwrap();
1547 let parsed: CardProps = serde_json::from_str(&json).unwrap();
1548 assert_eq!(original.subtitle, parsed.subtitle);
1549 }
1550
1551 #[test]
1552 fn card_props_omits_empty_subtitle_in_json() {
1553 let card = CardProps {
1554 title: "Card".to_string(),
1555 description: None,
1556 subtitle: None,
1557 badge: None,
1558 max_width: None,
1559 footer: Vec::new(),
1560 variant: CardVariant::Bordered,
1561 };
1562 let json = serde_json::to_string(&card).unwrap();
1563 assert!(
1564 !json.contains("\"subtitle\""),
1565 "empty subtitle must be skipped, got: {json}"
1566 );
1567 }
1568
1569 #[test]
1570 fn card_props_schema_includes_badge() {
1571 let schema = schemars::schema_for!(CardProps);
1572 let value = serde_json::to_value(&schema).expect("schema serializes to JSON");
1573 let props = value
1574 .get("properties")
1575 .and_then(|p| p.as_object())
1576 .expect("schema has a properties object");
1577 assert!(
1578 props.contains_key("badge"),
1579 "CardProps schema must expose a `badge` property; got keys: {:?}",
1580 props.keys().collect::<Vec<_>>()
1581 );
1582 let badge_schema = props.get("badge").expect("badge entry");
1587 let badge_json = badge_schema.to_string();
1588 assert!(
1589 badge_json.contains("\"string\""),
1590 "badge schema entry must mention string type; got: {badge_json}"
1591 );
1592 }
1593
1594 #[test]
1595 fn card_props_schema_includes_subtitle() {
1596 let schema = schemars::schema_for!(CardProps);
1597 let value = serde_json::to_value(&schema).expect("schema serializes to JSON");
1598 let props = value
1599 .get("properties")
1600 .and_then(|p| p.as_object())
1601 .expect("schema has a properties object");
1602 assert!(
1603 props.contains_key("subtitle"),
1604 "CardProps schema must expose a `subtitle` property; got keys: {:?}",
1605 props.keys().collect::<Vec<_>>()
1606 );
1607 let subtitle_schema = props.get("subtitle").expect("subtitle entry");
1612 let subtitle_json = subtitle_schema.to_string();
1613 assert!(
1614 subtitle_json.contains("\"string\""),
1615 "subtitle schema entry must mention string type; got: {subtitle_json}"
1616 );
1617 }
1618
1619 #[test]
1620 fn schema_for_checkbox_list_props_generates() {
1621 assert_schema_nonempty_object::<CheckboxListProps>("CheckboxListProps");
1622 }
1623
1624 #[test]
1625 fn checkbox_list_props_serde_roundtrip() {
1626 let json = serde_json::json!({
1627 "field": "services",
1628 "options": [{"value": "a", "label": "Alpha"}, {"value": "b", "label": "Beta"}],
1629 "selected_path": "/preselected"
1630 });
1631 let parsed: CheckboxListProps = serde_json::from_value(json.clone()).expect("decode");
1632 assert_eq!(parsed.field, "services");
1633 assert_eq!(parsed.options.len(), 2);
1634 assert_eq!(parsed.selected_path.as_deref(), Some("/preselected"));
1635 let reserialized = serde_json::to_value(&parsed).expect("encode");
1636 assert!(reserialized.get("label").is_none());
1638 assert!(reserialized.get("disabled").is_none());
1639 }
1640
1641 #[test]
1642 fn schema_for_rich_text_editor_props_generates() {
1643 assert_schema_nonempty_object::<RichTextEditorProps>("RichTextEditorProps");
1644 }
1645
1646 #[test]
1647 fn rich_text_editor_props_serde_roundtrip() {
1648 let json = serde_json::json!({
1649 "field": "body",
1650 "label": "Body"
1651 });
1652 let parsed: RichTextEditorProps = serde_json::from_value(json).expect("decode");
1653 assert_eq!(parsed.field, "body");
1654 assert_eq!(parsed.label, "Body");
1655 assert!(parsed.placeholder.is_none());
1656 assert!(parsed.default_value.is_none());
1657 assert!(parsed.data_path.is_none());
1658 assert!(parsed.error.is_none());
1659 let reserialized = serde_json::to_value(&parsed).expect("encode");
1660 assert!(reserialized.get("placeholder").is_none());
1662 assert!(reserialized.get("error").is_none());
1663 }
1664}
1665
1666#[cfg(test)]
1667mod strum_tests {
1668 use super::*;
1669
1670 #[test]
1674 fn variant_enums_strum_matches_serde_wire_format() {
1675 fn check<T: AsRef<str> + serde::Serialize>(variants: &[T], label: &str) {
1676 for v in variants {
1677 let json = serde_json::to_string(v).expect("serialize");
1678 let json_stripped = json.trim_matches('"');
1679 assert_eq!(
1680 v.as_ref(),
1681 json_stripped,
1682 "strum AsRefStr drifted from serde for {label} variant"
1683 );
1684 }
1685 }
1686 check(
1687 &[
1688 AlertVariant::Info,
1689 AlertVariant::Success,
1690 AlertVariant::Warning,
1691 AlertVariant::Error,
1692 ],
1693 "AlertVariant",
1694 );
1695 check(
1696 &[
1697 BadgeVariant::Default,
1698 BadgeVariant::Secondary,
1699 BadgeVariant::Destructive,
1700 BadgeVariant::Outline,
1701 ],
1702 "BadgeVariant",
1703 );
1704 check(
1705 &[
1706 ButtonVariant::Default,
1707 ButtonVariant::Secondary,
1708 ButtonVariant::Destructive,
1709 ButtonVariant::Outline,
1710 ButtonVariant::Ghost,
1711 ButtonVariant::Link,
1712 ],
1713 "ButtonVariant",
1714 );
1715 check(
1716 &[
1717 ToastVariant::Info,
1718 ToastVariant::Success,
1719 ToastVariant::Warning,
1720 ToastVariant::Error,
1721 ],
1722 "ToastVariant",
1723 );
1724 }
1725
1726 #[test]
1727 fn alert_variant_as_ref_str_matches_wire_format() {
1728 assert_eq!(AlertVariant::Success.as_ref(), "success");
1729 assert_eq!(AlertVariant::Warning.as_ref(), "warning");
1730 assert_eq!(AlertVariant::Info.as_ref(), "info");
1731 assert_eq!(AlertVariant::Error.as_ref(), "error");
1732 }
1733}
1734
1735#[cfg(test)]
1736mod card_variant_tests {
1737 use super::*;
1738
1739 #[test]
1740 fn card_variant_default_is_bordered() {
1741 assert_eq!(CardVariant::default(), CardVariant::Bordered);
1742 }
1743
1744 #[test]
1745 fn card_variant_serializes_snake_case() {
1746 assert_eq!(
1747 serde_json::to_value(CardVariant::Bordered).unwrap(),
1748 serde_json::json!("bordered")
1749 );
1750 assert_eq!(
1751 serde_json::to_value(CardVariant::Elevated).unwrap(),
1752 serde_json::json!("elevated")
1753 );
1754 }
1755
1756 #[test]
1757 fn card_variant_deserializes_snake_case() {
1758 assert_eq!(
1759 serde_json::from_value::<CardVariant>(serde_json::json!("bordered")).unwrap(),
1760 CardVariant::Bordered
1761 );
1762 assert_eq!(
1763 serde_json::from_value::<CardVariant>(serde_json::json!("elevated")).unwrap(),
1764 CardVariant::Elevated
1765 );
1766 }
1767
1768 #[test]
1769 fn card_props_without_variant_defaults_to_bordered() {
1770 let v = serde_json::json!({"title": "x"});
1771 let p: CardProps = serde_json::from_value(v).unwrap();
1772 assert_eq!(p.variant, CardVariant::Bordered);
1773 }
1774
1775 #[test]
1776 fn card_props_with_elevated_variant() {
1777 let v = serde_json::json!({"title": "x", "variant": "elevated"});
1778 let p: CardProps = serde_json::from_value(v).unwrap();
1779 assert_eq!(p.variant, CardVariant::Elevated);
1780 }
1781
1782 #[test]
1783 fn card_props_roundtrip_preserves_variant() {
1784 let p = CardProps {
1785 title: "x".into(),
1786 description: None,
1787 subtitle: None,
1788 badge: None,
1789 max_width: None,
1790 footer: vec![],
1791 variant: CardVariant::Elevated,
1792 };
1793 let j = serde_json::to_value(&p).unwrap();
1794 let back: CardProps = serde_json::from_value(j).unwrap();
1795 assert_eq!(back.variant, CardVariant::Elevated);
1796 }
1797}
1798
1799#[cfg(test)]
1800mod kanban_board_props_tests {
1801 use super::*;
1802
1803 #[test]
1804 fn kanban_board_props_serde_static_columns() {
1805 let v = serde_json::json!({
1806 "columns": [{"title": "To Do", "id": "todo", "count": 0}]
1807 });
1808 let p: KanbanBoardProps = serde_json::from_value(v).unwrap();
1809 assert_eq!(p.columns.len(), 1);
1810 assert!(p.items_path.is_none());
1811 assert!(p.group_by.is_none());
1812 }
1813
1814 #[test]
1815 fn kanban_board_props_serde_data_bound() {
1816 let v = serde_json::json!({
1817 "columns": [{"title": "Open", "id": "open"}],
1818 "items_path": "/data/order",
1819 "group_by": "status",
1820 "card_title_key": "name"
1821 });
1822 let p: KanbanBoardProps = serde_json::from_value(v).unwrap();
1823 assert_eq!(p.columns.len(), 1);
1824 assert_eq!(p.items_path.as_deref(), Some("/data/order"));
1825 assert_eq!(p.group_by.as_deref(), Some("status"));
1826 assert_eq!(p.card_title_key.as_deref(), Some("name"));
1827 }
1828
1829 #[test]
1830 fn kanban_board_props_serde_neither() {
1831 let v = serde_json::json!({});
1832 let p: KanbanBoardProps = serde_json::from_value(v).unwrap();
1833 assert!(p.columns.is_empty());
1834 assert!(p.items_path.is_none());
1835 assert!(p.group_by.is_none());
1836 }
1837
1838 #[test]
1839 fn kanban_board_props_empty_columns_skipped_on_serialize() {
1840 let p = KanbanBoardProps {
1841 columns: vec![],
1842 items_path: Some("/data/order".into()),
1843 group_by: Some("status".into()),
1844 card_title_key: None,
1845 card_description_key: None,
1846 row_actions: None,
1847 row_key: None,
1848 mobile_default_column: None,
1849 empty_label: None,
1850 };
1851 let j = serde_json::to_value(&p).unwrap();
1852 assert!(
1853 j.get("columns").is_none(),
1854 "empty columns must be skipped, got: {j}"
1855 );
1856 assert_eq!(
1857 j.get("items_path").and_then(|v| v.as_str()),
1858 Some("/data/order")
1859 );
1860 }
1861}
1862
1863#[cfg(test)]
1864mod page_header_actions_tests {
1865 use super::*;
1866
1867 #[test]
1868 fn page_header_actions_missing_field() {
1869 let v = serde_json::json!({"title": "X"});
1870 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
1871 assert!(p.actions.is_empty());
1872 }
1873
1874 #[test]
1875 fn page_header_actions_null() {
1876 let v = serde_json::json!({"title": "X", "actions": null});
1877 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
1878 assert!(p.actions.is_empty());
1879 }
1880
1881 #[test]
1882 fn page_header_actions_empty_string() {
1883 let v = serde_json::json!({"title": "X", "actions": ""});
1884 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
1885 assert!(p.actions.is_empty());
1886 }
1887
1888 #[test]
1889 fn page_header_actions_empty_array() {
1890 let v = serde_json::json!({"title": "X", "actions": []});
1891 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
1892 assert!(p.actions.is_empty());
1893 }
1894
1895 #[test]
1896 fn page_header_actions_non_empty_array() {
1897 let v = serde_json::json!({"title": "X", "actions": ["a", "b"]});
1898 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
1899 assert_eq!(p.actions, vec!["a".to_string(), "b".to_string()]);
1900 }
1901
1902 #[test]
1903 fn page_header_actions_non_empty_string_rejected() {
1904 let v = serde_json::json!({"title": "X", "actions": "not-empty"});
1905 let result: Result<PageHeaderProps, _> = serde_json::from_value(v);
1906 assert!(result.is_err(), "non-empty string must be rejected");
1907 }
1908
1909 #[test]
1910 fn page_header_actions_non_string_array_rejected() {
1911 let v = serde_json::json!({"title": "X", "actions": [1, 2, 3]});
1912 let result: Result<PageHeaderProps, _> = serde_json::from_value(v);
1913 assert!(result.is_err(), "array of non-strings must be rejected");
1914 }
1915}