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