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}
1294
1295#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1297#[serde(rename_all = "snake_case")]
1298pub enum ActionCardVariant {
1299 #[default]
1300 Default,
1301 Setup,
1302 Danger,
1303}
1304
1305#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1310pub struct ActionCardProps {
1311 pub title: String,
1312 pub description: String,
1313 #[serde(default, skip_serializing_if = "Option::is_none")]
1314 pub icon: Option<String>,
1315 #[serde(default)]
1316 pub variant: ActionCardVariant,
1317 #[serde(default, skip_serializing_if = "Option::is_none")]
1319 pub href: Option<String>,
1320}
1321
1322#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1327pub struct ProductTileProps {
1328 pub product_id: String,
1329 pub name: String,
1330 pub price: String,
1331 pub field: String,
1332 #[serde(default, skip_serializing_if = "Option::is_none")]
1333 pub default_quantity: Option<u32>,
1334}
1335
1336fn deserialize_actions_lax<'de, D: serde::Deserializer<'de>>(
1342 d: D,
1343) -> Result<Vec<String>, D::Error> {
1344 use serde::de::Error;
1345 let v = serde_json::Value::deserialize(d)?;
1346 match v {
1347 serde_json::Value::Null => Ok(Vec::new()),
1348 serde_json::Value::String(s) if s.is_empty() => Ok(Vec::new()),
1349 serde_json::Value::Array(arr) => arr
1350 .into_iter()
1351 .map(|item| {
1352 item.as_str()
1353 .map(String::from)
1354 .ok_or_else(|| D::Error::custom("PageHeader.actions: expected string in array"))
1355 })
1356 .collect(),
1357 other => Err(D::Error::custom(format!(
1358 "PageHeader.actions: expected null, empty string, or array of strings; got {other:?}"
1359 ))),
1360 }
1361}
1362
1363#[cfg(test)]
1364mod schema_smoke_tests {
1365 use super::*;
1376
1377 fn assert_schema_nonempty_object<T: schemars::JsonSchema>(type_label: &str) {
1378 let schema = schemars::schema_for!(T);
1379 let value = serde_json::to_value(&schema).expect("schema serializes to JSON");
1380 assert!(
1381 value.is_object(),
1382 "{type_label}: schema must be a JSON object"
1383 );
1384 let props = value
1385 .get("properties")
1386 .and_then(|p| p.as_object())
1387 .map(|o| !o.is_empty())
1388 .unwrap_or(false);
1389 assert!(
1390 props,
1391 "{type_label}: schema must have a non-empty `properties` field"
1392 );
1393 }
1394
1395 #[test]
1396 fn schema_for_card_props_generates() {
1397 assert_schema_nonempty_object::<CardProps>("CardProps");
1398 }
1399
1400 #[test]
1401 fn schema_for_table_props_generates() {
1402 assert_schema_nonempty_object::<TableProps>("TableProps");
1403 }
1404
1405 #[test]
1406 fn schema_for_form_props_generates() {
1407 assert_schema_nonempty_object::<FormProps>("FormProps");
1408 }
1409
1410 #[test]
1411 fn schema_for_button_props_generates() {
1412 assert_schema_nonempty_object::<ButtonProps>("ButtonProps");
1413 }
1414
1415 #[test]
1416 fn schema_for_input_props_generates() {
1417 assert_schema_nonempty_object::<InputProps>("InputProps");
1418 }
1419
1420 #[test]
1421 fn schema_for_select_props_generates() {
1422 assert_schema_nonempty_object::<SelectProps>("SelectProps");
1423 }
1424
1425 #[test]
1426 fn schema_for_alert_props_generates() {
1427 assert_schema_nonempty_object::<AlertProps>("AlertProps");
1428 }
1429
1430 #[test]
1431 fn schema_for_badge_props_generates() {
1432 assert_schema_nonempty_object::<BadgeProps>("BadgeProps");
1433 }
1434
1435 #[test]
1436 fn schema_for_modal_props_generates() {
1437 assert_schema_nonempty_object::<ModalProps>("ModalProps");
1438 }
1439
1440 #[test]
1441 fn schema_for_text_props_generates() {
1442 assert_schema_nonempty_object::<TextProps>("TextProps");
1443 }
1444
1445 #[test]
1446 fn schema_for_checkbox_props_generates() {
1447 assert_schema_nonempty_object::<CheckboxProps>("CheckboxProps");
1448 }
1449
1450 #[test]
1451 fn schema_for_switch_props_generates() {
1452 assert_schema_nonempty_object::<SwitchProps>("SwitchProps");
1453 }
1454
1455 #[test]
1456 fn schema_for_separator_props_generates() {
1457 assert_schema_nonempty_object::<SeparatorProps>("SeparatorProps");
1458 }
1459
1460 #[test]
1461 fn schema_for_description_list_props_generates() {
1462 assert_schema_nonempty_object::<DescriptionListProps>("DescriptionListProps");
1463 }
1464
1465 #[test]
1466 fn schema_for_tab_generates() {
1467 assert_schema_nonempty_object::<Tab>("Tab");
1468 }
1469
1470 #[test]
1471 fn schema_for_tabs_props_generates() {
1472 assert_schema_nonempty_object::<TabsProps>("TabsProps");
1473 }
1474
1475 #[test]
1476 fn schema_for_breadcrumb_props_generates() {
1477 assert_schema_nonempty_object::<BreadcrumbProps>("BreadcrumbProps");
1478 }
1479
1480 #[test]
1481 fn schema_for_pagination_props_generates() {
1482 assert_schema_nonempty_object::<PaginationProps>("PaginationProps");
1483 }
1484
1485 #[test]
1486 fn schema_for_progress_props_generates() {
1487 assert_schema_nonempty_object::<ProgressProps>("ProgressProps");
1488 }
1489
1490 #[test]
1491 fn schema_for_image_props_generates() {
1492 assert_schema_nonempty_object::<ImageProps>("ImageProps");
1493 }
1494
1495 #[test]
1496 fn image_inline_svg_factory_roundtrips_via_serde() {
1497 let p = ImageProps::inline_svg("<svg/>", "alt");
1498 let json = serde_json::to_value(&p).expect("serialization must not fail");
1499 let parsed: ImageProps =
1500 serde_json::from_value(json).expect("deserialization must not fail");
1501 assert_eq!(parsed.inline_svg, Some("<svg/>".to_string()));
1502 assert_eq!(parsed.alt, "alt");
1503 assert_eq!(parsed.src, "");
1504 }
1505
1506 #[test]
1507 fn schema_for_avatar_props_generates() {
1508 assert_schema_nonempty_object::<AvatarProps>("AvatarProps");
1509 }
1510
1511 #[test]
1512 fn schema_for_skeleton_props_generates() {
1513 assert_schema_nonempty_object::<SkeletonProps>("SkeletonProps");
1514 }
1515
1516 #[test]
1517 fn schema_for_stat_card_props_generates() {
1518 assert_schema_nonempty_object::<StatCardProps>("StatCardProps");
1519 }
1520
1521 #[test]
1522 fn schema_for_checklist_props_generates() {
1523 assert_schema_nonempty_object::<ChecklistProps>("ChecklistProps");
1524 }
1525
1526 #[test]
1527 fn schema_for_toast_props_generates() {
1528 assert_schema_nonempty_object::<ToastProps>("ToastProps");
1529 }
1530
1531 #[test]
1532 fn schema_for_notification_dropdown_props_generates() {
1533 assert_schema_nonempty_object::<NotificationDropdownProps>("NotificationDropdownProps");
1534 }
1535
1536 #[test]
1537 fn schema_for_sidebar_props_generates() {
1538 assert_schema_nonempty_object::<SidebarProps>("SidebarProps");
1539 }
1540
1541 #[test]
1542 fn schema_for_header_props_generates() {
1543 assert_schema_nonempty_object::<HeaderProps>("HeaderProps");
1544 }
1545
1546 #[test]
1547 fn schema_for_grid_props_generates() {
1548 assert_schema_nonempty_object::<GridProps>("GridProps");
1549 }
1550
1551 #[test]
1552 fn schema_for_collapsible_props_generates() {
1553 assert_schema_nonempty_object::<CollapsibleProps>("CollapsibleProps");
1554 }
1555
1556 #[test]
1557 fn schema_for_empty_state_props_generates() {
1558 assert_schema_nonempty_object::<EmptyStateProps>("EmptyStateProps");
1559 }
1560
1561 #[test]
1562 fn schema_for_form_section_props_generates() {
1563 assert_schema_nonempty_object::<FormSectionProps>("FormSectionProps");
1564 }
1565
1566 #[test]
1567 fn schema_for_page_header_props_generates() {
1568 assert_schema_nonempty_object::<PageHeaderProps>("PageHeaderProps");
1569 }
1570
1571 #[test]
1572 fn schema_for_button_group_props_generates() {
1573 assert_schema_nonempty_object::<ButtonGroupProps>("ButtonGroupProps");
1574 }
1575
1576 #[test]
1577 fn schema_for_action_item_generates() {
1578 assert_schema_nonempty_object::<ActionItem>("ActionItem");
1579 }
1580
1581 #[test]
1582 fn schema_for_action_group_props_generates() {
1583 assert_schema_nonempty_object::<ActionGroupProps>("ActionGroupProps");
1584 }
1585
1586 #[test]
1587 fn schema_for_dropdown_menu_action_generates() {
1588 assert_schema_nonempty_object::<DropdownMenuAction>("DropdownMenuAction");
1589 }
1590
1591 #[test]
1592 fn schema_for_data_table_props_generates() {
1593 assert_schema_nonempty_object::<DataTableProps>("DataTableProps");
1594 }
1595
1596 #[test]
1597 fn schema_for_kanban_column_props_generates() {
1598 assert_schema_nonempty_object::<KanbanColumnProps>("KanbanColumnProps");
1599 }
1600
1601 #[test]
1602 fn schema_for_kanban_board_props_generates() {
1603 assert_schema_nonempty_object::<KanbanBoardProps>("KanbanBoardProps");
1604 }
1605
1606 #[test]
1607 fn schema_for_calendar_cell_props_generates() {
1608 assert_schema_nonempty_object::<CalendarCellProps>("CalendarCellProps");
1609 }
1610
1611 #[test]
1612 fn schema_for_action_card_props_generates() {
1613 assert_schema_nonempty_object::<ActionCardProps>("ActionCardProps");
1614 }
1615
1616 #[test]
1617 fn schema_for_product_tile_props_generates() {
1618 assert_schema_nonempty_object::<ProductTileProps>("ProductTileProps");
1619 }
1620
1621 #[test]
1622 fn card_props_round_trips_footer() {
1623 let original = CardProps {
1624 title: "Hero".to_string(),
1625 description: None,
1626 subtitle: None,
1627 badge: None,
1628 max_width: None,
1629 footer: vec!["btn1".to_string(), "btn2".to_string()],
1630 variant: CardVariant::Bordered,
1631 };
1632 let json = serde_json::to_string(&original).unwrap();
1633 let parsed: CardProps = serde_json::from_str(&json).unwrap();
1634 assert_eq!(original.footer, parsed.footer);
1635 }
1636
1637 #[test]
1638 fn tab_round_trips_children() {
1639 let original = Tab {
1640 value: "overview".to_string(),
1641 label: "Overview".to_string(),
1642 children: vec!["panel1".to_string()],
1643 };
1644 let json = serde_json::to_string(&original).unwrap();
1645 let parsed: Tab = serde_json::from_str(&json).unwrap();
1646 assert_eq!(original.children, parsed.children);
1647 }
1648
1649 #[test]
1650 fn card_props_omits_empty_footer_in_json() {
1651 let card = CardProps {
1652 title: "Card".to_string(),
1653 description: None,
1654 subtitle: None,
1655 badge: None,
1656 max_width: None,
1657 footer: Vec::new(),
1658 variant: CardVariant::Bordered,
1659 };
1660 let json = serde_json::to_string(&card).unwrap();
1661 assert!(
1662 !json.contains("\"footer\""),
1663 "empty footer must be skipped, got: {json}"
1664 );
1665 }
1666
1667 #[test]
1668 fn card_props_round_trips_badge() {
1669 let original = CardProps {
1670 title: "Hero".to_string(),
1671 description: None,
1672 subtitle: None,
1673 badge: Some("Scade tra 9m".to_string()),
1674 max_width: None,
1675 footer: Vec::new(),
1676 variant: CardVariant::Bordered,
1677 };
1678 let json = serde_json::to_string(&original).unwrap();
1679 let parsed: CardProps = serde_json::from_str(&json).unwrap();
1680 assert_eq!(original.badge, parsed.badge);
1681 }
1682
1683 #[test]
1684 fn card_props_omits_empty_badge_in_json() {
1685 let card = CardProps {
1686 title: "Card".to_string(),
1687 description: None,
1688 subtitle: None,
1689 badge: None,
1690 max_width: None,
1691 footer: Vec::new(),
1692 variant: CardVariant::Bordered,
1693 };
1694 let json = serde_json::to_string(&card).unwrap();
1695 assert!(
1696 !json.contains("\"badge\""),
1697 "empty badge must be skipped, got: {json}"
1698 );
1699 }
1700
1701 #[test]
1702 fn card_props_round_trips_subtitle() {
1703 let original = CardProps {
1704 title: "Hero".to_string(),
1705 description: None,
1706 subtitle: Some("Marco Rossi".to_string()),
1707 badge: None,
1708 max_width: None,
1709 footer: Vec::new(),
1710 variant: CardVariant::Bordered,
1711 };
1712 let json = serde_json::to_string(&original).unwrap();
1713 let parsed: CardProps = serde_json::from_str(&json).unwrap();
1714 assert_eq!(original.subtitle, parsed.subtitle);
1715 }
1716
1717 #[test]
1718 fn card_props_omits_empty_subtitle_in_json() {
1719 let card = CardProps {
1720 title: "Card".to_string(),
1721 description: None,
1722 subtitle: None,
1723 badge: None,
1724 max_width: None,
1725 footer: Vec::new(),
1726 variant: CardVariant::Bordered,
1727 };
1728 let json = serde_json::to_string(&card).unwrap();
1729 assert!(
1730 !json.contains("\"subtitle\""),
1731 "empty subtitle must be skipped, got: {json}"
1732 );
1733 }
1734
1735 #[test]
1736 fn card_props_schema_includes_badge() {
1737 let schema = schemars::schema_for!(CardProps);
1738 let value = serde_json::to_value(&schema).expect("schema serializes to JSON");
1739 let props = value
1740 .get("properties")
1741 .and_then(|p| p.as_object())
1742 .expect("schema has a properties object");
1743 assert!(
1744 props.contains_key("badge"),
1745 "CardProps schema must expose a `badge` property; got keys: {:?}",
1746 props.keys().collect::<Vec<_>>()
1747 );
1748 let badge_schema = props.get("badge").expect("badge entry");
1753 let badge_json = badge_schema.to_string();
1754 assert!(
1755 badge_json.contains("\"string\""),
1756 "badge schema entry must mention string type; got: {badge_json}"
1757 );
1758 }
1759
1760 #[test]
1761 fn card_props_schema_includes_subtitle() {
1762 let schema = schemars::schema_for!(CardProps);
1763 let value = serde_json::to_value(&schema).expect("schema serializes to JSON");
1764 let props = value
1765 .get("properties")
1766 .and_then(|p| p.as_object())
1767 .expect("schema has a properties object");
1768 assert!(
1769 props.contains_key("subtitle"),
1770 "CardProps schema must expose a `subtitle` property; got keys: {:?}",
1771 props.keys().collect::<Vec<_>>()
1772 );
1773 let subtitle_schema = props.get("subtitle").expect("subtitle entry");
1778 let subtitle_json = subtitle_schema.to_string();
1779 assert!(
1780 subtitle_json.contains("\"string\""),
1781 "subtitle schema entry must mention string type; got: {subtitle_json}"
1782 );
1783 }
1784
1785 #[test]
1786 fn schema_for_checkbox_list_props_generates() {
1787 assert_schema_nonempty_object::<CheckboxListProps>("CheckboxListProps");
1788 }
1789
1790 #[test]
1791 fn checkbox_list_props_serde_roundtrip() {
1792 let json = serde_json::json!({
1793 "field": "services",
1794 "options": [{"value": "a", "label": "Alpha"}, {"value": "b", "label": "Beta"}],
1795 "selected_path": "/preselected"
1796 });
1797 let parsed: CheckboxListProps = serde_json::from_value(json.clone()).expect("decode");
1798 assert_eq!(parsed.field, "services");
1799 assert_eq!(parsed.options.len(), 2);
1800 assert_eq!(parsed.selected_path.as_deref(), Some("/preselected"));
1801 let reserialized = serde_json::to_value(&parsed).expect("encode");
1802 assert!(reserialized.get("label").is_none());
1804 assert!(reserialized.get("disabled").is_none());
1805 }
1806
1807 #[test]
1808 fn schema_for_rich_text_editor_props_generates() {
1809 assert_schema_nonempty_object::<RichTextEditorProps>("RichTextEditorProps");
1810 }
1811
1812 #[test]
1813 fn rich_text_editor_props_serde_roundtrip() {
1814 let json = serde_json::json!({
1815 "field": "body",
1816 "label": "Body"
1817 });
1818 let parsed: RichTextEditorProps = serde_json::from_value(json).expect("decode");
1819 assert_eq!(parsed.field, "body");
1820 assert_eq!(parsed.label, "Body");
1821 assert!(parsed.placeholder.is_none());
1822 assert!(parsed.default_value.is_none());
1823 assert!(parsed.data_path.is_none());
1824 assert!(parsed.error.is_none());
1825 let reserialized = serde_json::to_value(&parsed).expect("encode");
1826 assert!(reserialized.get("placeholder").is_none());
1828 assert!(reserialized.get("error").is_none());
1829 }
1830}
1831
1832#[cfg(test)]
1833mod strum_tests {
1834 use super::*;
1835
1836 #[test]
1840 fn variant_enums_strum_matches_serde_wire_format() {
1841 fn check<T: AsRef<str> + serde::Serialize>(variants: &[T], label: &str) {
1842 for v in variants {
1843 let json = serde_json::to_string(v).expect("serialize");
1844 let json_stripped = json.trim_matches('"');
1845 assert_eq!(
1846 v.as_ref(),
1847 json_stripped,
1848 "strum AsRefStr drifted from serde for {label} variant"
1849 );
1850 }
1851 }
1852 check(
1853 &[
1854 AlertVariant::Info,
1855 AlertVariant::Success,
1856 AlertVariant::Warning,
1857 AlertVariant::Error,
1858 ],
1859 "AlertVariant",
1860 );
1861 check(
1862 &[
1863 BadgeVariant::Default,
1864 BadgeVariant::Secondary,
1865 BadgeVariant::Destructive,
1866 BadgeVariant::Outline,
1867 ],
1868 "BadgeVariant",
1869 );
1870 check(
1871 &[
1872 ButtonVariant::Default,
1873 ButtonVariant::Secondary,
1874 ButtonVariant::Destructive,
1875 ButtonVariant::Outline,
1876 ButtonVariant::Ghost,
1877 ButtonVariant::Link,
1878 ],
1879 "ButtonVariant",
1880 );
1881 check(
1882 &[
1883 ToastVariant::Info,
1884 ToastVariant::Success,
1885 ToastVariant::Warning,
1886 ToastVariant::Error,
1887 ],
1888 "ToastVariant",
1889 );
1890 }
1891
1892 #[test]
1893 fn alert_variant_as_ref_str_matches_wire_format() {
1894 assert_eq!(AlertVariant::Success.as_ref(), "success");
1895 assert_eq!(AlertVariant::Warning.as_ref(), "warning");
1896 assert_eq!(AlertVariant::Info.as_ref(), "info");
1897 assert_eq!(AlertVariant::Error.as_ref(), "error");
1898 }
1899}
1900
1901#[cfg(test)]
1902mod card_variant_tests {
1903 use super::*;
1904
1905 #[test]
1906 fn card_variant_default_is_bordered() {
1907 assert_eq!(CardVariant::default(), CardVariant::Bordered);
1908 }
1909
1910 #[test]
1911 fn card_variant_serializes_snake_case() {
1912 assert_eq!(
1913 serde_json::to_value(CardVariant::Bordered).unwrap(),
1914 serde_json::json!("bordered")
1915 );
1916 assert_eq!(
1917 serde_json::to_value(CardVariant::Elevated).unwrap(),
1918 serde_json::json!("elevated")
1919 );
1920 }
1921
1922 #[test]
1923 fn card_variant_deserializes_snake_case() {
1924 assert_eq!(
1925 serde_json::from_value::<CardVariant>(serde_json::json!("bordered")).unwrap(),
1926 CardVariant::Bordered
1927 );
1928 assert_eq!(
1929 serde_json::from_value::<CardVariant>(serde_json::json!("elevated")).unwrap(),
1930 CardVariant::Elevated
1931 );
1932 }
1933
1934 #[test]
1935 fn card_props_without_variant_defaults_to_bordered() {
1936 let v = serde_json::json!({"title": "x"});
1937 let p: CardProps = serde_json::from_value(v).unwrap();
1938 assert_eq!(p.variant, CardVariant::Bordered);
1939 }
1940
1941 #[test]
1942 fn card_props_with_elevated_variant() {
1943 let v = serde_json::json!({"title": "x", "variant": "elevated"});
1944 let p: CardProps = serde_json::from_value(v).unwrap();
1945 assert_eq!(p.variant, CardVariant::Elevated);
1946 }
1947
1948 #[test]
1949 fn card_props_roundtrip_preserves_variant() {
1950 let p = CardProps {
1951 title: "x".into(),
1952 description: None,
1953 subtitle: None,
1954 badge: None,
1955 max_width: None,
1956 footer: vec![],
1957 variant: CardVariant::Elevated,
1958 };
1959 let j = serde_json::to_value(&p).unwrap();
1960 let back: CardProps = serde_json::from_value(j).unwrap();
1961 assert_eq!(back.variant, CardVariant::Elevated);
1962 }
1963}
1964
1965#[cfg(test)]
1966mod kanban_board_props_tests {
1967 use super::*;
1968
1969 #[test]
1970 fn kanban_board_props_serde_static_columns() {
1971 let v = serde_json::json!({
1972 "columns": [{"title": "To Do", "id": "todo", "count": 0}]
1973 });
1974 let p: KanbanBoardProps = serde_json::from_value(v).unwrap();
1975 assert_eq!(p.columns.len(), 1);
1976 assert!(p.items_path.is_none());
1977 assert!(p.group_by.is_none());
1978 }
1979
1980 #[test]
1981 fn kanban_board_props_serde_data_bound() {
1982 let v = serde_json::json!({
1983 "columns": [{"title": "Open", "id": "open"}],
1984 "items_path": "/data/order",
1985 "group_by": "status",
1986 "card_title_key": "name"
1987 });
1988 let p: KanbanBoardProps = serde_json::from_value(v).unwrap();
1989 assert_eq!(p.columns.len(), 1);
1990 assert_eq!(p.items_path.as_deref(), Some("/data/order"));
1991 assert_eq!(p.group_by.as_deref(), Some("status"));
1992 assert_eq!(p.card_title_key.as_deref(), Some("name"));
1993 }
1994
1995 #[test]
1996 fn kanban_board_props_serde_neither() {
1997 let v = serde_json::json!({});
1998 let p: KanbanBoardProps = serde_json::from_value(v).unwrap();
1999 assert!(p.columns.is_empty());
2000 assert!(p.items_path.is_none());
2001 assert!(p.group_by.is_none());
2002 }
2003
2004 #[test]
2005 fn kanban_board_props_empty_columns_skipped_on_serialize() {
2006 let p = KanbanBoardProps {
2007 columns: vec![],
2008 items_path: Some("/data/order".into()),
2009 group_by: Some("status".into()),
2010 card_title_key: None,
2011 card_description_key: None,
2012 row_actions: None,
2013 row_key: None,
2014 mobile_default_column: None,
2015 empty_label: None,
2016 };
2017 let j = serde_json::to_value(&p).unwrap();
2018 assert!(
2019 j.get("columns").is_none(),
2020 "empty columns must be skipped, got: {j}"
2021 );
2022 assert_eq!(
2023 j.get("items_path").and_then(|v| v.as_str()),
2024 Some("/data/order")
2025 );
2026 }
2027}
2028
2029#[cfg(test)]
2030mod page_header_actions_tests {
2031 use super::*;
2032
2033 #[test]
2034 fn page_header_actions_missing_field() {
2035 let v = serde_json::json!({"title": "X"});
2036 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
2037 assert!(p.actions.is_empty());
2038 }
2039
2040 #[test]
2041 fn page_header_actions_null() {
2042 let v = serde_json::json!({"title": "X", "actions": null});
2043 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
2044 assert!(p.actions.is_empty());
2045 }
2046
2047 #[test]
2048 fn page_header_actions_empty_string() {
2049 let v = serde_json::json!({"title": "X", "actions": ""});
2050 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
2051 assert!(p.actions.is_empty());
2052 }
2053
2054 #[test]
2055 fn page_header_actions_empty_array() {
2056 let v = serde_json::json!({"title": "X", "actions": []});
2057 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
2058 assert!(p.actions.is_empty());
2059 }
2060
2061 #[test]
2062 fn page_header_actions_non_empty_array() {
2063 let v = serde_json::json!({"title": "X", "actions": ["a", "b"]});
2064 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
2065 assert_eq!(p.actions, vec!["a".to_string(), "b".to_string()]);
2066 }
2067
2068 #[test]
2069 fn page_header_actions_non_empty_string_rejected() {
2070 let v = serde_json::json!({"title": "X", "actions": "not-empty"});
2071 let result: Result<PageHeaderProps, _> = serde_json::from_value(v);
2072 assert!(result.is_err(), "non-empty string must be rejected");
2073 }
2074
2075 #[test]
2076 fn page_header_actions_non_string_array_rejected() {
2077 let v = serde_json::json!({"title": "X", "actions": [1, 2, 3]});
2078 let result: Result<PageHeaderProps, _> = serde_json::from_value(v);
2079 assert!(result.is_err(), "array of non-strings must be rejected");
2080 }
2081}