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)]
949pub struct ActionItem {
950 pub label: String,
951 pub action: Action,
952 #[serde(default)]
955 pub destructive: bool,
956 #[serde(default, skip_serializing_if = "Option::is_none")]
957 pub variant: Option<ButtonVariant>,
958 #[serde(default, skip_serializing_if = "Option::is_none")]
959 pub icon: Option<String>,
960 #[serde(default, skip_serializing_if = "Option::is_none")]
964 pub visible_if: Option<String>,
965}
966
967#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
978pub struct ActionGroupProps {
979 pub items: Vec<ActionItem>,
980 pub menu_id: String,
983 #[serde(default, skip_serializing_if = "Option::is_none")]
985 pub max_inline: Option<u8>,
986 #[serde(default, skip_serializing_if = "Option::is_none")]
988 pub overflow_label: Option<String>,
989 #[serde(default, skip_serializing_if = "Option::is_none")]
991 pub row_key: Option<String>,
992}
993
994#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Default)]
1010pub struct SegmentedControlProps {
1011 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1013 pub items: Vec<SegmentedItem>,
1014 #[serde(default, skip_serializing_if = "Option::is_none")]
1017 pub data_path: Option<String>,
1018 #[serde(default)]
1020 pub size: Size,
1021 #[serde(default, skip_serializing_if = "Option::is_none")]
1024 pub aria_label: Option<String>,
1025}
1026
1027#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Default)]
1029pub struct SegmentedItem {
1030 pub label: String,
1032 pub href: String,
1034 #[serde(default)]
1036 pub active: bool,
1037 #[serde(default, skip_serializing_if = "Option::is_none")]
1040 pub aria_label: Option<String>,
1041}
1042
1043#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Default)]
1059pub struct SidebarLayoutProps {
1060 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1062 pub items: Vec<SidebarLayoutItem>,
1063 #[serde(default, skip_serializing_if = "Option::is_none")]
1065 pub data_path: Option<String>,
1066 pub active: String,
1069 #[serde(default, skip_serializing_if = "Option::is_none")]
1071 pub aria_label: Option<String>,
1072}
1073
1074#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Default)]
1076pub struct SidebarLayoutItem {
1077 pub slug: String,
1080 pub label: String,
1082 pub url: String,
1085}
1086
1087#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1096pub struct DetailPageProps {
1097 pub title: String,
1098 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1099 pub breadcrumb: Vec<BreadcrumbItem>,
1100 #[serde(
1102 default,
1103 deserialize_with = "deserialize_actions_lax",
1104 skip_serializing_if = "Vec::is_empty"
1105 )]
1106 pub actions: Vec<String>,
1107 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1110 pub info: Vec<String>,
1111}
1112
1113#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1115pub struct DropdownMenuAction {
1116 pub label: String,
1117 pub action: Action,
1118 #[serde(default)]
1119 pub destructive: bool,
1120 #[serde(default, skip_serializing_if = "Option::is_none")]
1127 pub visible_if: Option<String>,
1128}
1129
1130#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1133pub struct DataTableProps {
1134 pub columns: Vec<Column>,
1135 pub data_path: String,
1136 #[serde(default, skip_serializing_if = "Option::is_none")]
1137 pub row_actions: Option<Vec<DropdownMenuAction>>,
1138 #[serde(default, skip_serializing_if = "Option::is_none")]
1139 pub empty_message: Option<String>,
1140 #[serde(default, skip_serializing_if = "Option::is_none")]
1141 pub row_key: Option<String>,
1142 #[serde(default, skip_serializing_if = "Option::is_none")]
1144 pub row_href: Option<String>,
1145}
1146
1147#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1151pub struct MediaCardGridProps {
1152 pub data_path: String,
1153 pub title_key: String,
1155 #[serde(default, skip_serializing_if = "Option::is_none")]
1157 pub description_key: Option<String>,
1158 #[serde(default, skip_serializing_if = "Option::is_none")]
1160 pub image_key: Option<String>,
1161 #[serde(default, skip_serializing_if = "Option::is_none")]
1163 pub image_href_key: Option<String>,
1164 #[serde(default, skip_serializing_if = "Option::is_none")]
1166 pub image_aspect_ratio: Option<String>,
1167 #[serde(default, skip_serializing_if = "Option::is_none")]
1170 pub image_position: Option<String>,
1171 #[serde(default, skip_serializing_if = "Option::is_none")]
1173 pub badge_key: Option<String>,
1174 #[serde(default, skip_serializing_if = "Option::is_none")]
1176 pub badge_variant_key: Option<String>,
1177 #[serde(default, skip_serializing_if = "Option::is_none")]
1179 pub row_key: Option<String>,
1180 #[serde(default, skip_serializing_if = "Option::is_none")]
1181 pub row_actions: Option<Vec<DropdownMenuAction>>,
1182 #[serde(default, skip_serializing_if = "Option::is_none")]
1183 pub empty_message: Option<String>,
1184 #[serde(default, skip_serializing_if = "Option::is_none")]
1186 pub columns: Option<u8>,
1187}
1188
1189#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1197pub struct KanbanColumnProps {
1198 pub id: String,
1199 pub title: String,
1200 #[serde(default)]
1201 pub count: u32,
1202 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1204 pub children: Vec<String>,
1205}
1206
1207#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1220pub struct KanbanBoardProps {
1221 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1223 pub columns: Vec<KanbanColumnProps>,
1224 #[serde(default, skip_serializing_if = "Option::is_none")]
1227 pub items_path: Option<String>,
1228 #[serde(default, skip_serializing_if = "Option::is_none")]
1230 pub group_by: Option<String>,
1231 #[serde(default, skip_serializing_if = "Option::is_none")]
1233 pub card_title_key: Option<String>,
1234 #[serde(default, skip_serializing_if = "Option::is_none")]
1236 pub card_description_key: Option<String>,
1237 #[serde(default, skip_serializing_if = "Option::is_none")]
1240 pub row_actions: Option<Vec<DropdownMenuAction>>,
1241 #[serde(default, skip_serializing_if = "Option::is_none")]
1244 pub row_key: Option<String>,
1245 #[serde(default, skip_serializing_if = "Option::is_none")]
1246 pub mobile_default_column: Option<String>,
1247 #[serde(default, skip_serializing_if = "Option::is_none")]
1251 pub empty_label: Option<String>,
1252}
1253
1254#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1259pub struct CalendarCellProps {
1260 pub day: u8,
1261 #[serde(default)]
1262 pub is_today: bool,
1263 #[serde(default)]
1264 pub is_current_month: bool,
1265 #[serde(default)]
1266 pub event_count: u32,
1267 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1270 pub dot_colors: Vec<String>,
1271}
1272
1273#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1275#[serde(rename_all = "snake_case")]
1276pub enum ActionCardVariant {
1277 #[default]
1278 Default,
1279 Setup,
1280 Danger,
1281}
1282
1283#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1288pub struct ActionCardProps {
1289 pub title: String,
1290 pub description: String,
1291 #[serde(default, skip_serializing_if = "Option::is_none")]
1292 pub icon: Option<String>,
1293 #[serde(default)]
1294 pub variant: ActionCardVariant,
1295 #[serde(default, skip_serializing_if = "Option::is_none")]
1297 pub href: Option<String>,
1298}
1299
1300#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1305pub struct ProductTileProps {
1306 pub product_id: String,
1307 pub name: String,
1308 pub price: String,
1309 pub field: String,
1310 #[serde(default, skip_serializing_if = "Option::is_none")]
1311 pub default_quantity: Option<u32>,
1312}
1313
1314fn deserialize_actions_lax<'de, D: serde::Deserializer<'de>>(
1320 d: D,
1321) -> Result<Vec<String>, D::Error> {
1322 use serde::de::Error;
1323 let v = serde_json::Value::deserialize(d)?;
1324 match v {
1325 serde_json::Value::Null => Ok(Vec::new()),
1326 serde_json::Value::String(s) if s.is_empty() => Ok(Vec::new()),
1327 serde_json::Value::Array(arr) => arr
1328 .into_iter()
1329 .map(|item| {
1330 item.as_str()
1331 .map(String::from)
1332 .ok_or_else(|| D::Error::custom("PageHeader.actions: expected string in array"))
1333 })
1334 .collect(),
1335 other => Err(D::Error::custom(format!(
1336 "PageHeader.actions: expected null, empty string, or array of strings; got {other:?}"
1337 ))),
1338 }
1339}
1340
1341#[cfg(test)]
1342mod schema_smoke_tests {
1343 use super::*;
1354
1355 fn assert_schema_nonempty_object<T: schemars::JsonSchema>(type_label: &str) {
1356 let schema = schemars::schema_for!(T);
1357 let value = serde_json::to_value(&schema).expect("schema serializes to JSON");
1358 assert!(
1359 value.is_object(),
1360 "{type_label}: schema must be a JSON object"
1361 );
1362 let props = value
1363 .get("properties")
1364 .and_then(|p| p.as_object())
1365 .map(|o| !o.is_empty())
1366 .unwrap_or(false);
1367 assert!(
1368 props,
1369 "{type_label}: schema must have a non-empty `properties` field"
1370 );
1371 }
1372
1373 #[test]
1374 fn schema_for_card_props_generates() {
1375 assert_schema_nonempty_object::<CardProps>("CardProps");
1376 }
1377
1378 #[test]
1379 fn schema_for_table_props_generates() {
1380 assert_schema_nonempty_object::<TableProps>("TableProps");
1381 }
1382
1383 #[test]
1384 fn schema_for_form_props_generates() {
1385 assert_schema_nonempty_object::<FormProps>("FormProps");
1386 }
1387
1388 #[test]
1389 fn schema_for_button_props_generates() {
1390 assert_schema_nonempty_object::<ButtonProps>("ButtonProps");
1391 }
1392
1393 #[test]
1394 fn schema_for_input_props_generates() {
1395 assert_schema_nonempty_object::<InputProps>("InputProps");
1396 }
1397
1398 #[test]
1399 fn schema_for_select_props_generates() {
1400 assert_schema_nonempty_object::<SelectProps>("SelectProps");
1401 }
1402
1403 #[test]
1404 fn schema_for_alert_props_generates() {
1405 assert_schema_nonempty_object::<AlertProps>("AlertProps");
1406 }
1407
1408 #[test]
1409 fn schema_for_badge_props_generates() {
1410 assert_schema_nonempty_object::<BadgeProps>("BadgeProps");
1411 }
1412
1413 #[test]
1414 fn schema_for_modal_props_generates() {
1415 assert_schema_nonempty_object::<ModalProps>("ModalProps");
1416 }
1417
1418 #[test]
1419 fn schema_for_text_props_generates() {
1420 assert_schema_nonempty_object::<TextProps>("TextProps");
1421 }
1422
1423 #[test]
1424 fn schema_for_checkbox_props_generates() {
1425 assert_schema_nonempty_object::<CheckboxProps>("CheckboxProps");
1426 }
1427
1428 #[test]
1429 fn schema_for_switch_props_generates() {
1430 assert_schema_nonempty_object::<SwitchProps>("SwitchProps");
1431 }
1432
1433 #[test]
1434 fn schema_for_separator_props_generates() {
1435 assert_schema_nonempty_object::<SeparatorProps>("SeparatorProps");
1436 }
1437
1438 #[test]
1439 fn schema_for_description_list_props_generates() {
1440 assert_schema_nonempty_object::<DescriptionListProps>("DescriptionListProps");
1441 }
1442
1443 #[test]
1444 fn schema_for_tab_generates() {
1445 assert_schema_nonempty_object::<Tab>("Tab");
1446 }
1447
1448 #[test]
1449 fn schema_for_tabs_props_generates() {
1450 assert_schema_nonempty_object::<TabsProps>("TabsProps");
1451 }
1452
1453 #[test]
1454 fn schema_for_breadcrumb_props_generates() {
1455 assert_schema_nonempty_object::<BreadcrumbProps>("BreadcrumbProps");
1456 }
1457
1458 #[test]
1459 fn schema_for_pagination_props_generates() {
1460 assert_schema_nonempty_object::<PaginationProps>("PaginationProps");
1461 }
1462
1463 #[test]
1464 fn schema_for_progress_props_generates() {
1465 assert_schema_nonempty_object::<ProgressProps>("ProgressProps");
1466 }
1467
1468 #[test]
1469 fn schema_for_image_props_generates() {
1470 assert_schema_nonempty_object::<ImageProps>("ImageProps");
1471 }
1472
1473 #[test]
1474 fn image_inline_svg_factory_roundtrips_via_serde() {
1475 let p = ImageProps::inline_svg("<svg/>", "alt");
1476 let json = serde_json::to_value(&p).expect("serialization must not fail");
1477 let parsed: ImageProps =
1478 serde_json::from_value(json).expect("deserialization must not fail");
1479 assert_eq!(parsed.inline_svg, Some("<svg/>".to_string()));
1480 assert_eq!(parsed.alt, "alt");
1481 assert_eq!(parsed.src, "");
1482 }
1483
1484 #[test]
1485 fn schema_for_avatar_props_generates() {
1486 assert_schema_nonempty_object::<AvatarProps>("AvatarProps");
1487 }
1488
1489 #[test]
1490 fn schema_for_skeleton_props_generates() {
1491 assert_schema_nonempty_object::<SkeletonProps>("SkeletonProps");
1492 }
1493
1494 #[test]
1495 fn schema_for_stat_card_props_generates() {
1496 assert_schema_nonempty_object::<StatCardProps>("StatCardProps");
1497 }
1498
1499 #[test]
1500 fn schema_for_checklist_props_generates() {
1501 assert_schema_nonempty_object::<ChecklistProps>("ChecklistProps");
1502 }
1503
1504 #[test]
1505 fn schema_for_toast_props_generates() {
1506 assert_schema_nonempty_object::<ToastProps>("ToastProps");
1507 }
1508
1509 #[test]
1510 fn schema_for_notification_dropdown_props_generates() {
1511 assert_schema_nonempty_object::<NotificationDropdownProps>("NotificationDropdownProps");
1512 }
1513
1514 #[test]
1515 fn schema_for_sidebar_props_generates() {
1516 assert_schema_nonempty_object::<SidebarProps>("SidebarProps");
1517 }
1518
1519 #[test]
1520 fn schema_for_header_props_generates() {
1521 assert_schema_nonempty_object::<HeaderProps>("HeaderProps");
1522 }
1523
1524 #[test]
1525 fn schema_for_grid_props_generates() {
1526 assert_schema_nonempty_object::<GridProps>("GridProps");
1527 }
1528
1529 #[test]
1530 fn schema_for_collapsible_props_generates() {
1531 assert_schema_nonempty_object::<CollapsibleProps>("CollapsibleProps");
1532 }
1533
1534 #[test]
1535 fn schema_for_empty_state_props_generates() {
1536 assert_schema_nonempty_object::<EmptyStateProps>("EmptyStateProps");
1537 }
1538
1539 #[test]
1540 fn schema_for_form_section_props_generates() {
1541 assert_schema_nonempty_object::<FormSectionProps>("FormSectionProps");
1542 }
1543
1544 #[test]
1545 fn schema_for_page_header_props_generates() {
1546 assert_schema_nonempty_object::<PageHeaderProps>("PageHeaderProps");
1547 }
1548
1549 #[test]
1550 fn schema_for_button_group_props_generates() {
1551 assert_schema_nonempty_object::<ButtonGroupProps>("ButtonGroupProps");
1552 }
1553
1554 #[test]
1555 fn schema_for_action_item_generates() {
1556 assert_schema_nonempty_object::<ActionItem>("ActionItem");
1557 }
1558
1559 #[test]
1560 fn schema_for_action_group_props_generates() {
1561 assert_schema_nonempty_object::<ActionGroupProps>("ActionGroupProps");
1562 }
1563
1564 #[test]
1565 fn schema_for_dropdown_menu_action_generates() {
1566 assert_schema_nonempty_object::<DropdownMenuAction>("DropdownMenuAction");
1567 }
1568
1569 #[test]
1570 fn schema_for_data_table_props_generates() {
1571 assert_schema_nonempty_object::<DataTableProps>("DataTableProps");
1572 }
1573
1574 #[test]
1575 fn schema_for_kanban_column_props_generates() {
1576 assert_schema_nonempty_object::<KanbanColumnProps>("KanbanColumnProps");
1577 }
1578
1579 #[test]
1580 fn schema_for_kanban_board_props_generates() {
1581 assert_schema_nonempty_object::<KanbanBoardProps>("KanbanBoardProps");
1582 }
1583
1584 #[test]
1585 fn schema_for_calendar_cell_props_generates() {
1586 assert_schema_nonempty_object::<CalendarCellProps>("CalendarCellProps");
1587 }
1588
1589 #[test]
1590 fn schema_for_action_card_props_generates() {
1591 assert_schema_nonempty_object::<ActionCardProps>("ActionCardProps");
1592 }
1593
1594 #[test]
1595 fn schema_for_product_tile_props_generates() {
1596 assert_schema_nonempty_object::<ProductTileProps>("ProductTileProps");
1597 }
1598
1599 #[test]
1600 fn card_props_round_trips_footer() {
1601 let original = CardProps {
1602 title: "Hero".to_string(),
1603 description: None,
1604 subtitle: None,
1605 badge: None,
1606 max_width: None,
1607 footer: vec!["btn1".to_string(), "btn2".to_string()],
1608 variant: CardVariant::Bordered,
1609 };
1610 let json = serde_json::to_string(&original).unwrap();
1611 let parsed: CardProps = serde_json::from_str(&json).unwrap();
1612 assert_eq!(original.footer, parsed.footer);
1613 }
1614
1615 #[test]
1616 fn tab_round_trips_children() {
1617 let original = Tab {
1618 value: "overview".to_string(),
1619 label: "Overview".to_string(),
1620 children: vec!["panel1".to_string()],
1621 };
1622 let json = serde_json::to_string(&original).unwrap();
1623 let parsed: Tab = serde_json::from_str(&json).unwrap();
1624 assert_eq!(original.children, parsed.children);
1625 }
1626
1627 #[test]
1628 fn card_props_omits_empty_footer_in_json() {
1629 let card = CardProps {
1630 title: "Card".to_string(),
1631 description: None,
1632 subtitle: None,
1633 badge: None,
1634 max_width: None,
1635 footer: Vec::new(),
1636 variant: CardVariant::Bordered,
1637 };
1638 let json = serde_json::to_string(&card).unwrap();
1639 assert!(
1640 !json.contains("\"footer\""),
1641 "empty footer must be skipped, got: {json}"
1642 );
1643 }
1644
1645 #[test]
1646 fn card_props_round_trips_badge() {
1647 let original = CardProps {
1648 title: "Hero".to_string(),
1649 description: None,
1650 subtitle: None,
1651 badge: Some("Scade tra 9m".to_string()),
1652 max_width: None,
1653 footer: Vec::new(),
1654 variant: CardVariant::Bordered,
1655 };
1656 let json = serde_json::to_string(&original).unwrap();
1657 let parsed: CardProps = serde_json::from_str(&json).unwrap();
1658 assert_eq!(original.badge, parsed.badge);
1659 }
1660
1661 #[test]
1662 fn card_props_omits_empty_badge_in_json() {
1663 let card = CardProps {
1664 title: "Card".to_string(),
1665 description: None,
1666 subtitle: None,
1667 badge: None,
1668 max_width: None,
1669 footer: Vec::new(),
1670 variant: CardVariant::Bordered,
1671 };
1672 let json = serde_json::to_string(&card).unwrap();
1673 assert!(
1674 !json.contains("\"badge\""),
1675 "empty badge must be skipped, got: {json}"
1676 );
1677 }
1678
1679 #[test]
1680 fn card_props_round_trips_subtitle() {
1681 let original = CardProps {
1682 title: "Hero".to_string(),
1683 description: None,
1684 subtitle: Some("Marco Rossi".to_string()),
1685 badge: None,
1686 max_width: None,
1687 footer: Vec::new(),
1688 variant: CardVariant::Bordered,
1689 };
1690 let json = serde_json::to_string(&original).unwrap();
1691 let parsed: CardProps = serde_json::from_str(&json).unwrap();
1692 assert_eq!(original.subtitle, parsed.subtitle);
1693 }
1694
1695 #[test]
1696 fn card_props_omits_empty_subtitle_in_json() {
1697 let card = CardProps {
1698 title: "Card".to_string(),
1699 description: None,
1700 subtitle: None,
1701 badge: None,
1702 max_width: None,
1703 footer: Vec::new(),
1704 variant: CardVariant::Bordered,
1705 };
1706 let json = serde_json::to_string(&card).unwrap();
1707 assert!(
1708 !json.contains("\"subtitle\""),
1709 "empty subtitle must be skipped, got: {json}"
1710 );
1711 }
1712
1713 #[test]
1714 fn card_props_schema_includes_badge() {
1715 let schema = schemars::schema_for!(CardProps);
1716 let value = serde_json::to_value(&schema).expect("schema serializes to JSON");
1717 let props = value
1718 .get("properties")
1719 .and_then(|p| p.as_object())
1720 .expect("schema has a properties object");
1721 assert!(
1722 props.contains_key("badge"),
1723 "CardProps schema must expose a `badge` property; got keys: {:?}",
1724 props.keys().collect::<Vec<_>>()
1725 );
1726 let badge_schema = props.get("badge").expect("badge entry");
1731 let badge_json = badge_schema.to_string();
1732 assert!(
1733 badge_json.contains("\"string\""),
1734 "badge schema entry must mention string type; got: {badge_json}"
1735 );
1736 }
1737
1738 #[test]
1739 fn card_props_schema_includes_subtitle() {
1740 let schema = schemars::schema_for!(CardProps);
1741 let value = serde_json::to_value(&schema).expect("schema serializes to JSON");
1742 let props = value
1743 .get("properties")
1744 .and_then(|p| p.as_object())
1745 .expect("schema has a properties object");
1746 assert!(
1747 props.contains_key("subtitle"),
1748 "CardProps schema must expose a `subtitle` property; got keys: {:?}",
1749 props.keys().collect::<Vec<_>>()
1750 );
1751 let subtitle_schema = props.get("subtitle").expect("subtitle entry");
1756 let subtitle_json = subtitle_schema.to_string();
1757 assert!(
1758 subtitle_json.contains("\"string\""),
1759 "subtitle schema entry must mention string type; got: {subtitle_json}"
1760 );
1761 }
1762
1763 #[test]
1764 fn schema_for_checkbox_list_props_generates() {
1765 assert_schema_nonempty_object::<CheckboxListProps>("CheckboxListProps");
1766 }
1767
1768 #[test]
1769 fn checkbox_list_props_serde_roundtrip() {
1770 let json = serde_json::json!({
1771 "field": "services",
1772 "options": [{"value": "a", "label": "Alpha"}, {"value": "b", "label": "Beta"}],
1773 "selected_path": "/preselected"
1774 });
1775 let parsed: CheckboxListProps = serde_json::from_value(json.clone()).expect("decode");
1776 assert_eq!(parsed.field, "services");
1777 assert_eq!(parsed.options.len(), 2);
1778 assert_eq!(parsed.selected_path.as_deref(), Some("/preselected"));
1779 let reserialized = serde_json::to_value(&parsed).expect("encode");
1780 assert!(reserialized.get("label").is_none());
1782 assert!(reserialized.get("disabled").is_none());
1783 }
1784
1785 #[test]
1786 fn schema_for_rich_text_editor_props_generates() {
1787 assert_schema_nonempty_object::<RichTextEditorProps>("RichTextEditorProps");
1788 }
1789
1790 #[test]
1791 fn rich_text_editor_props_serde_roundtrip() {
1792 let json = serde_json::json!({
1793 "field": "body",
1794 "label": "Body"
1795 });
1796 let parsed: RichTextEditorProps = serde_json::from_value(json).expect("decode");
1797 assert_eq!(parsed.field, "body");
1798 assert_eq!(parsed.label, "Body");
1799 assert!(parsed.placeholder.is_none());
1800 assert!(parsed.default_value.is_none());
1801 assert!(parsed.data_path.is_none());
1802 assert!(parsed.error.is_none());
1803 let reserialized = serde_json::to_value(&parsed).expect("encode");
1804 assert!(reserialized.get("placeholder").is_none());
1806 assert!(reserialized.get("error").is_none());
1807 }
1808}
1809
1810#[cfg(test)]
1811mod strum_tests {
1812 use super::*;
1813
1814 #[test]
1818 fn variant_enums_strum_matches_serde_wire_format() {
1819 fn check<T: AsRef<str> + serde::Serialize>(variants: &[T], label: &str) {
1820 for v in variants {
1821 let json = serde_json::to_string(v).expect("serialize");
1822 let json_stripped = json.trim_matches('"');
1823 assert_eq!(
1824 v.as_ref(),
1825 json_stripped,
1826 "strum AsRefStr drifted from serde for {label} variant"
1827 );
1828 }
1829 }
1830 check(
1831 &[
1832 AlertVariant::Info,
1833 AlertVariant::Success,
1834 AlertVariant::Warning,
1835 AlertVariant::Error,
1836 ],
1837 "AlertVariant",
1838 );
1839 check(
1840 &[
1841 BadgeVariant::Default,
1842 BadgeVariant::Secondary,
1843 BadgeVariant::Destructive,
1844 BadgeVariant::Outline,
1845 ],
1846 "BadgeVariant",
1847 );
1848 check(
1849 &[
1850 ButtonVariant::Default,
1851 ButtonVariant::Secondary,
1852 ButtonVariant::Destructive,
1853 ButtonVariant::Outline,
1854 ButtonVariant::Ghost,
1855 ButtonVariant::Link,
1856 ],
1857 "ButtonVariant",
1858 );
1859 check(
1860 &[
1861 ToastVariant::Info,
1862 ToastVariant::Success,
1863 ToastVariant::Warning,
1864 ToastVariant::Error,
1865 ],
1866 "ToastVariant",
1867 );
1868 }
1869
1870 #[test]
1871 fn alert_variant_as_ref_str_matches_wire_format() {
1872 assert_eq!(AlertVariant::Success.as_ref(), "success");
1873 assert_eq!(AlertVariant::Warning.as_ref(), "warning");
1874 assert_eq!(AlertVariant::Info.as_ref(), "info");
1875 assert_eq!(AlertVariant::Error.as_ref(), "error");
1876 }
1877}
1878
1879#[cfg(test)]
1880mod card_variant_tests {
1881 use super::*;
1882
1883 #[test]
1884 fn card_variant_default_is_bordered() {
1885 assert_eq!(CardVariant::default(), CardVariant::Bordered);
1886 }
1887
1888 #[test]
1889 fn card_variant_serializes_snake_case() {
1890 assert_eq!(
1891 serde_json::to_value(CardVariant::Bordered).unwrap(),
1892 serde_json::json!("bordered")
1893 );
1894 assert_eq!(
1895 serde_json::to_value(CardVariant::Elevated).unwrap(),
1896 serde_json::json!("elevated")
1897 );
1898 }
1899
1900 #[test]
1901 fn card_variant_deserializes_snake_case() {
1902 assert_eq!(
1903 serde_json::from_value::<CardVariant>(serde_json::json!("bordered")).unwrap(),
1904 CardVariant::Bordered
1905 );
1906 assert_eq!(
1907 serde_json::from_value::<CardVariant>(serde_json::json!("elevated")).unwrap(),
1908 CardVariant::Elevated
1909 );
1910 }
1911
1912 #[test]
1913 fn card_props_without_variant_defaults_to_bordered() {
1914 let v = serde_json::json!({"title": "x"});
1915 let p: CardProps = serde_json::from_value(v).unwrap();
1916 assert_eq!(p.variant, CardVariant::Bordered);
1917 }
1918
1919 #[test]
1920 fn card_props_with_elevated_variant() {
1921 let v = serde_json::json!({"title": "x", "variant": "elevated"});
1922 let p: CardProps = serde_json::from_value(v).unwrap();
1923 assert_eq!(p.variant, CardVariant::Elevated);
1924 }
1925
1926 #[test]
1927 fn card_props_roundtrip_preserves_variant() {
1928 let p = CardProps {
1929 title: "x".into(),
1930 description: None,
1931 subtitle: None,
1932 badge: None,
1933 max_width: None,
1934 footer: vec![],
1935 variant: CardVariant::Elevated,
1936 };
1937 let j = serde_json::to_value(&p).unwrap();
1938 let back: CardProps = serde_json::from_value(j).unwrap();
1939 assert_eq!(back.variant, CardVariant::Elevated);
1940 }
1941}
1942
1943#[cfg(test)]
1944mod kanban_board_props_tests {
1945 use super::*;
1946
1947 #[test]
1948 fn kanban_board_props_serde_static_columns() {
1949 let v = serde_json::json!({
1950 "columns": [{"title": "To Do", "id": "todo", "count": 0}]
1951 });
1952 let p: KanbanBoardProps = serde_json::from_value(v).unwrap();
1953 assert_eq!(p.columns.len(), 1);
1954 assert!(p.items_path.is_none());
1955 assert!(p.group_by.is_none());
1956 }
1957
1958 #[test]
1959 fn kanban_board_props_serde_data_bound() {
1960 let v = serde_json::json!({
1961 "columns": [{"title": "Open", "id": "open"}],
1962 "items_path": "/data/order",
1963 "group_by": "status",
1964 "card_title_key": "name"
1965 });
1966 let p: KanbanBoardProps = serde_json::from_value(v).unwrap();
1967 assert_eq!(p.columns.len(), 1);
1968 assert_eq!(p.items_path.as_deref(), Some("/data/order"));
1969 assert_eq!(p.group_by.as_deref(), Some("status"));
1970 assert_eq!(p.card_title_key.as_deref(), Some("name"));
1971 }
1972
1973 #[test]
1974 fn kanban_board_props_serde_neither() {
1975 let v = serde_json::json!({});
1976 let p: KanbanBoardProps = serde_json::from_value(v).unwrap();
1977 assert!(p.columns.is_empty());
1978 assert!(p.items_path.is_none());
1979 assert!(p.group_by.is_none());
1980 }
1981
1982 #[test]
1983 fn kanban_board_props_empty_columns_skipped_on_serialize() {
1984 let p = KanbanBoardProps {
1985 columns: vec![],
1986 items_path: Some("/data/order".into()),
1987 group_by: Some("status".into()),
1988 card_title_key: None,
1989 card_description_key: None,
1990 row_actions: None,
1991 row_key: None,
1992 mobile_default_column: None,
1993 empty_label: None,
1994 };
1995 let j = serde_json::to_value(&p).unwrap();
1996 assert!(
1997 j.get("columns").is_none(),
1998 "empty columns must be skipped, got: {j}"
1999 );
2000 assert_eq!(
2001 j.get("items_path").and_then(|v| v.as_str()),
2002 Some("/data/order")
2003 );
2004 }
2005}
2006
2007#[cfg(test)]
2008mod page_header_actions_tests {
2009 use super::*;
2010
2011 #[test]
2012 fn page_header_actions_missing_field() {
2013 let v = serde_json::json!({"title": "X"});
2014 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
2015 assert!(p.actions.is_empty());
2016 }
2017
2018 #[test]
2019 fn page_header_actions_null() {
2020 let v = serde_json::json!({"title": "X", "actions": null});
2021 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
2022 assert!(p.actions.is_empty());
2023 }
2024
2025 #[test]
2026 fn page_header_actions_empty_string() {
2027 let v = serde_json::json!({"title": "X", "actions": ""});
2028 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
2029 assert!(p.actions.is_empty());
2030 }
2031
2032 #[test]
2033 fn page_header_actions_empty_array() {
2034 let v = serde_json::json!({"title": "X", "actions": []});
2035 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
2036 assert!(p.actions.is_empty());
2037 }
2038
2039 #[test]
2040 fn page_header_actions_non_empty_array() {
2041 let v = serde_json::json!({"title": "X", "actions": ["a", "b"]});
2042 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
2043 assert_eq!(p.actions, vec!["a".to_string(), "b".to_string()]);
2044 }
2045
2046 #[test]
2047 fn page_header_actions_non_empty_string_rejected() {
2048 let v = serde_json::json!({"title": "X", "actions": "not-empty"});
2049 let result: Result<PageHeaderProps, _> = serde_json::from_value(v);
2050 assert!(result.is_err(), "non-empty string must be rejected");
2051 }
2052
2053 #[test]
2054 fn page_header_actions_non_string_array_rejected() {
2055 let v = serde_json::json!({"title": "X", "actions": [1, 2, 3]});
2056 let result: Result<PageHeaderProps, _> = serde_json::from_value(v);
2057 assert!(result.is_err(), "array of non-strings must be rejected");
2058 }
2059}