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 Warning,
112 Outline,
113}
114
115#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
117#[serde(rename_all = "snake_case")]
118pub enum TextElement {
119 #[default]
120 P,
121 H1,
122 H2,
123 H3,
124 Span,
125 Div,
126 Section,
127}
128
129#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
134#[serde(rename_all = "snake_case")]
135pub enum ColumnFormat {
136 Date,
137 DateTime,
138 Currency,
139 Boolean,
140 Badge,
141 Image,
143 Icon,
148}
149
150#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
152#[serde(rename_all = "snake_case")]
153pub enum ColumnAlign {
154 #[default]
155 Left,
156 Center,
157 Right,
158}
159
160#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
162pub struct Column {
163 pub key: String,
164 pub label: String,
165 #[serde(default, skip_serializing_if = "Option::is_none")]
166 pub format: Option<ColumnFormat>,
167 #[serde(default, skip_serializing_if = "Option::is_none")]
170 pub align: Option<ColumnAlign>,
171}
172
173#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
175pub struct SelectOption {
176 pub value: String,
177 pub label: String,
178}
179
180#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
187#[serde(rename_all = "snake_case")]
188pub enum CardVariant {
189 #[default]
190 Bordered,
191 Elevated,
192}
193
194#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
196pub struct CardProps {
197 pub title: String,
198 #[serde(default, skip_serializing_if = "Option::is_none")]
199 pub description: Option<String>,
200 #[serde(default, skip_serializing_if = "Option::is_none")]
204 pub subtitle: Option<String>,
205 #[serde(default, skip_serializing_if = "Option::is_none")]
209 pub badge: Option<String>,
210 #[serde(default, skip_serializing_if = "Option::is_none")]
211 pub max_width: Option<FormMaxWidth>,
212 #[serde(default, skip_serializing_if = "Vec::is_empty")]
214 pub footer: Vec<String>,
215 #[serde(default)]
216 pub variant: CardVariant,
217}
218
219#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
221pub struct TableProps {
222 pub columns: Vec<Column>,
223 pub data_path: String,
224 #[serde(default, skip_serializing_if = "Option::is_none")]
225 pub row_actions: Option<Vec<Action>>,
226 #[serde(default, skip_serializing_if = "Option::is_none")]
227 pub empty_message: Option<String>,
228 #[serde(default, skip_serializing_if = "Option::is_none")]
229 pub sortable: Option<bool>,
230 #[serde(default, skip_serializing_if = "Option::is_none")]
231 pub sort_column: Option<String>,
232 #[serde(default, skip_serializing_if = "Option::is_none")]
233 pub sort_direction: Option<SortDirection>,
234}
235
236#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
238#[serde(rename_all = "snake_case")]
239pub enum FormMaxWidth {
240 #[default]
241 Default,
242 Narrow,
243 Wide,
244}
245
246#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
248pub struct FormProps {
249 pub action: Action,
250 #[serde(default, skip_serializing_if = "Option::is_none")]
251 pub method: Option<crate::action::HttpMethod>,
252 #[serde(default, skip_serializing_if = "Option::is_none")]
256 pub guard: Option<String>,
257 #[serde(default, skip_serializing_if = "Option::is_none")]
259 pub max_width: Option<FormMaxWidth>,
260 #[serde(default, skip_serializing_if = "Option::is_none")]
264 pub id: Option<String>,
265 #[serde(default, skip_serializing_if = "Option::is_none")]
270 pub enctype: Option<String>,
271}
272
273#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
275#[serde(rename_all = "snake_case")]
276pub enum ButtonType {
277 #[default]
278 Button,
279 Submit,
280}
281
282#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
284pub struct ButtonProps {
285 pub label: String,
286 #[serde(default)]
287 pub variant: ButtonVariant,
288 #[serde(default)]
289 pub size: Size,
290 #[serde(default, skip_serializing_if = "Option::is_none")]
291 pub disabled: Option<bool>,
292 #[serde(default, skip_serializing_if = "Option::is_none")]
293 pub icon: Option<String>,
294 #[serde(default, skip_serializing_if = "Option::is_none")]
295 pub icon_position: Option<IconPosition>,
296 #[serde(default, skip_serializing_if = "Option::is_none")]
297 pub button_type: Option<ButtonType>,
298 #[serde(default, skip_serializing_if = "Option::is_none")]
302 pub form: Option<String>,
303}
304
305#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
307pub struct InputProps {
308 pub field: String,
310 pub label: String,
311 #[serde(default)]
312 pub input_type: InputType,
313 #[serde(default, skip_serializing_if = "Option::is_none")]
314 pub placeholder: Option<String>,
315 #[serde(default, skip_serializing_if = "Option::is_none")]
316 pub required: Option<bool>,
317 #[serde(default, skip_serializing_if = "Option::is_none")]
318 pub disabled: Option<bool>,
319 #[serde(default, skip_serializing_if = "Option::is_none")]
320 pub error: Option<String>,
321 #[serde(default, skip_serializing_if = "Option::is_none")]
322 pub description: Option<String>,
323 #[serde(default, skip_serializing_if = "Option::is_none")]
324 pub default_value: Option<String>,
325 #[serde(default, skip_serializing_if = "Option::is_none")]
327 pub data_path: Option<String>,
328 #[serde(default, skip_serializing_if = "Option::is_none")]
330 pub step: Option<String>,
331 #[serde(default, skip_serializing_if = "Option::is_none")]
335 pub list: Option<String>,
336 #[serde(default, skip_serializing_if = "Option::is_none")]
341 pub accept: Option<String>,
342}
343
344#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
355pub struct RichTextEditorProps {
356 pub field: String,
357 pub label: String,
358 #[serde(default, skip_serializing_if = "Option::is_none")]
359 pub placeholder: Option<String>,
360 #[serde(default, skip_serializing_if = "Option::is_none")]
361 pub default_value: Option<String>,
362 #[serde(default, skip_serializing_if = "Option::is_none")]
363 pub data_path: Option<String>,
364 #[serde(default, skip_serializing_if = "Option::is_none")]
365 pub error: Option<String>,
366}
367
368#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
370pub struct SelectProps {
371 pub field: String,
373 pub label: String,
374 pub options: Vec<SelectOption>,
375 #[serde(default, skip_serializing_if = "Option::is_none")]
376 pub placeholder: Option<String>,
377 #[serde(default, skip_serializing_if = "Option::is_none")]
378 pub required: Option<bool>,
379 #[serde(default, skip_serializing_if = "Option::is_none")]
380 pub disabled: Option<bool>,
381 #[serde(default, skip_serializing_if = "Option::is_none")]
382 pub error: Option<String>,
383 #[serde(default, skip_serializing_if = "Option::is_none")]
384 pub description: Option<String>,
385 #[serde(default, skip_serializing_if = "Option::is_none")]
386 pub default_value: Option<String>,
387 #[serde(default, skip_serializing_if = "Option::is_none")]
389 pub data_path: Option<String>,
390}
391
392#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
394pub struct AlertProps {
395 pub message: String,
396 #[serde(default)]
397 pub variant: AlertVariant,
398 #[serde(default, skip_serializing_if = "Option::is_none")]
399 pub title: Option<String>,
400}
401
402#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
404pub struct BadgeProps {
405 pub label: String,
406 #[serde(default)]
407 pub variant: BadgeVariant,
408}
409
410#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
412pub struct ModalProps {
413 pub id: String,
414 pub title: String,
415 #[serde(default, skip_serializing_if = "Option::is_none")]
416 pub description: Option<String>,
417 #[serde(default, skip_serializing_if = "Option::is_none")]
418 pub trigger_label: Option<String>,
419 #[serde(default, skip_serializing_if = "Vec::is_empty")]
421 pub footer: Vec<String>,
422}
423
424#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
426pub struct TextProps {
427 pub content: String,
428 #[serde(default)]
429 pub element: TextElement,
430}
431
432#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
434pub struct CheckboxProps {
435 pub field: String,
437 #[serde(default, skip_serializing_if = "Option::is_none")]
440 pub value: Option<String>,
441 pub label: String,
442 #[serde(default, skip_serializing_if = "Option::is_none")]
443 pub description: Option<String>,
444 #[serde(default, skip_serializing_if = "Option::is_none")]
445 pub checked: Option<bool>,
446 #[serde(default, skip_serializing_if = "Option::is_none")]
448 pub data_path: Option<String>,
449 #[serde(default, skip_serializing_if = "Option::is_none")]
450 pub required: Option<bool>,
451 #[serde(default, skip_serializing_if = "Option::is_none")]
452 pub disabled: Option<bool>,
453 #[serde(default, skip_serializing_if = "Option::is_none")]
454 pub error: Option<String>,
455}
456
457#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
463pub struct CheckboxListProps {
464 pub field: String,
466 #[serde(default, skip_serializing_if = "Vec::is_empty")]
469 pub options: Vec<SelectOption>,
470 #[serde(default, skip_serializing_if = "Option::is_none")]
472 pub options_path: Option<String>,
473 #[serde(default, skip_serializing_if = "Option::is_none")]
475 pub selected_path: Option<String>,
476 #[serde(default, skip_serializing_if = "Option::is_none")]
477 pub label: Option<String>,
478 #[serde(default, skip_serializing_if = "Option::is_none")]
479 pub description: Option<String>,
480 #[serde(default, skip_serializing_if = "Option::is_none")]
481 pub disabled: Option<bool>,
482 #[serde(default, skip_serializing_if = "Option::is_none")]
483 pub error: Option<String>,
484}
485
486#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
488pub struct SwitchProps {
489 pub field: String,
491 pub label: String,
492 #[serde(default, skip_serializing_if = "Option::is_none")]
493 pub description: Option<String>,
494 #[serde(default, skip_serializing_if = "Option::is_none")]
495 pub checked: Option<bool>,
496 #[serde(default, skip_serializing_if = "Option::is_none")]
498 pub data_path: Option<String>,
499 #[serde(default, skip_serializing_if = "Option::is_none")]
500 pub required: Option<bool>,
501 #[serde(default, skip_serializing_if = "Option::is_none")]
502 pub disabled: Option<bool>,
503 #[serde(default, skip_serializing_if = "Option::is_none")]
504 pub error: Option<String>,
505 #[serde(default, skip_serializing_if = "Option::is_none")]
508 pub action: Option<Action>,
509 #[serde(default, skip_serializing_if = "Option::is_none")]
512 pub compact: Option<bool>,
513}
514
515#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
517pub struct SeparatorProps {
518 #[serde(default, skip_serializing_if = "Option::is_none")]
519 pub orientation: Option<Orientation>,
520}
521
522#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
524pub struct DescriptionItem {
525 pub label: String,
526 pub value: String,
527 #[serde(default, skip_serializing_if = "Option::is_none")]
528 pub format: Option<ColumnFormat>,
529}
530
531#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
533pub struct DescriptionListProps {
534 #[serde(default, skip_serializing_if = "Vec::is_empty")]
535 pub items: Vec<DescriptionItem>,
536 #[serde(default, skip_serializing_if = "Option::is_none")]
537 pub columns: Option<u8>,
538 #[serde(default, skip_serializing_if = "Option::is_none")]
542 pub data_path: Option<String>,
543}
544
545#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
547pub struct Tab {
548 pub value: String,
549 pub label: String,
550 #[serde(default, skip_serializing_if = "Vec::is_empty")]
552 pub children: Vec<String>,
553}
554
555#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
557pub struct TabsProps {
558 pub default_tab: String,
559 pub tabs: Vec<Tab>,
560}
561
562#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
564pub struct BreadcrumbItem {
565 pub label: String,
566 #[serde(default, skip_serializing_if = "Option::is_none")]
567 pub url: Option<String>,
568}
569
570#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
572pub struct BreadcrumbProps {
573 pub items: Vec<BreadcrumbItem>,
574}
575
576#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
578pub struct PaginationProps {
579 pub current_page: u32,
580 pub per_page: u32,
581 pub total: u32,
582 #[serde(default, skip_serializing_if = "Option::is_none")]
583 pub base_url: Option<String>,
584}
585
586#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
588pub struct ProgressProps {
589 pub value: u8,
591 #[serde(default, skip_serializing_if = "Option::is_none")]
592 pub max: Option<u8>,
593 #[serde(default, skip_serializing_if = "Option::is_none")]
594 pub label: Option<String>,
595}
596
597#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
599pub struct ImageProps {
600 #[serde(default)]
601 pub src: String,
602 pub alt: String,
603 #[serde(default, skip_serializing_if = "Option::is_none")]
604 pub aspect_ratio: Option<String>,
605 #[serde(default, skip_serializing_if = "Option::is_none")]
610 pub placeholder_label: Option<String>,
611 #[serde(default, skip_serializing_if = "Option::is_none")]
619 pub inline_svg: Option<String>,
620 #[serde(default, skip_serializing_if = "Option::is_none")]
624 pub data_path: Option<String>,
625}
626
627impl ImageProps {
628 pub fn inline_svg(svg: impl Into<String>, alt: impl Into<String>) -> Self {
634 Self {
635 src: String::new(),
636 alt: alt.into(),
637 aspect_ratio: None,
638 placeholder_label: None,
639 inline_svg: Some(svg.into()),
640 data_path: None,
641 }
642 }
643}
644
645#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
647pub struct AvatarProps {
648 #[serde(default, skip_serializing_if = "Option::is_none")]
649 pub src: Option<String>,
650 pub alt: String,
651 #[serde(default, skip_serializing_if = "Option::is_none")]
652 pub fallback: Option<String>,
653 #[serde(default, skip_serializing_if = "Option::is_none")]
654 pub size: Option<Size>,
655}
656
657#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
659pub struct SkeletonProps {
660 #[serde(default, skip_serializing_if = "Option::is_none")]
661 pub width: Option<String>,
662 #[serde(default, skip_serializing_if = "Option::is_none")]
663 pub height: Option<String>,
664 #[serde(default, skip_serializing_if = "Option::is_none")]
665 pub rounded: Option<bool>,
666}
667
668#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
685pub struct RawHtmlProps {
686 #[serde(default)]
688 pub html: String,
689}
690
691#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
697pub struct StreamTextProps {
698 #[serde(default)]
701 pub sse_url: String,
702 #[serde(default, skip_serializing_if = "Option::is_none")]
704 pub placeholder: Option<String>,
705 #[serde(default, skip_serializing_if = "Option::is_none")]
707 pub loading_text: Option<String>,
708}
709
710#[derive(
712 Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, strum::AsRefStr,
713)]
714#[serde(rename_all = "snake_case")]
715#[strum(serialize_all = "snake_case")]
716pub enum ToastVariant {
717 #[default]
718 Info,
719 Success,
720 Warning,
721 Error,
722}
723
724#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
726pub struct ChecklistItem {
727 pub label: String,
728 #[serde(default)]
729 pub checked: bool,
730 #[serde(default, skip_serializing_if = "Option::is_none")]
731 pub href: Option<String>,
732}
733
734#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
736pub struct NotificationItem {
737 #[serde(default, skip_serializing_if = "Option::is_none")]
738 pub icon: Option<String>,
739 pub text: String,
740 #[serde(default, skip_serializing_if = "Option::is_none")]
741 pub timestamp: Option<String>,
742 #[serde(default)]
743 pub read: bool,
744 #[serde(default, skip_serializing_if = "Option::is_none")]
745 pub action_url: Option<String>,
746}
747
748#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
750pub struct SidebarNavItem {
751 pub label: String,
752 pub href: String,
753 #[serde(default, skip_serializing_if = "Option::is_none")]
754 pub icon: Option<String>,
755 #[serde(default)]
756 pub active: bool,
757 #[serde(default, skip_serializing_if = "Option::is_none")]
760 pub disabled: Option<bool>,
761}
762
763#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
765pub struct SidebarGroup {
766 pub label: String,
767 #[serde(default)]
768 pub collapsed: bool,
769 pub items: Vec<SidebarNavItem>,
770}
771
772#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
774pub struct StatCardProps {
775 pub label: String,
776 pub value: String,
777 #[serde(default, skip_serializing_if = "Option::is_none")]
778 pub icon: Option<String>,
779 #[serde(default, skip_serializing_if = "Option::is_none")]
780 pub subtitle: Option<String>,
781 #[serde(default, skip_serializing_if = "Option::is_none")]
783 pub sse_target: Option<String>,
784 #[serde(default, skip_serializing_if = "Option::is_none")]
789 pub value_path: Option<String>,
790}
791
792#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
794pub struct ChecklistProps {
795 pub title: String,
796 pub items: Vec<ChecklistItem>,
797 #[serde(default = "default_true")]
798 pub dismissible: bool,
799 #[serde(default, skip_serializing_if = "Option::is_none")]
800 pub dismiss_label: Option<String>,
801 #[serde(default, skip_serializing_if = "Option::is_none")]
803 pub data_key: Option<String>,
804}
805
806fn default_true() -> bool {
807 true
808}
809
810#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
815pub struct ToastProps {
816 pub message: String,
817 #[serde(default)]
818 pub variant: ToastVariant,
819 #[serde(default, skip_serializing_if = "Option::is_none")]
821 pub timeout: Option<u32>,
822 #[serde(default = "default_true")]
823 pub dismissible: bool,
824}
825
826#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
828pub struct NotificationDropdownProps {
829 pub notifications: Vec<NotificationItem>,
830 #[serde(default, skip_serializing_if = "Option::is_none")]
831 pub empty_text: Option<String>,
832}
833
834#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
836pub struct SidebarProps {
837 #[serde(default, skip_serializing_if = "Vec::is_empty")]
838 pub fixed_top: Vec<SidebarNavItem>,
839 #[serde(default, skip_serializing_if = "Vec::is_empty")]
840 pub groups: Vec<SidebarGroup>,
841 #[serde(default, skip_serializing_if = "Vec::is_empty")]
842 pub fixed_bottom: Vec<SidebarNavItem>,
843}
844
845#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
847pub struct HeaderProps {
848 pub business_name: String,
849 #[serde(default, skip_serializing_if = "Option::is_none")]
851 pub notification_count: Option<u32>,
852 #[serde(default, skip_serializing_if = "Option::is_none")]
853 pub user_name: Option<String>,
854 #[serde(default, skip_serializing_if = "Option::is_none")]
855 pub user_avatar: Option<String>,
856 #[serde(default, skip_serializing_if = "Option::is_none")]
857 pub logout_url: Option<String>,
858}
859
860#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
862#[serde(rename_all = "snake_case")]
863pub enum GapSize {
864 None,
865 Sm,
866 #[default]
867 Md,
868 Lg,
869 Xl,
870}
871
872#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
874pub struct GridProps {
875 #[serde(default = "default_grid_columns")]
877 pub columns: u8,
878 #[serde(default, skip_serializing_if = "Option::is_none")]
880 pub md_columns: Option<u8>,
881 #[serde(default, skip_serializing_if = "Option::is_none")]
883 pub lg_columns: Option<u8>,
884 #[serde(default)]
886 pub gap: GapSize,
887 #[serde(default, skip_serializing_if = "Option::is_none")]
890 pub scrollable: Option<bool>,
891}
892
893fn default_grid_columns() -> u8 {
894 2
895}
896
897#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
899pub struct CollapsibleProps {
900 pub title: String,
901 #[serde(default)]
902 pub expanded: bool,
903}
904
905#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
907pub struct EmptyStateProps {
908 pub title: String,
909 #[serde(default, skip_serializing_if = "Option::is_none")]
910 pub description: Option<String>,
911 #[serde(default, skip_serializing_if = "Option::is_none")]
912 pub action: Option<Action>,
913 #[serde(default, skip_serializing_if = "Option::is_none")]
914 pub action_label: Option<String>,
915}
916
917#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
919#[serde(rename_all = "snake_case")]
920pub enum FormSectionLayout {
921 #[default]
922 Stacked,
923 TwoColumn,
924}
925
926#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
928pub struct FormSectionProps {
929 pub title: String,
930 #[serde(default, skip_serializing_if = "Option::is_none")]
931 pub description: Option<String>,
932 #[serde(default, skip_serializing_if = "Option::is_none")]
934 pub layout: Option<FormSectionLayout>,
935}
936
937#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
939pub struct PageHeaderProps {
940 pub title: String,
941 #[serde(default, skip_serializing_if = "Vec::is_empty")]
942 pub breadcrumb: Vec<BreadcrumbItem>,
943 #[serde(
945 default,
946 deserialize_with = "deserialize_actions_lax",
947 skip_serializing_if = "Vec::is_empty"
948 )]
949 pub actions: Vec<String>,
950}
951
952#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Default)]
954pub struct ButtonGroupProps {
955 #[serde(default)]
957 pub gap: GapSize,
958}
959
960#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
971pub struct ActionItem {
972 pub label: String,
973 pub action: Action,
974 #[serde(default)]
977 pub destructive: bool,
978 #[serde(default, skip_serializing_if = "Option::is_none")]
979 pub variant: Option<ButtonVariant>,
980 #[serde(default, skip_serializing_if = "Option::is_none")]
981 pub icon: Option<String>,
982 #[serde(default, skip_serializing_if = "Option::is_none")]
986 pub visible_if: Option<String>,
987}
988
989#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1000pub struct ActionGroupProps {
1001 pub items: Vec<ActionItem>,
1002 pub menu_id: String,
1005 #[serde(default, skip_serializing_if = "Option::is_none")]
1007 pub max_inline: Option<u8>,
1008 #[serde(default, skip_serializing_if = "Option::is_none")]
1010 pub overflow_label: Option<String>,
1011 #[serde(default, skip_serializing_if = "Option::is_none")]
1013 pub row_key: Option<String>,
1014}
1015
1016#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Default)]
1032pub struct SegmentedControlProps {
1033 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1035 pub items: Vec<SegmentedItem>,
1036 #[serde(default, skip_serializing_if = "Option::is_none")]
1039 pub data_path: Option<String>,
1040 #[serde(default)]
1042 pub size: Size,
1043 #[serde(default, skip_serializing_if = "Option::is_none")]
1046 pub aria_label: Option<String>,
1047}
1048
1049#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Default)]
1051pub struct SegmentedItem {
1052 pub label: String,
1054 pub href: String,
1056 #[serde(default)]
1058 pub active: bool,
1059 #[serde(default, skip_serializing_if = "Option::is_none")]
1062 pub aria_label: Option<String>,
1063}
1064
1065#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Default)]
1081pub struct SidebarLayoutProps {
1082 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1084 pub items: Vec<SidebarLayoutItem>,
1085 #[serde(default, skip_serializing_if = "Option::is_none")]
1087 pub data_path: Option<String>,
1088 pub active: String,
1091 #[serde(default, skip_serializing_if = "Option::is_none")]
1093 pub aria_label: Option<String>,
1094}
1095
1096#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Default)]
1098pub struct SidebarLayoutItem {
1099 pub slug: String,
1102 pub label: String,
1104 pub url: String,
1107}
1108
1109#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1118pub struct DetailPageProps {
1119 pub title: String,
1120 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1121 pub breadcrumb: Vec<BreadcrumbItem>,
1122 #[serde(
1124 default,
1125 deserialize_with = "deserialize_actions_lax",
1126 skip_serializing_if = "Vec::is_empty"
1127 )]
1128 pub actions: Vec<String>,
1129 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1132 pub info: Vec<String>,
1133}
1134
1135#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1137pub struct DropdownMenuAction {
1138 pub label: String,
1139 pub action: Action,
1140 #[serde(default)]
1141 pub destructive: bool,
1142 #[serde(default, skip_serializing_if = "Option::is_none")]
1149 pub visible_if: Option<String>,
1150}
1151
1152#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1155pub struct DataTableProps {
1156 pub columns: Vec<Column>,
1157 pub data_path: String,
1158 #[serde(default, skip_serializing_if = "Option::is_none")]
1159 pub row_actions: Option<Vec<DropdownMenuAction>>,
1160 #[serde(default, skip_serializing_if = "Option::is_none")]
1161 pub empty_message: Option<String>,
1162 #[serde(default, skip_serializing_if = "Option::is_none")]
1163 pub row_key: Option<String>,
1164 #[serde(default, skip_serializing_if = "Option::is_none")]
1166 pub row_href: Option<String>,
1167}
1168
1169#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1173pub struct MediaCardGridProps {
1174 pub data_path: String,
1175 pub title_key: String,
1177 #[serde(default, skip_serializing_if = "Option::is_none")]
1179 pub description_key: Option<String>,
1180 #[serde(default, skip_serializing_if = "Option::is_none")]
1182 pub image_key: Option<String>,
1183 #[serde(default, skip_serializing_if = "Option::is_none")]
1185 pub image_href_key: Option<String>,
1186 #[serde(default, skip_serializing_if = "Option::is_none")]
1188 pub image_aspect_ratio: Option<String>,
1189 #[serde(default, skip_serializing_if = "Option::is_none")]
1192 pub image_position: Option<String>,
1193 #[serde(default, skip_serializing_if = "Option::is_none")]
1195 pub badge_key: Option<String>,
1196 #[serde(default, skip_serializing_if = "Option::is_none")]
1198 pub badge_variant_key: Option<String>,
1199 #[serde(default, skip_serializing_if = "Option::is_none")]
1201 pub row_key: Option<String>,
1202 #[serde(default, skip_serializing_if = "Option::is_none")]
1203 pub row_actions: Option<Vec<DropdownMenuAction>>,
1204 #[serde(default, skip_serializing_if = "Option::is_none")]
1205 pub empty_message: Option<String>,
1206 #[serde(default, skip_serializing_if = "Option::is_none")]
1208 pub columns: Option<u8>,
1209}
1210
1211#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1219pub struct KanbanColumnProps {
1220 pub id: String,
1221 pub title: String,
1222 #[serde(default)]
1223 pub count: u32,
1224 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1226 pub children: Vec<String>,
1227}
1228
1229#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1242pub struct KanbanBoardProps {
1243 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1245 pub columns: Vec<KanbanColumnProps>,
1246 #[serde(default, skip_serializing_if = "Option::is_none")]
1249 pub items_path: Option<String>,
1250 #[serde(default, skip_serializing_if = "Option::is_none")]
1252 pub group_by: Option<String>,
1253 #[serde(default, skip_serializing_if = "Option::is_none")]
1255 pub card_title_key: Option<String>,
1256 #[serde(default, skip_serializing_if = "Option::is_none")]
1258 pub card_description_key: Option<String>,
1259 #[serde(default, skip_serializing_if = "Option::is_none")]
1262 pub row_actions: Option<Vec<DropdownMenuAction>>,
1263 #[serde(default, skip_serializing_if = "Option::is_none")]
1266 pub row_key: Option<String>,
1267 #[serde(default, skip_serializing_if = "Option::is_none")]
1268 pub mobile_default_column: Option<String>,
1269 #[serde(default, skip_serializing_if = "Option::is_none")]
1273 pub empty_label: Option<String>,
1274}
1275
1276#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1281pub struct CalendarCellProps {
1282 pub day: u8,
1283 #[serde(default)]
1284 pub is_today: bool,
1285 #[serde(default)]
1286 pub is_current_month: bool,
1287 #[serde(default)]
1288 pub event_count: u32,
1289 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1292 pub dot_colors: Vec<String>,
1293 #[serde(default)]
1297 pub closed: bool,
1298}
1299
1300#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1302#[serde(rename_all = "snake_case")]
1303pub enum ActionCardVariant {
1304 #[default]
1305 Default,
1306 Setup,
1307 Danger,
1308}
1309
1310#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1315pub struct ActionCardProps {
1316 pub title: String,
1317 pub description: String,
1318 #[serde(default, skip_serializing_if = "Option::is_none")]
1319 pub icon: Option<String>,
1320 #[serde(default)]
1321 pub variant: ActionCardVariant,
1322 #[serde(default, skip_serializing_if = "Option::is_none")]
1324 pub href: Option<String>,
1325}
1326
1327#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1332pub struct ProductTileProps {
1333 pub product_id: String,
1334 pub name: String,
1335 pub price: String,
1336 pub field: String,
1337 #[serde(default, skip_serializing_if = "Option::is_none")]
1338 pub default_quantity: Option<u32>,
1339}
1340
1341fn deserialize_actions_lax<'de, D: serde::Deserializer<'de>>(
1347 d: D,
1348) -> Result<Vec<String>, D::Error> {
1349 use serde::de::Error;
1350 let v = serde_json::Value::deserialize(d)?;
1351 match v {
1352 serde_json::Value::Null => Ok(Vec::new()),
1353 serde_json::Value::String(s) if s.is_empty() => Ok(Vec::new()),
1354 serde_json::Value::Array(arr) => arr
1355 .into_iter()
1356 .map(|item| {
1357 item.as_str()
1358 .map(String::from)
1359 .ok_or_else(|| D::Error::custom("PageHeader.actions: expected string in array"))
1360 })
1361 .collect(),
1362 other => Err(D::Error::custom(format!(
1363 "PageHeader.actions: expected null, empty string, or array of strings; got {other:?}"
1364 ))),
1365 }
1366}
1367
1368#[cfg(test)]
1369mod schema_smoke_tests {
1370 use super::*;
1381
1382 fn assert_schema_nonempty_object<T: schemars::JsonSchema>(type_label: &str) {
1383 let schema = schemars::schema_for!(T);
1384 let value = serde_json::to_value(&schema).expect("schema serializes to JSON");
1385 assert!(
1386 value.is_object(),
1387 "{type_label}: schema must be a JSON object"
1388 );
1389 let props = value
1390 .get("properties")
1391 .and_then(|p| p.as_object())
1392 .map(|o| !o.is_empty())
1393 .unwrap_or(false);
1394 assert!(
1395 props,
1396 "{type_label}: schema must have a non-empty `properties` field"
1397 );
1398 }
1399
1400 #[test]
1401 fn schema_for_card_props_generates() {
1402 assert_schema_nonempty_object::<CardProps>("CardProps");
1403 }
1404
1405 #[test]
1406 fn schema_for_table_props_generates() {
1407 assert_schema_nonempty_object::<TableProps>("TableProps");
1408 }
1409
1410 #[test]
1411 fn schema_for_form_props_generates() {
1412 assert_schema_nonempty_object::<FormProps>("FormProps");
1413 }
1414
1415 #[test]
1416 fn schema_for_button_props_generates() {
1417 assert_schema_nonempty_object::<ButtonProps>("ButtonProps");
1418 }
1419
1420 #[test]
1421 fn schema_for_input_props_generates() {
1422 assert_schema_nonempty_object::<InputProps>("InputProps");
1423 }
1424
1425 #[test]
1426 fn schema_for_select_props_generates() {
1427 assert_schema_nonempty_object::<SelectProps>("SelectProps");
1428 }
1429
1430 #[test]
1431 fn schema_for_alert_props_generates() {
1432 assert_schema_nonempty_object::<AlertProps>("AlertProps");
1433 }
1434
1435 #[test]
1436 fn schema_for_badge_props_generates() {
1437 assert_schema_nonempty_object::<BadgeProps>("BadgeProps");
1438 }
1439
1440 #[test]
1441 fn schema_for_modal_props_generates() {
1442 assert_schema_nonempty_object::<ModalProps>("ModalProps");
1443 }
1444
1445 #[test]
1446 fn schema_for_text_props_generates() {
1447 assert_schema_nonempty_object::<TextProps>("TextProps");
1448 }
1449
1450 #[test]
1451 fn schema_for_checkbox_props_generates() {
1452 assert_schema_nonempty_object::<CheckboxProps>("CheckboxProps");
1453 }
1454
1455 #[test]
1456 fn schema_for_switch_props_generates() {
1457 assert_schema_nonempty_object::<SwitchProps>("SwitchProps");
1458 }
1459
1460 #[test]
1461 fn schema_for_separator_props_generates() {
1462 assert_schema_nonempty_object::<SeparatorProps>("SeparatorProps");
1463 }
1464
1465 #[test]
1466 fn schema_for_description_list_props_generates() {
1467 assert_schema_nonempty_object::<DescriptionListProps>("DescriptionListProps");
1468 }
1469
1470 #[test]
1471 fn schema_for_tab_generates() {
1472 assert_schema_nonempty_object::<Tab>("Tab");
1473 }
1474
1475 #[test]
1476 fn schema_for_tabs_props_generates() {
1477 assert_schema_nonempty_object::<TabsProps>("TabsProps");
1478 }
1479
1480 #[test]
1481 fn schema_for_breadcrumb_props_generates() {
1482 assert_schema_nonempty_object::<BreadcrumbProps>("BreadcrumbProps");
1483 }
1484
1485 #[test]
1486 fn schema_for_pagination_props_generates() {
1487 assert_schema_nonempty_object::<PaginationProps>("PaginationProps");
1488 }
1489
1490 #[test]
1491 fn schema_for_progress_props_generates() {
1492 assert_schema_nonempty_object::<ProgressProps>("ProgressProps");
1493 }
1494
1495 #[test]
1496 fn schema_for_image_props_generates() {
1497 assert_schema_nonempty_object::<ImageProps>("ImageProps");
1498 }
1499
1500 #[test]
1501 fn image_inline_svg_factory_roundtrips_via_serde() {
1502 let p = ImageProps::inline_svg("<svg/>", "alt");
1503 let json = serde_json::to_value(&p).expect("serialization must not fail");
1504 let parsed: ImageProps =
1505 serde_json::from_value(json).expect("deserialization must not fail");
1506 assert_eq!(parsed.inline_svg, Some("<svg/>".to_string()));
1507 assert_eq!(parsed.alt, "alt");
1508 assert_eq!(parsed.src, "");
1509 }
1510
1511 #[test]
1512 fn schema_for_avatar_props_generates() {
1513 assert_schema_nonempty_object::<AvatarProps>("AvatarProps");
1514 }
1515
1516 #[test]
1517 fn schema_for_skeleton_props_generates() {
1518 assert_schema_nonempty_object::<SkeletonProps>("SkeletonProps");
1519 }
1520
1521 #[test]
1522 fn schema_for_stat_card_props_generates() {
1523 assert_schema_nonempty_object::<StatCardProps>("StatCardProps");
1524 }
1525
1526 #[test]
1527 fn schema_for_checklist_props_generates() {
1528 assert_schema_nonempty_object::<ChecklistProps>("ChecklistProps");
1529 }
1530
1531 #[test]
1532 fn schema_for_toast_props_generates() {
1533 assert_schema_nonempty_object::<ToastProps>("ToastProps");
1534 }
1535
1536 #[test]
1537 fn schema_for_notification_dropdown_props_generates() {
1538 assert_schema_nonempty_object::<NotificationDropdownProps>("NotificationDropdownProps");
1539 }
1540
1541 #[test]
1542 fn schema_for_sidebar_props_generates() {
1543 assert_schema_nonempty_object::<SidebarProps>("SidebarProps");
1544 }
1545
1546 #[test]
1547 fn schema_for_header_props_generates() {
1548 assert_schema_nonempty_object::<HeaderProps>("HeaderProps");
1549 }
1550
1551 #[test]
1552 fn schema_for_grid_props_generates() {
1553 assert_schema_nonempty_object::<GridProps>("GridProps");
1554 }
1555
1556 #[test]
1557 fn schema_for_collapsible_props_generates() {
1558 assert_schema_nonempty_object::<CollapsibleProps>("CollapsibleProps");
1559 }
1560
1561 #[test]
1562 fn schema_for_empty_state_props_generates() {
1563 assert_schema_nonempty_object::<EmptyStateProps>("EmptyStateProps");
1564 }
1565
1566 #[test]
1567 fn schema_for_form_section_props_generates() {
1568 assert_schema_nonempty_object::<FormSectionProps>("FormSectionProps");
1569 }
1570
1571 #[test]
1572 fn schema_for_page_header_props_generates() {
1573 assert_schema_nonempty_object::<PageHeaderProps>("PageHeaderProps");
1574 }
1575
1576 #[test]
1577 fn schema_for_button_group_props_generates() {
1578 assert_schema_nonempty_object::<ButtonGroupProps>("ButtonGroupProps");
1579 }
1580
1581 #[test]
1582 fn schema_for_action_item_generates() {
1583 assert_schema_nonempty_object::<ActionItem>("ActionItem");
1584 }
1585
1586 #[test]
1587 fn schema_for_action_group_props_generates() {
1588 assert_schema_nonempty_object::<ActionGroupProps>("ActionGroupProps");
1589 }
1590
1591 #[test]
1592 fn schema_for_dropdown_menu_action_generates() {
1593 assert_schema_nonempty_object::<DropdownMenuAction>("DropdownMenuAction");
1594 }
1595
1596 #[test]
1597 fn schema_for_data_table_props_generates() {
1598 assert_schema_nonempty_object::<DataTableProps>("DataTableProps");
1599 }
1600
1601 #[test]
1602 fn schema_for_kanban_column_props_generates() {
1603 assert_schema_nonempty_object::<KanbanColumnProps>("KanbanColumnProps");
1604 }
1605
1606 #[test]
1607 fn schema_for_kanban_board_props_generates() {
1608 assert_schema_nonempty_object::<KanbanBoardProps>("KanbanBoardProps");
1609 }
1610
1611 #[test]
1612 fn schema_for_calendar_cell_props_generates() {
1613 assert_schema_nonempty_object::<CalendarCellProps>("CalendarCellProps");
1614 }
1615
1616 #[test]
1617 fn schema_for_action_card_props_generates() {
1618 assert_schema_nonempty_object::<ActionCardProps>("ActionCardProps");
1619 }
1620
1621 #[test]
1622 fn schema_for_product_tile_props_generates() {
1623 assert_schema_nonempty_object::<ProductTileProps>("ProductTileProps");
1624 }
1625
1626 #[test]
1627 fn card_props_round_trips_footer() {
1628 let original = CardProps {
1629 title: "Hero".to_string(),
1630 description: None,
1631 subtitle: None,
1632 badge: None,
1633 max_width: None,
1634 footer: vec!["btn1".to_string(), "btn2".to_string()],
1635 variant: CardVariant::Bordered,
1636 };
1637 let json = serde_json::to_string(&original).unwrap();
1638 let parsed: CardProps = serde_json::from_str(&json).unwrap();
1639 assert_eq!(original.footer, parsed.footer);
1640 }
1641
1642 #[test]
1643 fn tab_round_trips_children() {
1644 let original = Tab {
1645 value: "overview".to_string(),
1646 label: "Overview".to_string(),
1647 children: vec!["panel1".to_string()],
1648 };
1649 let json = serde_json::to_string(&original).unwrap();
1650 let parsed: Tab = serde_json::from_str(&json).unwrap();
1651 assert_eq!(original.children, parsed.children);
1652 }
1653
1654 #[test]
1655 fn card_props_omits_empty_footer_in_json() {
1656 let card = CardProps {
1657 title: "Card".to_string(),
1658 description: None,
1659 subtitle: None,
1660 badge: None,
1661 max_width: None,
1662 footer: Vec::new(),
1663 variant: CardVariant::Bordered,
1664 };
1665 let json = serde_json::to_string(&card).unwrap();
1666 assert!(
1667 !json.contains("\"footer\""),
1668 "empty footer must be skipped, got: {json}"
1669 );
1670 }
1671
1672 #[test]
1673 fn card_props_round_trips_badge() {
1674 let original = CardProps {
1675 title: "Hero".to_string(),
1676 description: None,
1677 subtitle: None,
1678 badge: Some("Scade tra 9m".to_string()),
1679 max_width: None,
1680 footer: Vec::new(),
1681 variant: CardVariant::Bordered,
1682 };
1683 let json = serde_json::to_string(&original).unwrap();
1684 let parsed: CardProps = serde_json::from_str(&json).unwrap();
1685 assert_eq!(original.badge, parsed.badge);
1686 }
1687
1688 #[test]
1689 fn card_props_omits_empty_badge_in_json() {
1690 let card = CardProps {
1691 title: "Card".to_string(),
1692 description: None,
1693 subtitle: None,
1694 badge: None,
1695 max_width: None,
1696 footer: Vec::new(),
1697 variant: CardVariant::Bordered,
1698 };
1699 let json = serde_json::to_string(&card).unwrap();
1700 assert!(
1701 !json.contains("\"badge\""),
1702 "empty badge must be skipped, got: {json}"
1703 );
1704 }
1705
1706 #[test]
1707 fn card_props_round_trips_subtitle() {
1708 let original = CardProps {
1709 title: "Hero".to_string(),
1710 description: None,
1711 subtitle: Some("Marco Rossi".to_string()),
1712 badge: None,
1713 max_width: None,
1714 footer: Vec::new(),
1715 variant: CardVariant::Bordered,
1716 };
1717 let json = serde_json::to_string(&original).unwrap();
1718 let parsed: CardProps = serde_json::from_str(&json).unwrap();
1719 assert_eq!(original.subtitle, parsed.subtitle);
1720 }
1721
1722 #[test]
1723 fn card_props_omits_empty_subtitle_in_json() {
1724 let card = CardProps {
1725 title: "Card".to_string(),
1726 description: None,
1727 subtitle: None,
1728 badge: None,
1729 max_width: None,
1730 footer: Vec::new(),
1731 variant: CardVariant::Bordered,
1732 };
1733 let json = serde_json::to_string(&card).unwrap();
1734 assert!(
1735 !json.contains("\"subtitle\""),
1736 "empty subtitle must be skipped, got: {json}"
1737 );
1738 }
1739
1740 #[test]
1741 fn card_props_schema_includes_badge() {
1742 let schema = schemars::schema_for!(CardProps);
1743 let value = serde_json::to_value(&schema).expect("schema serializes to JSON");
1744 let props = value
1745 .get("properties")
1746 .and_then(|p| p.as_object())
1747 .expect("schema has a properties object");
1748 assert!(
1749 props.contains_key("badge"),
1750 "CardProps schema must expose a `badge` property; got keys: {:?}",
1751 props.keys().collect::<Vec<_>>()
1752 );
1753 let badge_schema = props.get("badge").expect("badge entry");
1758 let badge_json = badge_schema.to_string();
1759 assert!(
1760 badge_json.contains("\"string\""),
1761 "badge schema entry must mention string type; got: {badge_json}"
1762 );
1763 }
1764
1765 #[test]
1766 fn card_props_schema_includes_subtitle() {
1767 let schema = schemars::schema_for!(CardProps);
1768 let value = serde_json::to_value(&schema).expect("schema serializes to JSON");
1769 let props = value
1770 .get("properties")
1771 .and_then(|p| p.as_object())
1772 .expect("schema has a properties object");
1773 assert!(
1774 props.contains_key("subtitle"),
1775 "CardProps schema must expose a `subtitle` property; got keys: {:?}",
1776 props.keys().collect::<Vec<_>>()
1777 );
1778 let subtitle_schema = props.get("subtitle").expect("subtitle entry");
1783 let subtitle_json = subtitle_schema.to_string();
1784 assert!(
1785 subtitle_json.contains("\"string\""),
1786 "subtitle schema entry must mention string type; got: {subtitle_json}"
1787 );
1788 }
1789
1790 #[test]
1791 fn schema_for_checkbox_list_props_generates() {
1792 assert_schema_nonempty_object::<CheckboxListProps>("CheckboxListProps");
1793 }
1794
1795 #[test]
1796 fn checkbox_list_props_serde_roundtrip() {
1797 let json = serde_json::json!({
1798 "field": "services",
1799 "options": [{"value": "a", "label": "Alpha"}, {"value": "b", "label": "Beta"}],
1800 "selected_path": "/preselected"
1801 });
1802 let parsed: CheckboxListProps = serde_json::from_value(json.clone()).expect("decode");
1803 assert_eq!(parsed.field, "services");
1804 assert_eq!(parsed.options.len(), 2);
1805 assert_eq!(parsed.selected_path.as_deref(), Some("/preselected"));
1806 let reserialized = serde_json::to_value(&parsed).expect("encode");
1807 assert!(reserialized.get("label").is_none());
1809 assert!(reserialized.get("disabled").is_none());
1810 }
1811
1812 #[test]
1813 fn schema_for_rich_text_editor_props_generates() {
1814 assert_schema_nonempty_object::<RichTextEditorProps>("RichTextEditorProps");
1815 }
1816
1817 #[test]
1818 fn rich_text_editor_props_serde_roundtrip() {
1819 let json = serde_json::json!({
1820 "field": "body",
1821 "label": "Body"
1822 });
1823 let parsed: RichTextEditorProps = serde_json::from_value(json).expect("decode");
1824 assert_eq!(parsed.field, "body");
1825 assert_eq!(parsed.label, "Body");
1826 assert!(parsed.placeholder.is_none());
1827 assert!(parsed.default_value.is_none());
1828 assert!(parsed.data_path.is_none());
1829 assert!(parsed.error.is_none());
1830 let reserialized = serde_json::to_value(&parsed).expect("encode");
1831 assert!(reserialized.get("placeholder").is_none());
1833 assert!(reserialized.get("error").is_none());
1834 }
1835}
1836
1837#[cfg(test)]
1838mod strum_tests {
1839 use super::*;
1840
1841 #[test]
1845 fn variant_enums_strum_matches_serde_wire_format() {
1846 fn check<T: AsRef<str> + serde::Serialize>(variants: &[T], label: &str) {
1847 for v in variants {
1848 let json = serde_json::to_string(v).expect("serialize");
1849 let json_stripped = json.trim_matches('"');
1850 assert_eq!(
1851 v.as_ref(),
1852 json_stripped,
1853 "strum AsRefStr drifted from serde for {label} variant"
1854 );
1855 }
1856 }
1857 check(
1858 &[
1859 AlertVariant::Info,
1860 AlertVariant::Success,
1861 AlertVariant::Warning,
1862 AlertVariant::Error,
1863 ],
1864 "AlertVariant",
1865 );
1866 check(
1867 &[
1868 BadgeVariant::Default,
1869 BadgeVariant::Secondary,
1870 BadgeVariant::Destructive,
1871 BadgeVariant::Outline,
1872 ],
1873 "BadgeVariant",
1874 );
1875 check(
1876 &[
1877 ButtonVariant::Default,
1878 ButtonVariant::Secondary,
1879 ButtonVariant::Destructive,
1880 ButtonVariant::Outline,
1881 ButtonVariant::Ghost,
1882 ButtonVariant::Link,
1883 ],
1884 "ButtonVariant",
1885 );
1886 check(
1887 &[
1888 ToastVariant::Info,
1889 ToastVariant::Success,
1890 ToastVariant::Warning,
1891 ToastVariant::Error,
1892 ],
1893 "ToastVariant",
1894 );
1895 }
1896
1897 #[test]
1898 fn alert_variant_as_ref_str_matches_wire_format() {
1899 assert_eq!(AlertVariant::Success.as_ref(), "success");
1900 assert_eq!(AlertVariant::Warning.as_ref(), "warning");
1901 assert_eq!(AlertVariant::Info.as_ref(), "info");
1902 assert_eq!(AlertVariant::Error.as_ref(), "error");
1903 }
1904}
1905
1906#[cfg(test)]
1907mod card_variant_tests {
1908 use super::*;
1909
1910 #[test]
1911 fn card_variant_default_is_bordered() {
1912 assert_eq!(CardVariant::default(), CardVariant::Bordered);
1913 }
1914
1915 #[test]
1916 fn card_variant_serializes_snake_case() {
1917 assert_eq!(
1918 serde_json::to_value(CardVariant::Bordered).unwrap(),
1919 serde_json::json!("bordered")
1920 );
1921 assert_eq!(
1922 serde_json::to_value(CardVariant::Elevated).unwrap(),
1923 serde_json::json!("elevated")
1924 );
1925 }
1926
1927 #[test]
1928 fn card_variant_deserializes_snake_case() {
1929 assert_eq!(
1930 serde_json::from_value::<CardVariant>(serde_json::json!("bordered")).unwrap(),
1931 CardVariant::Bordered
1932 );
1933 assert_eq!(
1934 serde_json::from_value::<CardVariant>(serde_json::json!("elevated")).unwrap(),
1935 CardVariant::Elevated
1936 );
1937 }
1938
1939 #[test]
1940 fn card_props_without_variant_defaults_to_bordered() {
1941 let v = serde_json::json!({"title": "x"});
1942 let p: CardProps = serde_json::from_value(v).unwrap();
1943 assert_eq!(p.variant, CardVariant::Bordered);
1944 }
1945
1946 #[test]
1947 fn card_props_with_elevated_variant() {
1948 let v = serde_json::json!({"title": "x", "variant": "elevated"});
1949 let p: CardProps = serde_json::from_value(v).unwrap();
1950 assert_eq!(p.variant, CardVariant::Elevated);
1951 }
1952
1953 #[test]
1954 fn card_props_roundtrip_preserves_variant() {
1955 let p = CardProps {
1956 title: "x".into(),
1957 description: None,
1958 subtitle: None,
1959 badge: None,
1960 max_width: None,
1961 footer: vec![],
1962 variant: CardVariant::Elevated,
1963 };
1964 let j = serde_json::to_value(&p).unwrap();
1965 let back: CardProps = serde_json::from_value(j).unwrap();
1966 assert_eq!(back.variant, CardVariant::Elevated);
1967 }
1968}
1969
1970#[cfg(test)]
1971mod kanban_board_props_tests {
1972 use super::*;
1973
1974 #[test]
1975 fn kanban_board_props_serde_static_columns() {
1976 let v = serde_json::json!({
1977 "columns": [{"title": "To Do", "id": "todo", "count": 0}]
1978 });
1979 let p: KanbanBoardProps = serde_json::from_value(v).unwrap();
1980 assert_eq!(p.columns.len(), 1);
1981 assert!(p.items_path.is_none());
1982 assert!(p.group_by.is_none());
1983 }
1984
1985 #[test]
1986 fn kanban_board_props_serde_data_bound() {
1987 let v = serde_json::json!({
1988 "columns": [{"title": "Open", "id": "open"}],
1989 "items_path": "/data/order",
1990 "group_by": "status",
1991 "card_title_key": "name"
1992 });
1993 let p: KanbanBoardProps = serde_json::from_value(v).unwrap();
1994 assert_eq!(p.columns.len(), 1);
1995 assert_eq!(p.items_path.as_deref(), Some("/data/order"));
1996 assert_eq!(p.group_by.as_deref(), Some("status"));
1997 assert_eq!(p.card_title_key.as_deref(), Some("name"));
1998 }
1999
2000 #[test]
2001 fn kanban_board_props_serde_neither() {
2002 let v = serde_json::json!({});
2003 let p: KanbanBoardProps = serde_json::from_value(v).unwrap();
2004 assert!(p.columns.is_empty());
2005 assert!(p.items_path.is_none());
2006 assert!(p.group_by.is_none());
2007 }
2008
2009 #[test]
2010 fn kanban_board_props_empty_columns_skipped_on_serialize() {
2011 let p = KanbanBoardProps {
2012 columns: vec![],
2013 items_path: Some("/data/order".into()),
2014 group_by: Some("status".into()),
2015 card_title_key: None,
2016 card_description_key: None,
2017 row_actions: None,
2018 row_key: None,
2019 mobile_default_column: None,
2020 empty_label: None,
2021 };
2022 let j = serde_json::to_value(&p).unwrap();
2023 assert!(
2024 j.get("columns").is_none(),
2025 "empty columns must be skipped, got: {j}"
2026 );
2027 assert_eq!(
2028 j.get("items_path").and_then(|v| v.as_str()),
2029 Some("/data/order")
2030 );
2031 }
2032}
2033
2034#[cfg(test)]
2035mod page_header_actions_tests {
2036 use super::*;
2037
2038 #[test]
2039 fn page_header_actions_missing_field() {
2040 let v = serde_json::json!({"title": "X"});
2041 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
2042 assert!(p.actions.is_empty());
2043 }
2044
2045 #[test]
2046 fn page_header_actions_null() {
2047 let v = serde_json::json!({"title": "X", "actions": null});
2048 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
2049 assert!(p.actions.is_empty());
2050 }
2051
2052 #[test]
2053 fn page_header_actions_empty_string() {
2054 let v = serde_json::json!({"title": "X", "actions": ""});
2055 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
2056 assert!(p.actions.is_empty());
2057 }
2058
2059 #[test]
2060 fn page_header_actions_empty_array() {
2061 let v = serde_json::json!({"title": "X", "actions": []});
2062 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
2063 assert!(p.actions.is_empty());
2064 }
2065
2066 #[test]
2067 fn page_header_actions_non_empty_array() {
2068 let v = serde_json::json!({"title": "X", "actions": ["a", "b"]});
2069 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
2070 assert_eq!(p.actions, vec!["a".to_string(), "b".to_string()]);
2071 }
2072
2073 #[test]
2074 fn page_header_actions_non_empty_string_rejected() {
2075 let v = serde_json::json!({"title": "X", "actions": "not-empty"});
2076 let result: Result<PageHeaderProps, _> = serde_json::from_value(v);
2077 assert!(result.is_err(), "non-empty string must be rejected");
2078 }
2079
2080 #[test]
2081 fn page_header_actions_non_string_array_rejected() {
2082 let v = serde_json::json!({"title": "X", "actions": [1, 2, 3]});
2083 let result: Result<PageHeaderProps, _> = serde_json::from_value(v);
2084 assert!(result.is_err(), "array of non-strings must be rejected");
2085 }
2086}