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}
144
145#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
147#[serde(rename_all = "snake_case")]
148pub enum ColumnAlign {
149 #[default]
150 Left,
151 Center,
152 Right,
153}
154
155#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
157pub struct Column {
158 pub key: String,
159 pub label: String,
160 #[serde(default, skip_serializing_if = "Option::is_none")]
161 pub format: Option<ColumnFormat>,
162 #[serde(default, skip_serializing_if = "Option::is_none")]
165 pub align: Option<ColumnAlign>,
166}
167
168#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
170pub struct SelectOption {
171 pub value: String,
172 pub label: String,
173}
174
175#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
182#[serde(rename_all = "snake_case")]
183pub enum CardVariant {
184 #[default]
185 Bordered,
186 Elevated,
187}
188
189#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
191pub struct CardProps {
192 pub title: String,
193 #[serde(default, skip_serializing_if = "Option::is_none")]
194 pub description: Option<String>,
195 #[serde(default, skip_serializing_if = "Option::is_none")]
199 pub subtitle: Option<String>,
200 #[serde(default, skip_serializing_if = "Option::is_none")]
204 pub badge: Option<String>,
205 #[serde(default, skip_serializing_if = "Option::is_none")]
206 pub max_width: Option<FormMaxWidth>,
207 #[serde(default, skip_serializing_if = "Vec::is_empty")]
209 pub footer: Vec<String>,
210 #[serde(default)]
211 pub variant: CardVariant,
212}
213
214#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
216pub struct TableProps {
217 pub columns: Vec<Column>,
218 pub data_path: String,
219 #[serde(default, skip_serializing_if = "Option::is_none")]
220 pub row_actions: Option<Vec<Action>>,
221 #[serde(default, skip_serializing_if = "Option::is_none")]
222 pub empty_message: Option<String>,
223 #[serde(default, skip_serializing_if = "Option::is_none")]
224 pub sortable: Option<bool>,
225 #[serde(default, skip_serializing_if = "Option::is_none")]
226 pub sort_column: Option<String>,
227 #[serde(default, skip_serializing_if = "Option::is_none")]
228 pub sort_direction: Option<SortDirection>,
229}
230
231#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
233#[serde(rename_all = "snake_case")]
234pub enum FormMaxWidth {
235 #[default]
236 Default,
237 Narrow,
238 Wide,
239}
240
241#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
243pub struct FormProps {
244 pub action: Action,
245 #[serde(default, skip_serializing_if = "Option::is_none")]
246 pub method: Option<crate::action::HttpMethod>,
247 #[serde(default, skip_serializing_if = "Option::is_none")]
251 pub guard: Option<String>,
252 #[serde(default, skip_serializing_if = "Option::is_none")]
254 pub max_width: Option<FormMaxWidth>,
255 #[serde(default, skip_serializing_if = "Option::is_none")]
259 pub id: Option<String>,
260 #[serde(default, skip_serializing_if = "Option::is_none")]
265 pub enctype: Option<String>,
266}
267
268#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
270#[serde(rename_all = "snake_case")]
271pub enum ButtonType {
272 #[default]
273 Button,
274 Submit,
275}
276
277#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
279pub struct ButtonProps {
280 pub label: String,
281 #[serde(default)]
282 pub variant: ButtonVariant,
283 #[serde(default)]
284 pub size: Size,
285 #[serde(default, skip_serializing_if = "Option::is_none")]
286 pub disabled: Option<bool>,
287 #[serde(default, skip_serializing_if = "Option::is_none")]
288 pub icon: Option<String>,
289 #[serde(default, skip_serializing_if = "Option::is_none")]
290 pub icon_position: Option<IconPosition>,
291 #[serde(default, skip_serializing_if = "Option::is_none")]
292 pub button_type: Option<ButtonType>,
293 #[serde(default, skip_serializing_if = "Option::is_none")]
297 pub form: Option<String>,
298}
299
300#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
302pub struct InputProps {
303 pub field: String,
305 pub label: String,
306 #[serde(default)]
307 pub input_type: InputType,
308 #[serde(default, skip_serializing_if = "Option::is_none")]
309 pub placeholder: Option<String>,
310 #[serde(default, skip_serializing_if = "Option::is_none")]
311 pub required: Option<bool>,
312 #[serde(default, skip_serializing_if = "Option::is_none")]
313 pub disabled: Option<bool>,
314 #[serde(default, skip_serializing_if = "Option::is_none")]
315 pub error: Option<String>,
316 #[serde(default, skip_serializing_if = "Option::is_none")]
317 pub description: Option<String>,
318 #[serde(default, skip_serializing_if = "Option::is_none")]
319 pub default_value: Option<String>,
320 #[serde(default, skip_serializing_if = "Option::is_none")]
322 pub data_path: Option<String>,
323 #[serde(default, skip_serializing_if = "Option::is_none")]
325 pub step: Option<String>,
326 #[serde(default, skip_serializing_if = "Option::is_none")]
330 pub list: Option<String>,
331 #[serde(default, skip_serializing_if = "Option::is_none")]
336 pub accept: Option<String>,
337}
338
339#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
350pub struct RichTextEditorProps {
351 pub field: String,
352 pub label: String,
353 #[serde(default, skip_serializing_if = "Option::is_none")]
354 pub placeholder: Option<String>,
355 #[serde(default, skip_serializing_if = "Option::is_none")]
356 pub default_value: Option<String>,
357 #[serde(default, skip_serializing_if = "Option::is_none")]
358 pub data_path: Option<String>,
359 #[serde(default, skip_serializing_if = "Option::is_none")]
360 pub error: Option<String>,
361}
362
363#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
365pub struct SelectProps {
366 pub field: String,
368 pub label: String,
369 pub options: Vec<SelectOption>,
370 #[serde(default, skip_serializing_if = "Option::is_none")]
371 pub placeholder: Option<String>,
372 #[serde(default, skip_serializing_if = "Option::is_none")]
373 pub required: Option<bool>,
374 #[serde(default, skip_serializing_if = "Option::is_none")]
375 pub disabled: Option<bool>,
376 #[serde(default, skip_serializing_if = "Option::is_none")]
377 pub error: Option<String>,
378 #[serde(default, skip_serializing_if = "Option::is_none")]
379 pub description: Option<String>,
380 #[serde(default, skip_serializing_if = "Option::is_none")]
381 pub default_value: Option<String>,
382 #[serde(default, skip_serializing_if = "Option::is_none")]
384 pub data_path: Option<String>,
385}
386
387#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
389pub struct AlertProps {
390 pub message: String,
391 #[serde(default)]
392 pub variant: AlertVariant,
393 #[serde(default, skip_serializing_if = "Option::is_none")]
394 pub title: Option<String>,
395}
396
397#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
399pub struct BadgeProps {
400 pub label: String,
401 #[serde(default)]
402 pub variant: BadgeVariant,
403}
404
405#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
407pub struct ModalProps {
408 pub id: String,
409 pub title: String,
410 #[serde(default, skip_serializing_if = "Option::is_none")]
411 pub description: Option<String>,
412 #[serde(default, skip_serializing_if = "Option::is_none")]
413 pub trigger_label: Option<String>,
414 #[serde(default, skip_serializing_if = "Vec::is_empty")]
416 pub footer: Vec<String>,
417}
418
419#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
421pub struct TextProps {
422 pub content: String,
423 #[serde(default)]
424 pub element: TextElement,
425}
426
427#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
429pub struct CheckboxProps {
430 pub field: String,
432 #[serde(default, skip_serializing_if = "Option::is_none")]
435 pub value: Option<String>,
436 pub label: String,
437 #[serde(default, skip_serializing_if = "Option::is_none")]
438 pub description: Option<String>,
439 #[serde(default, skip_serializing_if = "Option::is_none")]
440 pub checked: Option<bool>,
441 #[serde(default, skip_serializing_if = "Option::is_none")]
443 pub data_path: Option<String>,
444 #[serde(default, skip_serializing_if = "Option::is_none")]
445 pub required: Option<bool>,
446 #[serde(default, skip_serializing_if = "Option::is_none")]
447 pub disabled: Option<bool>,
448 #[serde(default, skip_serializing_if = "Option::is_none")]
449 pub error: Option<String>,
450}
451
452#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
458pub struct CheckboxListProps {
459 pub field: String,
461 #[serde(default, skip_serializing_if = "Vec::is_empty")]
464 pub options: Vec<SelectOption>,
465 #[serde(default, skip_serializing_if = "Option::is_none")]
467 pub options_path: Option<String>,
468 #[serde(default, skip_serializing_if = "Option::is_none")]
470 pub selected_path: Option<String>,
471 #[serde(default, skip_serializing_if = "Option::is_none")]
472 pub label: Option<String>,
473 #[serde(default, skip_serializing_if = "Option::is_none")]
474 pub description: Option<String>,
475 #[serde(default, skip_serializing_if = "Option::is_none")]
476 pub disabled: Option<bool>,
477 #[serde(default, skip_serializing_if = "Option::is_none")]
478 pub error: Option<String>,
479}
480
481#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
483pub struct SwitchProps {
484 pub field: String,
486 pub label: String,
487 #[serde(default, skip_serializing_if = "Option::is_none")]
488 pub description: Option<String>,
489 #[serde(default, skip_serializing_if = "Option::is_none")]
490 pub checked: Option<bool>,
491 #[serde(default, skip_serializing_if = "Option::is_none")]
493 pub data_path: Option<String>,
494 #[serde(default, skip_serializing_if = "Option::is_none")]
495 pub required: Option<bool>,
496 #[serde(default, skip_serializing_if = "Option::is_none")]
497 pub disabled: Option<bool>,
498 #[serde(default, skip_serializing_if = "Option::is_none")]
499 pub error: Option<String>,
500 #[serde(default, skip_serializing_if = "Option::is_none")]
503 pub action: Option<Action>,
504 #[serde(default, skip_serializing_if = "Option::is_none")]
507 pub compact: Option<bool>,
508}
509
510#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
512pub struct SeparatorProps {
513 #[serde(default, skip_serializing_if = "Option::is_none")]
514 pub orientation: Option<Orientation>,
515}
516
517#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
519pub struct DescriptionItem {
520 pub label: String,
521 pub value: String,
522 #[serde(default, skip_serializing_if = "Option::is_none")]
523 pub format: Option<ColumnFormat>,
524}
525
526#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
528pub struct DescriptionListProps {
529 #[serde(default, skip_serializing_if = "Vec::is_empty")]
530 pub items: Vec<DescriptionItem>,
531 #[serde(default, skip_serializing_if = "Option::is_none")]
532 pub columns: Option<u8>,
533 #[serde(default, skip_serializing_if = "Option::is_none")]
537 pub data_path: Option<String>,
538}
539
540#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
542pub struct Tab {
543 pub value: String,
544 pub label: String,
545 #[serde(default, skip_serializing_if = "Vec::is_empty")]
547 pub children: Vec<String>,
548}
549
550#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
552pub struct TabsProps {
553 pub default_tab: String,
554 pub tabs: Vec<Tab>,
555}
556
557#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
559pub struct BreadcrumbItem {
560 pub label: String,
561 #[serde(default, skip_serializing_if = "Option::is_none")]
562 pub url: Option<String>,
563}
564
565#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
567pub struct BreadcrumbProps {
568 pub items: Vec<BreadcrumbItem>,
569}
570
571#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
573pub struct PaginationProps {
574 pub current_page: u32,
575 pub per_page: u32,
576 pub total: u32,
577 #[serde(default, skip_serializing_if = "Option::is_none")]
578 pub base_url: Option<String>,
579}
580
581#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
583pub struct ProgressProps {
584 pub value: u8,
586 #[serde(default, skip_serializing_if = "Option::is_none")]
587 pub max: Option<u8>,
588 #[serde(default, skip_serializing_if = "Option::is_none")]
589 pub label: Option<String>,
590}
591
592#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
594pub struct ImageProps {
595 #[serde(default)]
596 pub src: String,
597 pub alt: String,
598 #[serde(default, skip_serializing_if = "Option::is_none")]
599 pub aspect_ratio: Option<String>,
600 #[serde(default, skip_serializing_if = "Option::is_none")]
605 pub placeholder_label: Option<String>,
606 #[serde(default, skip_serializing_if = "Option::is_none")]
614 pub inline_svg: Option<String>,
615 #[serde(default, skip_serializing_if = "Option::is_none")]
619 pub data_path: Option<String>,
620}
621
622impl ImageProps {
623 pub fn inline_svg(svg: impl Into<String>, alt: impl Into<String>) -> Self {
629 Self {
630 src: String::new(),
631 alt: alt.into(),
632 aspect_ratio: None,
633 placeholder_label: None,
634 inline_svg: Some(svg.into()),
635 data_path: None,
636 }
637 }
638}
639
640#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
642pub struct AvatarProps {
643 #[serde(default, skip_serializing_if = "Option::is_none")]
644 pub src: Option<String>,
645 pub alt: String,
646 #[serde(default, skip_serializing_if = "Option::is_none")]
647 pub fallback: Option<String>,
648 #[serde(default, skip_serializing_if = "Option::is_none")]
649 pub size: Option<Size>,
650}
651
652#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
654pub struct SkeletonProps {
655 #[serde(default, skip_serializing_if = "Option::is_none")]
656 pub width: Option<String>,
657 #[serde(default, skip_serializing_if = "Option::is_none")]
658 pub height: Option<String>,
659 #[serde(default, skip_serializing_if = "Option::is_none")]
660 pub rounded: Option<bool>,
661}
662
663#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
680pub struct RawHtmlProps {
681 #[serde(default)]
683 pub html: String,
684}
685
686#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
692pub struct StreamTextProps {
693 #[serde(default)]
696 pub sse_url: String,
697 #[serde(default, skip_serializing_if = "Option::is_none")]
699 pub placeholder: Option<String>,
700 #[serde(default, skip_serializing_if = "Option::is_none")]
702 pub loading_text: Option<String>,
703}
704
705#[derive(
707 Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, strum::AsRefStr,
708)]
709#[serde(rename_all = "snake_case")]
710#[strum(serialize_all = "snake_case")]
711pub enum ToastVariant {
712 #[default]
713 Info,
714 Success,
715 Warning,
716 Error,
717}
718
719#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
721pub struct ChecklistItem {
722 pub label: String,
723 #[serde(default)]
724 pub checked: bool,
725 #[serde(default, skip_serializing_if = "Option::is_none")]
726 pub href: Option<String>,
727}
728
729#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
731pub struct NotificationItem {
732 #[serde(default, skip_serializing_if = "Option::is_none")]
733 pub icon: Option<String>,
734 pub text: String,
735 #[serde(default, skip_serializing_if = "Option::is_none")]
736 pub timestamp: Option<String>,
737 #[serde(default)]
738 pub read: bool,
739 #[serde(default, skip_serializing_if = "Option::is_none")]
740 pub action_url: Option<String>,
741}
742
743#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
745pub struct SidebarNavItem {
746 pub label: String,
747 pub href: String,
748 #[serde(default, skip_serializing_if = "Option::is_none")]
749 pub icon: Option<String>,
750 #[serde(default)]
751 pub active: bool,
752 #[serde(default, skip_serializing_if = "Option::is_none")]
755 pub disabled: Option<bool>,
756}
757
758#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
760pub struct SidebarGroup {
761 pub label: String,
762 #[serde(default)]
763 pub collapsed: bool,
764 pub items: Vec<SidebarNavItem>,
765}
766
767#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
769pub struct StatCardProps {
770 pub label: String,
771 pub value: String,
772 #[serde(default, skip_serializing_if = "Option::is_none")]
773 pub icon: Option<String>,
774 #[serde(default, skip_serializing_if = "Option::is_none")]
775 pub subtitle: Option<String>,
776 #[serde(default, skip_serializing_if = "Option::is_none")]
778 pub sse_target: Option<String>,
779 #[serde(default, skip_serializing_if = "Option::is_none")]
784 pub value_path: Option<String>,
785}
786
787#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
789pub struct ChecklistProps {
790 pub title: String,
791 pub items: Vec<ChecklistItem>,
792 #[serde(default = "default_true")]
793 pub dismissible: bool,
794 #[serde(default, skip_serializing_if = "Option::is_none")]
795 pub dismiss_label: Option<String>,
796 #[serde(default, skip_serializing_if = "Option::is_none")]
798 pub data_key: Option<String>,
799}
800
801fn default_true() -> bool {
802 true
803}
804
805#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
810pub struct ToastProps {
811 pub message: String,
812 #[serde(default)]
813 pub variant: ToastVariant,
814 #[serde(default, skip_serializing_if = "Option::is_none")]
816 pub timeout: Option<u32>,
817 #[serde(default = "default_true")]
818 pub dismissible: bool,
819}
820
821#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
823pub struct NotificationDropdownProps {
824 pub notifications: Vec<NotificationItem>,
825 #[serde(default, skip_serializing_if = "Option::is_none")]
826 pub empty_text: Option<String>,
827}
828
829#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
831pub struct SidebarProps {
832 #[serde(default, skip_serializing_if = "Vec::is_empty")]
833 pub fixed_top: Vec<SidebarNavItem>,
834 #[serde(default, skip_serializing_if = "Vec::is_empty")]
835 pub groups: Vec<SidebarGroup>,
836 #[serde(default, skip_serializing_if = "Vec::is_empty")]
837 pub fixed_bottom: Vec<SidebarNavItem>,
838}
839
840#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
842pub struct HeaderProps {
843 pub business_name: String,
844 #[serde(default, skip_serializing_if = "Option::is_none")]
846 pub notification_count: Option<u32>,
847 #[serde(default, skip_serializing_if = "Option::is_none")]
848 pub user_name: Option<String>,
849 #[serde(default, skip_serializing_if = "Option::is_none")]
850 pub user_avatar: Option<String>,
851 #[serde(default, skip_serializing_if = "Option::is_none")]
852 pub logout_url: Option<String>,
853}
854
855#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
857#[serde(rename_all = "snake_case")]
858pub enum GapSize {
859 None,
860 Sm,
861 #[default]
862 Md,
863 Lg,
864 Xl,
865}
866
867#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
869pub struct GridProps {
870 #[serde(default = "default_grid_columns")]
872 pub columns: u8,
873 #[serde(default, skip_serializing_if = "Option::is_none")]
875 pub md_columns: Option<u8>,
876 #[serde(default, skip_serializing_if = "Option::is_none")]
878 pub lg_columns: Option<u8>,
879 #[serde(default)]
881 pub gap: GapSize,
882 #[serde(default, skip_serializing_if = "Option::is_none")]
885 pub scrollable: Option<bool>,
886}
887
888fn default_grid_columns() -> u8 {
889 2
890}
891
892#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
894pub struct CollapsibleProps {
895 pub title: String,
896 #[serde(default)]
897 pub expanded: bool,
898}
899
900#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
902pub struct EmptyStateProps {
903 pub title: String,
904 #[serde(default, skip_serializing_if = "Option::is_none")]
905 pub description: Option<String>,
906 #[serde(default, skip_serializing_if = "Option::is_none")]
907 pub action: Option<Action>,
908 #[serde(default, skip_serializing_if = "Option::is_none")]
909 pub action_label: Option<String>,
910}
911
912#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
914#[serde(rename_all = "snake_case")]
915pub enum FormSectionLayout {
916 #[default]
917 Stacked,
918 TwoColumn,
919}
920
921#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
923pub struct FormSectionProps {
924 pub title: String,
925 #[serde(default, skip_serializing_if = "Option::is_none")]
926 pub description: Option<String>,
927 #[serde(default, skip_serializing_if = "Option::is_none")]
929 pub layout: Option<FormSectionLayout>,
930}
931
932#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
934pub struct PageHeaderProps {
935 pub title: String,
936 #[serde(default, skip_serializing_if = "Vec::is_empty")]
937 pub breadcrumb: Vec<BreadcrumbItem>,
938 #[serde(
940 default,
941 deserialize_with = "deserialize_actions_lax",
942 skip_serializing_if = "Vec::is_empty"
943 )]
944 pub actions: Vec<String>,
945}
946
947#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Default)]
949pub struct ButtonGroupProps {
950 #[serde(default)]
952 pub gap: GapSize,
953}
954
955#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
966pub struct ActionItem {
967 pub label: String,
968 pub action: Action,
969 #[serde(default)]
972 pub destructive: bool,
973 #[serde(default, skip_serializing_if = "Option::is_none")]
974 pub variant: Option<ButtonVariant>,
975 #[serde(default, skip_serializing_if = "Option::is_none")]
976 pub icon: Option<String>,
977 #[serde(default, skip_serializing_if = "Option::is_none")]
981 pub visible_if: Option<String>,
982}
983
984#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
995pub struct ActionGroupProps {
996 pub items: Vec<ActionItem>,
997 pub menu_id: String,
1000 #[serde(default, skip_serializing_if = "Option::is_none")]
1002 pub max_inline: Option<u8>,
1003 #[serde(default, skip_serializing_if = "Option::is_none")]
1005 pub overflow_label: Option<String>,
1006 #[serde(default, skip_serializing_if = "Option::is_none")]
1008 pub row_key: Option<String>,
1009}
1010
1011#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Default)]
1027pub struct SegmentedControlProps {
1028 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1030 pub items: Vec<SegmentedItem>,
1031 #[serde(default, skip_serializing_if = "Option::is_none")]
1034 pub data_path: Option<String>,
1035 #[serde(default)]
1037 pub size: Size,
1038 #[serde(default, skip_serializing_if = "Option::is_none")]
1041 pub aria_label: Option<String>,
1042}
1043
1044#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Default)]
1046pub struct SegmentedItem {
1047 pub label: String,
1049 pub href: String,
1051 #[serde(default)]
1053 pub active: bool,
1054 #[serde(default, skip_serializing_if = "Option::is_none")]
1057 pub aria_label: Option<String>,
1058}
1059
1060#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Default)]
1076pub struct SidebarLayoutProps {
1077 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1079 pub items: Vec<SidebarLayoutItem>,
1080 #[serde(default, skip_serializing_if = "Option::is_none")]
1082 pub data_path: Option<String>,
1083 pub active: String,
1086 #[serde(default, skip_serializing_if = "Option::is_none")]
1088 pub aria_label: Option<String>,
1089}
1090
1091#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Default)]
1093pub struct SidebarLayoutItem {
1094 pub slug: String,
1097 pub label: String,
1099 pub url: String,
1102}
1103
1104#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1113pub struct DetailPageProps {
1114 pub title: String,
1115 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1116 pub breadcrumb: Vec<BreadcrumbItem>,
1117 #[serde(
1119 default,
1120 deserialize_with = "deserialize_actions_lax",
1121 skip_serializing_if = "Vec::is_empty"
1122 )]
1123 pub actions: Vec<String>,
1124 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1127 pub info: Vec<String>,
1128}
1129
1130#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1132pub struct DropdownMenuAction {
1133 pub label: String,
1134 pub action: Action,
1135 #[serde(default)]
1136 pub destructive: bool,
1137 #[serde(default, skip_serializing_if = "Option::is_none")]
1144 pub visible_if: Option<String>,
1145}
1146
1147#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1150pub struct DataTableProps {
1151 pub columns: Vec<Column>,
1152 pub data_path: String,
1153 #[serde(default, skip_serializing_if = "Option::is_none")]
1154 pub row_actions: Option<Vec<DropdownMenuAction>>,
1155 #[serde(default, skip_serializing_if = "Option::is_none")]
1156 pub empty_message: Option<String>,
1157 #[serde(default, skip_serializing_if = "Option::is_none")]
1158 pub row_key: Option<String>,
1159 #[serde(default, skip_serializing_if = "Option::is_none")]
1161 pub row_href: Option<String>,
1162}
1163
1164#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1168pub struct MediaCardGridProps {
1169 pub data_path: String,
1170 pub title_key: String,
1172 #[serde(default, skip_serializing_if = "Option::is_none")]
1174 pub description_key: Option<String>,
1175 #[serde(default, skip_serializing_if = "Option::is_none")]
1177 pub image_key: Option<String>,
1178 #[serde(default, skip_serializing_if = "Option::is_none")]
1180 pub image_href_key: Option<String>,
1181 #[serde(default, skip_serializing_if = "Option::is_none")]
1183 pub image_aspect_ratio: Option<String>,
1184 #[serde(default, skip_serializing_if = "Option::is_none")]
1187 pub image_position: Option<String>,
1188 #[serde(default, skip_serializing_if = "Option::is_none")]
1190 pub badge_key: Option<String>,
1191 #[serde(default, skip_serializing_if = "Option::is_none")]
1193 pub badge_variant_key: Option<String>,
1194 #[serde(default, skip_serializing_if = "Option::is_none")]
1196 pub row_key: Option<String>,
1197 #[serde(default, skip_serializing_if = "Option::is_none")]
1198 pub row_actions: Option<Vec<DropdownMenuAction>>,
1199 #[serde(default, skip_serializing_if = "Option::is_none")]
1200 pub empty_message: Option<String>,
1201 #[serde(default, skip_serializing_if = "Option::is_none")]
1203 pub columns: Option<u8>,
1204}
1205
1206#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1214pub struct KanbanColumnProps {
1215 pub id: String,
1216 pub title: String,
1217 #[serde(default)]
1218 pub count: u32,
1219 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1221 pub children: Vec<String>,
1222}
1223
1224#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1237pub struct KanbanBoardProps {
1238 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1240 pub columns: Vec<KanbanColumnProps>,
1241 #[serde(default, skip_serializing_if = "Option::is_none")]
1244 pub items_path: Option<String>,
1245 #[serde(default, skip_serializing_if = "Option::is_none")]
1247 pub group_by: Option<String>,
1248 #[serde(default, skip_serializing_if = "Option::is_none")]
1250 pub card_title_key: Option<String>,
1251 #[serde(default, skip_serializing_if = "Option::is_none")]
1253 pub card_description_key: Option<String>,
1254 #[serde(default, skip_serializing_if = "Option::is_none")]
1257 pub row_actions: Option<Vec<DropdownMenuAction>>,
1258 #[serde(default, skip_serializing_if = "Option::is_none")]
1261 pub row_key: Option<String>,
1262 #[serde(default, skip_serializing_if = "Option::is_none")]
1263 pub mobile_default_column: Option<String>,
1264 #[serde(default, skip_serializing_if = "Option::is_none")]
1268 pub empty_label: Option<String>,
1269}
1270
1271#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1276pub struct CalendarCellProps {
1277 pub day: u8,
1278 #[serde(default)]
1279 pub is_today: bool,
1280 #[serde(default)]
1281 pub is_current_month: bool,
1282 #[serde(default)]
1283 pub event_count: u32,
1284 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1287 pub dot_colors: Vec<String>,
1288}
1289
1290#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1292#[serde(rename_all = "snake_case")]
1293pub enum ActionCardVariant {
1294 #[default]
1295 Default,
1296 Setup,
1297 Danger,
1298}
1299
1300#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1305pub struct ActionCardProps {
1306 pub title: String,
1307 pub description: String,
1308 #[serde(default, skip_serializing_if = "Option::is_none")]
1309 pub icon: Option<String>,
1310 #[serde(default)]
1311 pub variant: ActionCardVariant,
1312 #[serde(default, skip_serializing_if = "Option::is_none")]
1314 pub href: Option<String>,
1315}
1316
1317#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1322pub struct ProductTileProps {
1323 pub product_id: String,
1324 pub name: String,
1325 pub price: String,
1326 pub field: String,
1327 #[serde(default, skip_serializing_if = "Option::is_none")]
1328 pub default_quantity: Option<u32>,
1329}
1330
1331fn deserialize_actions_lax<'de, D: serde::Deserializer<'de>>(
1337 d: D,
1338) -> Result<Vec<String>, D::Error> {
1339 use serde::de::Error;
1340 let v = serde_json::Value::deserialize(d)?;
1341 match v {
1342 serde_json::Value::Null => Ok(Vec::new()),
1343 serde_json::Value::String(s) if s.is_empty() => Ok(Vec::new()),
1344 serde_json::Value::Array(arr) => arr
1345 .into_iter()
1346 .map(|item| {
1347 item.as_str()
1348 .map(String::from)
1349 .ok_or_else(|| D::Error::custom("PageHeader.actions: expected string in array"))
1350 })
1351 .collect(),
1352 other => Err(D::Error::custom(format!(
1353 "PageHeader.actions: expected null, empty string, or array of strings; got {other:?}"
1354 ))),
1355 }
1356}
1357
1358#[cfg(test)]
1359mod schema_smoke_tests {
1360 use super::*;
1371
1372 fn assert_schema_nonempty_object<T: schemars::JsonSchema>(type_label: &str) {
1373 let schema = schemars::schema_for!(T);
1374 let value = serde_json::to_value(&schema).expect("schema serializes to JSON");
1375 assert!(
1376 value.is_object(),
1377 "{type_label}: schema must be a JSON object"
1378 );
1379 let props = value
1380 .get("properties")
1381 .and_then(|p| p.as_object())
1382 .map(|o| !o.is_empty())
1383 .unwrap_or(false);
1384 assert!(
1385 props,
1386 "{type_label}: schema must have a non-empty `properties` field"
1387 );
1388 }
1389
1390 #[test]
1391 fn schema_for_card_props_generates() {
1392 assert_schema_nonempty_object::<CardProps>("CardProps");
1393 }
1394
1395 #[test]
1396 fn schema_for_table_props_generates() {
1397 assert_schema_nonempty_object::<TableProps>("TableProps");
1398 }
1399
1400 #[test]
1401 fn schema_for_form_props_generates() {
1402 assert_schema_nonempty_object::<FormProps>("FormProps");
1403 }
1404
1405 #[test]
1406 fn schema_for_button_props_generates() {
1407 assert_schema_nonempty_object::<ButtonProps>("ButtonProps");
1408 }
1409
1410 #[test]
1411 fn schema_for_input_props_generates() {
1412 assert_schema_nonempty_object::<InputProps>("InputProps");
1413 }
1414
1415 #[test]
1416 fn schema_for_select_props_generates() {
1417 assert_schema_nonempty_object::<SelectProps>("SelectProps");
1418 }
1419
1420 #[test]
1421 fn schema_for_alert_props_generates() {
1422 assert_schema_nonempty_object::<AlertProps>("AlertProps");
1423 }
1424
1425 #[test]
1426 fn schema_for_badge_props_generates() {
1427 assert_schema_nonempty_object::<BadgeProps>("BadgeProps");
1428 }
1429
1430 #[test]
1431 fn schema_for_modal_props_generates() {
1432 assert_schema_nonempty_object::<ModalProps>("ModalProps");
1433 }
1434
1435 #[test]
1436 fn schema_for_text_props_generates() {
1437 assert_schema_nonempty_object::<TextProps>("TextProps");
1438 }
1439
1440 #[test]
1441 fn schema_for_checkbox_props_generates() {
1442 assert_schema_nonempty_object::<CheckboxProps>("CheckboxProps");
1443 }
1444
1445 #[test]
1446 fn schema_for_switch_props_generates() {
1447 assert_schema_nonempty_object::<SwitchProps>("SwitchProps");
1448 }
1449
1450 #[test]
1451 fn schema_for_separator_props_generates() {
1452 assert_schema_nonempty_object::<SeparatorProps>("SeparatorProps");
1453 }
1454
1455 #[test]
1456 fn schema_for_description_list_props_generates() {
1457 assert_schema_nonempty_object::<DescriptionListProps>("DescriptionListProps");
1458 }
1459
1460 #[test]
1461 fn schema_for_tab_generates() {
1462 assert_schema_nonempty_object::<Tab>("Tab");
1463 }
1464
1465 #[test]
1466 fn schema_for_tabs_props_generates() {
1467 assert_schema_nonempty_object::<TabsProps>("TabsProps");
1468 }
1469
1470 #[test]
1471 fn schema_for_breadcrumb_props_generates() {
1472 assert_schema_nonempty_object::<BreadcrumbProps>("BreadcrumbProps");
1473 }
1474
1475 #[test]
1476 fn schema_for_pagination_props_generates() {
1477 assert_schema_nonempty_object::<PaginationProps>("PaginationProps");
1478 }
1479
1480 #[test]
1481 fn schema_for_progress_props_generates() {
1482 assert_schema_nonempty_object::<ProgressProps>("ProgressProps");
1483 }
1484
1485 #[test]
1486 fn schema_for_image_props_generates() {
1487 assert_schema_nonempty_object::<ImageProps>("ImageProps");
1488 }
1489
1490 #[test]
1491 fn image_inline_svg_factory_roundtrips_via_serde() {
1492 let p = ImageProps::inline_svg("<svg/>", "alt");
1493 let json = serde_json::to_value(&p).expect("serialization must not fail");
1494 let parsed: ImageProps =
1495 serde_json::from_value(json).expect("deserialization must not fail");
1496 assert_eq!(parsed.inline_svg, Some("<svg/>".to_string()));
1497 assert_eq!(parsed.alt, "alt");
1498 assert_eq!(parsed.src, "");
1499 }
1500
1501 #[test]
1502 fn schema_for_avatar_props_generates() {
1503 assert_schema_nonempty_object::<AvatarProps>("AvatarProps");
1504 }
1505
1506 #[test]
1507 fn schema_for_skeleton_props_generates() {
1508 assert_schema_nonempty_object::<SkeletonProps>("SkeletonProps");
1509 }
1510
1511 #[test]
1512 fn schema_for_stat_card_props_generates() {
1513 assert_schema_nonempty_object::<StatCardProps>("StatCardProps");
1514 }
1515
1516 #[test]
1517 fn schema_for_checklist_props_generates() {
1518 assert_schema_nonempty_object::<ChecklistProps>("ChecklistProps");
1519 }
1520
1521 #[test]
1522 fn schema_for_toast_props_generates() {
1523 assert_schema_nonempty_object::<ToastProps>("ToastProps");
1524 }
1525
1526 #[test]
1527 fn schema_for_notification_dropdown_props_generates() {
1528 assert_schema_nonempty_object::<NotificationDropdownProps>("NotificationDropdownProps");
1529 }
1530
1531 #[test]
1532 fn schema_for_sidebar_props_generates() {
1533 assert_schema_nonempty_object::<SidebarProps>("SidebarProps");
1534 }
1535
1536 #[test]
1537 fn schema_for_header_props_generates() {
1538 assert_schema_nonempty_object::<HeaderProps>("HeaderProps");
1539 }
1540
1541 #[test]
1542 fn schema_for_grid_props_generates() {
1543 assert_schema_nonempty_object::<GridProps>("GridProps");
1544 }
1545
1546 #[test]
1547 fn schema_for_collapsible_props_generates() {
1548 assert_schema_nonempty_object::<CollapsibleProps>("CollapsibleProps");
1549 }
1550
1551 #[test]
1552 fn schema_for_empty_state_props_generates() {
1553 assert_schema_nonempty_object::<EmptyStateProps>("EmptyStateProps");
1554 }
1555
1556 #[test]
1557 fn schema_for_form_section_props_generates() {
1558 assert_schema_nonempty_object::<FormSectionProps>("FormSectionProps");
1559 }
1560
1561 #[test]
1562 fn schema_for_page_header_props_generates() {
1563 assert_schema_nonempty_object::<PageHeaderProps>("PageHeaderProps");
1564 }
1565
1566 #[test]
1567 fn schema_for_button_group_props_generates() {
1568 assert_schema_nonempty_object::<ButtonGroupProps>("ButtonGroupProps");
1569 }
1570
1571 #[test]
1572 fn schema_for_action_item_generates() {
1573 assert_schema_nonempty_object::<ActionItem>("ActionItem");
1574 }
1575
1576 #[test]
1577 fn schema_for_action_group_props_generates() {
1578 assert_schema_nonempty_object::<ActionGroupProps>("ActionGroupProps");
1579 }
1580
1581 #[test]
1582 fn schema_for_dropdown_menu_action_generates() {
1583 assert_schema_nonempty_object::<DropdownMenuAction>("DropdownMenuAction");
1584 }
1585
1586 #[test]
1587 fn schema_for_data_table_props_generates() {
1588 assert_schema_nonempty_object::<DataTableProps>("DataTableProps");
1589 }
1590
1591 #[test]
1592 fn schema_for_kanban_column_props_generates() {
1593 assert_schema_nonempty_object::<KanbanColumnProps>("KanbanColumnProps");
1594 }
1595
1596 #[test]
1597 fn schema_for_kanban_board_props_generates() {
1598 assert_schema_nonempty_object::<KanbanBoardProps>("KanbanBoardProps");
1599 }
1600
1601 #[test]
1602 fn schema_for_calendar_cell_props_generates() {
1603 assert_schema_nonempty_object::<CalendarCellProps>("CalendarCellProps");
1604 }
1605
1606 #[test]
1607 fn schema_for_action_card_props_generates() {
1608 assert_schema_nonempty_object::<ActionCardProps>("ActionCardProps");
1609 }
1610
1611 #[test]
1612 fn schema_for_product_tile_props_generates() {
1613 assert_schema_nonempty_object::<ProductTileProps>("ProductTileProps");
1614 }
1615
1616 #[test]
1617 fn card_props_round_trips_footer() {
1618 let original = CardProps {
1619 title: "Hero".to_string(),
1620 description: None,
1621 subtitle: None,
1622 badge: None,
1623 max_width: None,
1624 footer: vec!["btn1".to_string(), "btn2".to_string()],
1625 variant: CardVariant::Bordered,
1626 };
1627 let json = serde_json::to_string(&original).unwrap();
1628 let parsed: CardProps = serde_json::from_str(&json).unwrap();
1629 assert_eq!(original.footer, parsed.footer);
1630 }
1631
1632 #[test]
1633 fn tab_round_trips_children() {
1634 let original = Tab {
1635 value: "overview".to_string(),
1636 label: "Overview".to_string(),
1637 children: vec!["panel1".to_string()],
1638 };
1639 let json = serde_json::to_string(&original).unwrap();
1640 let parsed: Tab = serde_json::from_str(&json).unwrap();
1641 assert_eq!(original.children, parsed.children);
1642 }
1643
1644 #[test]
1645 fn card_props_omits_empty_footer_in_json() {
1646 let card = CardProps {
1647 title: "Card".to_string(),
1648 description: None,
1649 subtitle: None,
1650 badge: None,
1651 max_width: None,
1652 footer: Vec::new(),
1653 variant: CardVariant::Bordered,
1654 };
1655 let json = serde_json::to_string(&card).unwrap();
1656 assert!(
1657 !json.contains("\"footer\""),
1658 "empty footer must be skipped, got: {json}"
1659 );
1660 }
1661
1662 #[test]
1663 fn card_props_round_trips_badge() {
1664 let original = CardProps {
1665 title: "Hero".to_string(),
1666 description: None,
1667 subtitle: None,
1668 badge: Some("Scade tra 9m".to_string()),
1669 max_width: None,
1670 footer: Vec::new(),
1671 variant: CardVariant::Bordered,
1672 };
1673 let json = serde_json::to_string(&original).unwrap();
1674 let parsed: CardProps = serde_json::from_str(&json).unwrap();
1675 assert_eq!(original.badge, parsed.badge);
1676 }
1677
1678 #[test]
1679 fn card_props_omits_empty_badge_in_json() {
1680 let card = CardProps {
1681 title: "Card".to_string(),
1682 description: None,
1683 subtitle: None,
1684 badge: None,
1685 max_width: None,
1686 footer: Vec::new(),
1687 variant: CardVariant::Bordered,
1688 };
1689 let json = serde_json::to_string(&card).unwrap();
1690 assert!(
1691 !json.contains("\"badge\""),
1692 "empty badge must be skipped, got: {json}"
1693 );
1694 }
1695
1696 #[test]
1697 fn card_props_round_trips_subtitle() {
1698 let original = CardProps {
1699 title: "Hero".to_string(),
1700 description: None,
1701 subtitle: Some("Marco Rossi".to_string()),
1702 badge: None,
1703 max_width: None,
1704 footer: Vec::new(),
1705 variant: CardVariant::Bordered,
1706 };
1707 let json = serde_json::to_string(&original).unwrap();
1708 let parsed: CardProps = serde_json::from_str(&json).unwrap();
1709 assert_eq!(original.subtitle, parsed.subtitle);
1710 }
1711
1712 #[test]
1713 fn card_props_omits_empty_subtitle_in_json() {
1714 let card = CardProps {
1715 title: "Card".to_string(),
1716 description: None,
1717 subtitle: None,
1718 badge: None,
1719 max_width: None,
1720 footer: Vec::new(),
1721 variant: CardVariant::Bordered,
1722 };
1723 let json = serde_json::to_string(&card).unwrap();
1724 assert!(
1725 !json.contains("\"subtitle\""),
1726 "empty subtitle must be skipped, got: {json}"
1727 );
1728 }
1729
1730 #[test]
1731 fn card_props_schema_includes_badge() {
1732 let schema = schemars::schema_for!(CardProps);
1733 let value = serde_json::to_value(&schema).expect("schema serializes to JSON");
1734 let props = value
1735 .get("properties")
1736 .and_then(|p| p.as_object())
1737 .expect("schema has a properties object");
1738 assert!(
1739 props.contains_key("badge"),
1740 "CardProps schema must expose a `badge` property; got keys: {:?}",
1741 props.keys().collect::<Vec<_>>()
1742 );
1743 let badge_schema = props.get("badge").expect("badge entry");
1748 let badge_json = badge_schema.to_string();
1749 assert!(
1750 badge_json.contains("\"string\""),
1751 "badge schema entry must mention string type; got: {badge_json}"
1752 );
1753 }
1754
1755 #[test]
1756 fn card_props_schema_includes_subtitle() {
1757 let schema = schemars::schema_for!(CardProps);
1758 let value = serde_json::to_value(&schema).expect("schema serializes to JSON");
1759 let props = value
1760 .get("properties")
1761 .and_then(|p| p.as_object())
1762 .expect("schema has a properties object");
1763 assert!(
1764 props.contains_key("subtitle"),
1765 "CardProps schema must expose a `subtitle` property; got keys: {:?}",
1766 props.keys().collect::<Vec<_>>()
1767 );
1768 let subtitle_schema = props.get("subtitle").expect("subtitle entry");
1773 let subtitle_json = subtitle_schema.to_string();
1774 assert!(
1775 subtitle_json.contains("\"string\""),
1776 "subtitle schema entry must mention string type; got: {subtitle_json}"
1777 );
1778 }
1779
1780 #[test]
1781 fn schema_for_checkbox_list_props_generates() {
1782 assert_schema_nonempty_object::<CheckboxListProps>("CheckboxListProps");
1783 }
1784
1785 #[test]
1786 fn checkbox_list_props_serde_roundtrip() {
1787 let json = serde_json::json!({
1788 "field": "services",
1789 "options": [{"value": "a", "label": "Alpha"}, {"value": "b", "label": "Beta"}],
1790 "selected_path": "/preselected"
1791 });
1792 let parsed: CheckboxListProps = serde_json::from_value(json.clone()).expect("decode");
1793 assert_eq!(parsed.field, "services");
1794 assert_eq!(parsed.options.len(), 2);
1795 assert_eq!(parsed.selected_path.as_deref(), Some("/preselected"));
1796 let reserialized = serde_json::to_value(&parsed).expect("encode");
1797 assert!(reserialized.get("label").is_none());
1799 assert!(reserialized.get("disabled").is_none());
1800 }
1801
1802 #[test]
1803 fn schema_for_rich_text_editor_props_generates() {
1804 assert_schema_nonempty_object::<RichTextEditorProps>("RichTextEditorProps");
1805 }
1806
1807 #[test]
1808 fn rich_text_editor_props_serde_roundtrip() {
1809 let json = serde_json::json!({
1810 "field": "body",
1811 "label": "Body"
1812 });
1813 let parsed: RichTextEditorProps = serde_json::from_value(json).expect("decode");
1814 assert_eq!(parsed.field, "body");
1815 assert_eq!(parsed.label, "Body");
1816 assert!(parsed.placeholder.is_none());
1817 assert!(parsed.default_value.is_none());
1818 assert!(parsed.data_path.is_none());
1819 assert!(parsed.error.is_none());
1820 let reserialized = serde_json::to_value(&parsed).expect("encode");
1821 assert!(reserialized.get("placeholder").is_none());
1823 assert!(reserialized.get("error").is_none());
1824 }
1825}
1826
1827#[cfg(test)]
1828mod strum_tests {
1829 use super::*;
1830
1831 #[test]
1835 fn variant_enums_strum_matches_serde_wire_format() {
1836 fn check<T: AsRef<str> + serde::Serialize>(variants: &[T], label: &str) {
1837 for v in variants {
1838 let json = serde_json::to_string(v).expect("serialize");
1839 let json_stripped = json.trim_matches('"');
1840 assert_eq!(
1841 v.as_ref(),
1842 json_stripped,
1843 "strum AsRefStr drifted from serde for {label} variant"
1844 );
1845 }
1846 }
1847 check(
1848 &[
1849 AlertVariant::Info,
1850 AlertVariant::Success,
1851 AlertVariant::Warning,
1852 AlertVariant::Error,
1853 ],
1854 "AlertVariant",
1855 );
1856 check(
1857 &[
1858 BadgeVariant::Default,
1859 BadgeVariant::Secondary,
1860 BadgeVariant::Destructive,
1861 BadgeVariant::Outline,
1862 ],
1863 "BadgeVariant",
1864 );
1865 check(
1866 &[
1867 ButtonVariant::Default,
1868 ButtonVariant::Secondary,
1869 ButtonVariant::Destructive,
1870 ButtonVariant::Outline,
1871 ButtonVariant::Ghost,
1872 ButtonVariant::Link,
1873 ],
1874 "ButtonVariant",
1875 );
1876 check(
1877 &[
1878 ToastVariant::Info,
1879 ToastVariant::Success,
1880 ToastVariant::Warning,
1881 ToastVariant::Error,
1882 ],
1883 "ToastVariant",
1884 );
1885 }
1886
1887 #[test]
1888 fn alert_variant_as_ref_str_matches_wire_format() {
1889 assert_eq!(AlertVariant::Success.as_ref(), "success");
1890 assert_eq!(AlertVariant::Warning.as_ref(), "warning");
1891 assert_eq!(AlertVariant::Info.as_ref(), "info");
1892 assert_eq!(AlertVariant::Error.as_ref(), "error");
1893 }
1894}
1895
1896#[cfg(test)]
1897mod card_variant_tests {
1898 use super::*;
1899
1900 #[test]
1901 fn card_variant_default_is_bordered() {
1902 assert_eq!(CardVariant::default(), CardVariant::Bordered);
1903 }
1904
1905 #[test]
1906 fn card_variant_serializes_snake_case() {
1907 assert_eq!(
1908 serde_json::to_value(CardVariant::Bordered).unwrap(),
1909 serde_json::json!("bordered")
1910 );
1911 assert_eq!(
1912 serde_json::to_value(CardVariant::Elevated).unwrap(),
1913 serde_json::json!("elevated")
1914 );
1915 }
1916
1917 #[test]
1918 fn card_variant_deserializes_snake_case() {
1919 assert_eq!(
1920 serde_json::from_value::<CardVariant>(serde_json::json!("bordered")).unwrap(),
1921 CardVariant::Bordered
1922 );
1923 assert_eq!(
1924 serde_json::from_value::<CardVariant>(serde_json::json!("elevated")).unwrap(),
1925 CardVariant::Elevated
1926 );
1927 }
1928
1929 #[test]
1930 fn card_props_without_variant_defaults_to_bordered() {
1931 let v = serde_json::json!({"title": "x"});
1932 let p: CardProps = serde_json::from_value(v).unwrap();
1933 assert_eq!(p.variant, CardVariant::Bordered);
1934 }
1935
1936 #[test]
1937 fn card_props_with_elevated_variant() {
1938 let v = serde_json::json!({"title": "x", "variant": "elevated"});
1939 let p: CardProps = serde_json::from_value(v).unwrap();
1940 assert_eq!(p.variant, CardVariant::Elevated);
1941 }
1942
1943 #[test]
1944 fn card_props_roundtrip_preserves_variant() {
1945 let p = CardProps {
1946 title: "x".into(),
1947 description: None,
1948 subtitle: None,
1949 badge: None,
1950 max_width: None,
1951 footer: vec![],
1952 variant: CardVariant::Elevated,
1953 };
1954 let j = serde_json::to_value(&p).unwrap();
1955 let back: CardProps = serde_json::from_value(j).unwrap();
1956 assert_eq!(back.variant, CardVariant::Elevated);
1957 }
1958}
1959
1960#[cfg(test)]
1961mod kanban_board_props_tests {
1962 use super::*;
1963
1964 #[test]
1965 fn kanban_board_props_serde_static_columns() {
1966 let v = serde_json::json!({
1967 "columns": [{"title": "To Do", "id": "todo", "count": 0}]
1968 });
1969 let p: KanbanBoardProps = serde_json::from_value(v).unwrap();
1970 assert_eq!(p.columns.len(), 1);
1971 assert!(p.items_path.is_none());
1972 assert!(p.group_by.is_none());
1973 }
1974
1975 #[test]
1976 fn kanban_board_props_serde_data_bound() {
1977 let v = serde_json::json!({
1978 "columns": [{"title": "Open", "id": "open"}],
1979 "items_path": "/data/order",
1980 "group_by": "status",
1981 "card_title_key": "name"
1982 });
1983 let p: KanbanBoardProps = serde_json::from_value(v).unwrap();
1984 assert_eq!(p.columns.len(), 1);
1985 assert_eq!(p.items_path.as_deref(), Some("/data/order"));
1986 assert_eq!(p.group_by.as_deref(), Some("status"));
1987 assert_eq!(p.card_title_key.as_deref(), Some("name"));
1988 }
1989
1990 #[test]
1991 fn kanban_board_props_serde_neither() {
1992 let v = serde_json::json!({});
1993 let p: KanbanBoardProps = serde_json::from_value(v).unwrap();
1994 assert!(p.columns.is_empty());
1995 assert!(p.items_path.is_none());
1996 assert!(p.group_by.is_none());
1997 }
1998
1999 #[test]
2000 fn kanban_board_props_empty_columns_skipped_on_serialize() {
2001 let p = KanbanBoardProps {
2002 columns: vec![],
2003 items_path: Some("/data/order".into()),
2004 group_by: Some("status".into()),
2005 card_title_key: None,
2006 card_description_key: None,
2007 row_actions: None,
2008 row_key: None,
2009 mobile_default_column: None,
2010 empty_label: None,
2011 };
2012 let j = serde_json::to_value(&p).unwrap();
2013 assert!(
2014 j.get("columns").is_none(),
2015 "empty columns must be skipped, got: {j}"
2016 );
2017 assert_eq!(
2018 j.get("items_path").and_then(|v| v.as_str()),
2019 Some("/data/order")
2020 );
2021 }
2022}
2023
2024#[cfg(test)]
2025mod page_header_actions_tests {
2026 use super::*;
2027
2028 #[test]
2029 fn page_header_actions_missing_field() {
2030 let v = serde_json::json!({"title": "X"});
2031 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
2032 assert!(p.actions.is_empty());
2033 }
2034
2035 #[test]
2036 fn page_header_actions_null() {
2037 let v = serde_json::json!({"title": "X", "actions": null});
2038 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
2039 assert!(p.actions.is_empty());
2040 }
2041
2042 #[test]
2043 fn page_header_actions_empty_string() {
2044 let v = serde_json::json!({"title": "X", "actions": ""});
2045 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
2046 assert!(p.actions.is_empty());
2047 }
2048
2049 #[test]
2050 fn page_header_actions_empty_array() {
2051 let v = serde_json::json!({"title": "X", "actions": []});
2052 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
2053 assert!(p.actions.is_empty());
2054 }
2055
2056 #[test]
2057 fn page_header_actions_non_empty_array() {
2058 let v = serde_json::json!({"title": "X", "actions": ["a", "b"]});
2059 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
2060 assert_eq!(p.actions, vec!["a".to_string(), "b".to_string()]);
2061 }
2062
2063 #[test]
2064 fn page_header_actions_non_empty_string_rejected() {
2065 let v = serde_json::json!({"title": "X", "actions": "not-empty"});
2066 let result: Result<PageHeaderProps, _> = serde_json::from_value(v);
2067 assert!(result.is_err(), "non-empty string must be rejected");
2068 }
2069
2070 #[test]
2071 fn page_header_actions_non_string_array_rejected() {
2072 let v = serde_json::json!({"title": "X", "actions": [1, 2, 3]});
2073 let result: Result<PageHeaderProps, _> = serde_json::from_value(v);
2074 assert!(result.is_err(), "array of non-strings must be rejected");
2075 }
2076}