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}
139
140#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
142pub struct Column {
143 pub key: String,
144 pub label: String,
145 #[serde(default, skip_serializing_if = "Option::is_none")]
146 pub format: Option<ColumnFormat>,
147}
148
149#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
151pub struct SelectOption {
152 pub value: String,
153 pub label: String,
154}
155
156#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
163#[serde(rename_all = "snake_case")]
164pub enum CardVariant {
165 #[default]
166 Bordered,
167 Elevated,
168}
169
170#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
172pub struct CardProps {
173 pub title: String,
174 #[serde(default, skip_serializing_if = "Option::is_none")]
175 pub description: Option<String>,
176 #[serde(default, skip_serializing_if = "Option::is_none")]
180 pub subtitle: Option<String>,
181 #[serde(default, skip_serializing_if = "Option::is_none")]
185 pub badge: Option<String>,
186 #[serde(default, skip_serializing_if = "Option::is_none")]
187 pub max_width: Option<FormMaxWidth>,
188 #[serde(default, skip_serializing_if = "Vec::is_empty")]
190 pub footer: Vec<String>,
191 #[serde(default)]
192 pub variant: CardVariant,
193}
194
195#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
197pub struct TableProps {
198 pub columns: Vec<Column>,
199 pub data_path: String,
200 #[serde(default, skip_serializing_if = "Option::is_none")]
201 pub row_actions: Option<Vec<Action>>,
202 #[serde(default, skip_serializing_if = "Option::is_none")]
203 pub empty_message: Option<String>,
204 #[serde(default, skip_serializing_if = "Option::is_none")]
205 pub sortable: Option<bool>,
206 #[serde(default, skip_serializing_if = "Option::is_none")]
207 pub sort_column: Option<String>,
208 #[serde(default, skip_serializing_if = "Option::is_none")]
209 pub sort_direction: Option<SortDirection>,
210}
211
212#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
214#[serde(rename_all = "snake_case")]
215pub enum FormMaxWidth {
216 #[default]
217 Default,
218 Narrow,
219 Wide,
220}
221
222#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
224pub struct FormProps {
225 pub action: Action,
226 #[serde(default, skip_serializing_if = "Option::is_none")]
227 pub method: Option<crate::action::HttpMethod>,
228 #[serde(default, skip_serializing_if = "Option::is_none")]
232 pub guard: Option<String>,
233 #[serde(default, skip_serializing_if = "Option::is_none")]
235 pub max_width: Option<FormMaxWidth>,
236 #[serde(default, skip_serializing_if = "Option::is_none")]
240 pub id: Option<String>,
241 #[serde(default, skip_serializing_if = "Option::is_none")]
246 pub enctype: Option<String>,
247}
248
249#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
251#[serde(rename_all = "snake_case")]
252pub enum ButtonType {
253 #[default]
254 Button,
255 Submit,
256}
257
258#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
260pub struct ButtonProps {
261 pub label: String,
262 #[serde(default)]
263 pub variant: ButtonVariant,
264 #[serde(default)]
265 pub size: Size,
266 #[serde(default, skip_serializing_if = "Option::is_none")]
267 pub disabled: Option<bool>,
268 #[serde(default, skip_serializing_if = "Option::is_none")]
269 pub icon: Option<String>,
270 #[serde(default, skip_serializing_if = "Option::is_none")]
271 pub icon_position: Option<IconPosition>,
272 #[serde(default, skip_serializing_if = "Option::is_none")]
273 pub button_type: Option<ButtonType>,
274 #[serde(default, skip_serializing_if = "Option::is_none")]
278 pub form: Option<String>,
279}
280
281#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
283pub struct InputProps {
284 pub field: String,
286 pub label: String,
287 #[serde(default)]
288 pub input_type: InputType,
289 #[serde(default, skip_serializing_if = "Option::is_none")]
290 pub placeholder: Option<String>,
291 #[serde(default, skip_serializing_if = "Option::is_none")]
292 pub required: Option<bool>,
293 #[serde(default, skip_serializing_if = "Option::is_none")]
294 pub disabled: Option<bool>,
295 #[serde(default, skip_serializing_if = "Option::is_none")]
296 pub error: Option<String>,
297 #[serde(default, skip_serializing_if = "Option::is_none")]
298 pub description: Option<String>,
299 #[serde(default, skip_serializing_if = "Option::is_none")]
300 pub default_value: Option<String>,
301 #[serde(default, skip_serializing_if = "Option::is_none")]
303 pub data_path: Option<String>,
304 #[serde(default, skip_serializing_if = "Option::is_none")]
306 pub step: Option<String>,
307 #[serde(default, skip_serializing_if = "Option::is_none")]
311 pub list: Option<String>,
312 #[serde(default, skip_serializing_if = "Option::is_none")]
317 pub accept: Option<String>,
318}
319
320#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
331pub struct RichTextEditorProps {
332 pub field: String,
333 pub label: String,
334 #[serde(default, skip_serializing_if = "Option::is_none")]
335 pub placeholder: Option<String>,
336 #[serde(default, skip_serializing_if = "Option::is_none")]
337 pub default_value: Option<String>,
338 #[serde(default, skip_serializing_if = "Option::is_none")]
339 pub data_path: Option<String>,
340 #[serde(default, skip_serializing_if = "Option::is_none")]
341 pub error: Option<String>,
342}
343
344#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
346pub struct SelectProps {
347 pub field: String,
349 pub label: String,
350 pub options: Vec<SelectOption>,
351 #[serde(default, skip_serializing_if = "Option::is_none")]
352 pub placeholder: Option<String>,
353 #[serde(default, skip_serializing_if = "Option::is_none")]
354 pub required: Option<bool>,
355 #[serde(default, skip_serializing_if = "Option::is_none")]
356 pub disabled: Option<bool>,
357 #[serde(default, skip_serializing_if = "Option::is_none")]
358 pub error: Option<String>,
359 #[serde(default, skip_serializing_if = "Option::is_none")]
360 pub description: Option<String>,
361 #[serde(default, skip_serializing_if = "Option::is_none")]
362 pub default_value: Option<String>,
363 #[serde(default, skip_serializing_if = "Option::is_none")]
365 pub data_path: Option<String>,
366}
367
368#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
370pub struct AlertProps {
371 pub message: String,
372 #[serde(default)]
373 pub variant: AlertVariant,
374 #[serde(default, skip_serializing_if = "Option::is_none")]
375 pub title: Option<String>,
376}
377
378#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
380pub struct BadgeProps {
381 pub label: String,
382 #[serde(default)]
383 pub variant: BadgeVariant,
384}
385
386#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
388pub struct ModalProps {
389 pub id: String,
390 pub title: String,
391 #[serde(default, skip_serializing_if = "Option::is_none")]
392 pub description: Option<String>,
393 #[serde(default, skip_serializing_if = "Option::is_none")]
394 pub trigger_label: Option<String>,
395 #[serde(default, skip_serializing_if = "Vec::is_empty")]
397 pub footer: Vec<String>,
398}
399
400#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
402pub struct TextProps {
403 pub content: String,
404 #[serde(default)]
405 pub element: TextElement,
406}
407
408#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
410pub struct CheckboxProps {
411 pub field: String,
413 #[serde(default, skip_serializing_if = "Option::is_none")]
416 pub value: Option<String>,
417 pub label: String,
418 #[serde(default, skip_serializing_if = "Option::is_none")]
419 pub description: Option<String>,
420 #[serde(default, skip_serializing_if = "Option::is_none")]
421 pub checked: Option<bool>,
422 #[serde(default, skip_serializing_if = "Option::is_none")]
424 pub data_path: Option<String>,
425 #[serde(default, skip_serializing_if = "Option::is_none")]
426 pub required: Option<bool>,
427 #[serde(default, skip_serializing_if = "Option::is_none")]
428 pub disabled: Option<bool>,
429 #[serde(default, skip_serializing_if = "Option::is_none")]
430 pub error: Option<String>,
431}
432
433#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
439pub struct CheckboxListProps {
440 pub field: String,
442 #[serde(default, skip_serializing_if = "Vec::is_empty")]
445 pub options: Vec<SelectOption>,
446 #[serde(default, skip_serializing_if = "Option::is_none")]
448 pub options_path: Option<String>,
449 #[serde(default, skip_serializing_if = "Option::is_none")]
451 pub selected_path: Option<String>,
452 #[serde(default, skip_serializing_if = "Option::is_none")]
453 pub label: Option<String>,
454 #[serde(default, skip_serializing_if = "Option::is_none")]
455 pub description: Option<String>,
456 #[serde(default, skip_serializing_if = "Option::is_none")]
457 pub disabled: Option<bool>,
458 #[serde(default, skip_serializing_if = "Option::is_none")]
459 pub error: Option<String>,
460}
461
462#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
464pub struct SwitchProps {
465 pub field: String,
467 pub label: String,
468 #[serde(default, skip_serializing_if = "Option::is_none")]
469 pub description: Option<String>,
470 #[serde(default, skip_serializing_if = "Option::is_none")]
471 pub checked: Option<bool>,
472 #[serde(default, skip_serializing_if = "Option::is_none")]
474 pub data_path: Option<String>,
475 #[serde(default, skip_serializing_if = "Option::is_none")]
476 pub required: Option<bool>,
477 #[serde(default, skip_serializing_if = "Option::is_none")]
478 pub disabled: Option<bool>,
479 #[serde(default, skip_serializing_if = "Option::is_none")]
480 pub error: Option<String>,
481 #[serde(default, skip_serializing_if = "Option::is_none")]
484 pub action: Option<Action>,
485 #[serde(default, skip_serializing_if = "Option::is_none")]
488 pub compact: Option<bool>,
489}
490
491#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
493pub struct SeparatorProps {
494 #[serde(default, skip_serializing_if = "Option::is_none")]
495 pub orientation: Option<Orientation>,
496}
497
498#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
500pub struct DescriptionItem {
501 pub label: String,
502 pub value: String,
503 #[serde(default, skip_serializing_if = "Option::is_none")]
504 pub format: Option<ColumnFormat>,
505}
506
507#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
509pub struct DescriptionListProps {
510 #[serde(default, skip_serializing_if = "Vec::is_empty")]
511 pub items: Vec<DescriptionItem>,
512 #[serde(default, skip_serializing_if = "Option::is_none")]
513 pub columns: Option<u8>,
514 #[serde(default, skip_serializing_if = "Option::is_none")]
518 pub data_path: Option<String>,
519}
520
521#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
523pub struct Tab {
524 pub value: String,
525 pub label: String,
526 #[serde(default, skip_serializing_if = "Vec::is_empty")]
528 pub children: Vec<String>,
529}
530
531#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
533pub struct TabsProps {
534 pub default_tab: String,
535 pub tabs: Vec<Tab>,
536}
537
538#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
540pub struct BreadcrumbItem {
541 pub label: String,
542 #[serde(default, skip_serializing_if = "Option::is_none")]
543 pub url: Option<String>,
544}
545
546#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
548pub struct BreadcrumbProps {
549 pub items: Vec<BreadcrumbItem>,
550}
551
552#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
554pub struct PaginationProps {
555 pub current_page: u32,
556 pub per_page: u32,
557 pub total: u32,
558 #[serde(default, skip_serializing_if = "Option::is_none")]
559 pub base_url: Option<String>,
560}
561
562#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
564pub struct ProgressProps {
565 pub value: u8,
567 #[serde(default, skip_serializing_if = "Option::is_none")]
568 pub max: Option<u8>,
569 #[serde(default, skip_serializing_if = "Option::is_none")]
570 pub label: Option<String>,
571}
572
573#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
575pub struct ImageProps {
576 #[serde(default)]
577 pub src: String,
578 pub alt: String,
579 #[serde(default, skip_serializing_if = "Option::is_none")]
580 pub aspect_ratio: Option<String>,
581 #[serde(default, skip_serializing_if = "Option::is_none")]
586 pub placeholder_label: Option<String>,
587 #[serde(default, skip_serializing_if = "Option::is_none")]
595 pub inline_svg: Option<String>,
596 #[serde(default, skip_serializing_if = "Option::is_none")]
600 pub data_path: Option<String>,
601}
602
603impl ImageProps {
604 pub fn inline_svg(svg: impl Into<String>, alt: impl Into<String>) -> Self {
610 Self {
611 src: String::new(),
612 alt: alt.into(),
613 aspect_ratio: None,
614 placeholder_label: None,
615 inline_svg: Some(svg.into()),
616 data_path: None,
617 }
618 }
619}
620
621#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
623pub struct AvatarProps {
624 #[serde(default, skip_serializing_if = "Option::is_none")]
625 pub src: Option<String>,
626 pub alt: String,
627 #[serde(default, skip_serializing_if = "Option::is_none")]
628 pub fallback: Option<String>,
629 #[serde(default, skip_serializing_if = "Option::is_none")]
630 pub size: Option<Size>,
631}
632
633#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
635pub struct SkeletonProps {
636 #[serde(default, skip_serializing_if = "Option::is_none")]
637 pub width: Option<String>,
638 #[serde(default, skip_serializing_if = "Option::is_none")]
639 pub height: Option<String>,
640 #[serde(default, skip_serializing_if = "Option::is_none")]
641 pub rounded: Option<bool>,
642}
643
644#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
661pub struct RawHtmlProps {
662 #[serde(default)]
664 pub html: String,
665}
666
667#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
673pub struct StreamTextProps {
674 #[serde(default)]
677 pub sse_url: String,
678 #[serde(default, skip_serializing_if = "Option::is_none")]
680 pub placeholder: Option<String>,
681 #[serde(default, skip_serializing_if = "Option::is_none")]
683 pub loading_text: Option<String>,
684}
685
686#[derive(
688 Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, strum::AsRefStr,
689)]
690#[serde(rename_all = "snake_case")]
691#[strum(serialize_all = "snake_case")]
692pub enum ToastVariant {
693 #[default]
694 Info,
695 Success,
696 Warning,
697 Error,
698}
699
700#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
702pub struct ChecklistItem {
703 pub label: String,
704 #[serde(default)]
705 pub checked: bool,
706 #[serde(default, skip_serializing_if = "Option::is_none")]
707 pub href: Option<String>,
708}
709
710#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
712pub struct NotificationItem {
713 #[serde(default, skip_serializing_if = "Option::is_none")]
714 pub icon: Option<String>,
715 pub text: String,
716 #[serde(default, skip_serializing_if = "Option::is_none")]
717 pub timestamp: Option<String>,
718 #[serde(default)]
719 pub read: bool,
720 #[serde(default, skip_serializing_if = "Option::is_none")]
721 pub action_url: Option<String>,
722}
723
724#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
726pub struct SidebarNavItem {
727 pub label: String,
728 pub href: String,
729 #[serde(default, skip_serializing_if = "Option::is_none")]
730 pub icon: Option<String>,
731 #[serde(default)]
732 pub active: bool,
733 #[serde(default, skip_serializing_if = "Option::is_none")]
736 pub disabled: Option<bool>,
737}
738
739#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
741pub struct SidebarGroup {
742 pub label: String,
743 #[serde(default)]
744 pub collapsed: bool,
745 pub items: Vec<SidebarNavItem>,
746}
747
748#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
750pub struct StatCardProps {
751 pub label: String,
752 pub value: String,
753 #[serde(default, skip_serializing_if = "Option::is_none")]
754 pub icon: Option<String>,
755 #[serde(default, skip_serializing_if = "Option::is_none")]
756 pub subtitle: Option<String>,
757 #[serde(default, skip_serializing_if = "Option::is_none")]
759 pub sse_target: Option<String>,
760}
761
762#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
764pub struct ChecklistProps {
765 pub title: String,
766 pub items: Vec<ChecklistItem>,
767 #[serde(default = "default_true")]
768 pub dismissible: bool,
769 #[serde(default, skip_serializing_if = "Option::is_none")]
770 pub dismiss_label: Option<String>,
771 #[serde(default, skip_serializing_if = "Option::is_none")]
773 pub data_key: Option<String>,
774}
775
776fn default_true() -> bool {
777 true
778}
779
780#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
785pub struct ToastProps {
786 pub message: String,
787 #[serde(default)]
788 pub variant: ToastVariant,
789 #[serde(default, skip_serializing_if = "Option::is_none")]
791 pub timeout: Option<u32>,
792 #[serde(default = "default_true")]
793 pub dismissible: bool,
794}
795
796#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
798pub struct NotificationDropdownProps {
799 pub notifications: Vec<NotificationItem>,
800 #[serde(default, skip_serializing_if = "Option::is_none")]
801 pub empty_text: Option<String>,
802}
803
804#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
806pub struct SidebarProps {
807 #[serde(default, skip_serializing_if = "Vec::is_empty")]
808 pub fixed_top: Vec<SidebarNavItem>,
809 #[serde(default, skip_serializing_if = "Vec::is_empty")]
810 pub groups: Vec<SidebarGroup>,
811 #[serde(default, skip_serializing_if = "Vec::is_empty")]
812 pub fixed_bottom: Vec<SidebarNavItem>,
813}
814
815#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
817pub struct HeaderProps {
818 pub business_name: String,
819 #[serde(default, skip_serializing_if = "Option::is_none")]
821 pub notification_count: Option<u32>,
822 #[serde(default, skip_serializing_if = "Option::is_none")]
823 pub user_name: Option<String>,
824 #[serde(default, skip_serializing_if = "Option::is_none")]
825 pub user_avatar: Option<String>,
826 #[serde(default, skip_serializing_if = "Option::is_none")]
827 pub logout_url: Option<String>,
828}
829
830#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
832#[serde(rename_all = "snake_case")]
833pub enum GapSize {
834 None,
835 Sm,
836 #[default]
837 Md,
838 Lg,
839 Xl,
840}
841
842#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
844pub struct GridProps {
845 #[serde(default = "default_grid_columns")]
847 pub columns: u8,
848 #[serde(default, skip_serializing_if = "Option::is_none")]
850 pub md_columns: Option<u8>,
851 #[serde(default, skip_serializing_if = "Option::is_none")]
853 pub lg_columns: Option<u8>,
854 #[serde(default)]
856 pub gap: GapSize,
857 #[serde(default, skip_serializing_if = "Option::is_none")]
860 pub scrollable: Option<bool>,
861}
862
863fn default_grid_columns() -> u8 {
864 2
865}
866
867#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
869pub struct CollapsibleProps {
870 pub title: String,
871 #[serde(default)]
872 pub expanded: bool,
873}
874
875#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
877pub struct EmptyStateProps {
878 pub title: String,
879 #[serde(default, skip_serializing_if = "Option::is_none")]
880 pub description: Option<String>,
881 #[serde(default, skip_serializing_if = "Option::is_none")]
882 pub action: Option<Action>,
883 #[serde(default, skip_serializing_if = "Option::is_none")]
884 pub action_label: Option<String>,
885}
886
887#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
889#[serde(rename_all = "snake_case")]
890pub enum FormSectionLayout {
891 #[default]
892 Stacked,
893 TwoColumn,
894}
895
896#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
898pub struct FormSectionProps {
899 pub title: String,
900 #[serde(default, skip_serializing_if = "Option::is_none")]
901 pub description: Option<String>,
902 #[serde(default, skip_serializing_if = "Option::is_none")]
904 pub layout: Option<FormSectionLayout>,
905}
906
907#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
909pub struct PageHeaderProps {
910 pub title: String,
911 #[serde(default, skip_serializing_if = "Vec::is_empty")]
912 pub breadcrumb: Vec<BreadcrumbItem>,
913 #[serde(
915 default,
916 deserialize_with = "deserialize_actions_lax",
917 skip_serializing_if = "Vec::is_empty"
918 )]
919 pub actions: Vec<String>,
920}
921
922#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Default)]
924pub struct ButtonGroupProps {
925 #[serde(default)]
927 pub gap: GapSize,
928}
929
930#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
939pub struct DetailPageProps {
940 pub title: String,
941 #[serde(default, skip_serializing_if = "Vec::is_empty")]
942 pub breadcrumb: Vec<BreadcrumbItem>,
943 #[serde(
945 default,
946 deserialize_with = "deserialize_actions_lax",
947 skip_serializing_if = "Vec::is_empty"
948 )]
949 pub actions: Vec<String>,
950 #[serde(default, skip_serializing_if = "Vec::is_empty")]
953 pub info: Vec<String>,
954}
955
956#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
958pub struct DropdownMenuAction {
959 pub label: String,
960 pub action: Action,
961 #[serde(default)]
962 pub destructive: bool,
963 #[serde(default, skip_serializing_if = "Option::is_none")]
970 pub visible_if: Option<String>,
971}
972
973#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
975pub struct DropdownMenuProps {
976 pub menu_id: String,
977 pub trigger_label: String,
978 pub items: Vec<DropdownMenuAction>,
979 #[serde(default, skip_serializing_if = "Option::is_none")]
980 pub trigger_variant: Option<ButtonVariant>,
981}
982
983#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
986pub struct DataTableProps {
987 pub columns: Vec<Column>,
988 pub data_path: String,
989 #[serde(default, skip_serializing_if = "Option::is_none")]
990 pub row_actions: Option<Vec<DropdownMenuAction>>,
991 #[serde(default, skip_serializing_if = "Option::is_none")]
992 pub empty_message: Option<String>,
993 #[serde(default, skip_serializing_if = "Option::is_none")]
994 pub row_key: Option<String>,
995 #[serde(default, skip_serializing_if = "Option::is_none")]
997 pub row_href: Option<String>,
998}
999
1000#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1004pub struct MediaCardGridProps {
1005 pub data_path: String,
1006 pub title_key: String,
1008 #[serde(default, skip_serializing_if = "Option::is_none")]
1010 pub description_key: Option<String>,
1011 #[serde(default, skip_serializing_if = "Option::is_none")]
1013 pub image_key: Option<String>,
1014 #[serde(default, skip_serializing_if = "Option::is_none")]
1016 pub image_href_key: Option<String>,
1017 #[serde(default, skip_serializing_if = "Option::is_none")]
1019 pub image_aspect_ratio: Option<String>,
1020 #[serde(default, skip_serializing_if = "Option::is_none")]
1023 pub image_position: Option<String>,
1024 #[serde(default, skip_serializing_if = "Option::is_none")]
1026 pub badge_key: Option<String>,
1027 #[serde(default, skip_serializing_if = "Option::is_none")]
1029 pub badge_variant_key: Option<String>,
1030 #[serde(default, skip_serializing_if = "Option::is_none")]
1032 pub row_key: Option<String>,
1033 #[serde(default, skip_serializing_if = "Option::is_none")]
1034 pub row_actions: Option<Vec<DropdownMenuAction>>,
1035 #[serde(default, skip_serializing_if = "Option::is_none")]
1036 pub empty_message: Option<String>,
1037 #[serde(default, skip_serializing_if = "Option::is_none")]
1039 pub columns: Option<u8>,
1040}
1041
1042#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1044pub struct KanbanColumnProps {
1045 pub id: String,
1046 pub title: String,
1047 pub count: u32,
1048 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1050 pub children: Vec<String>,
1051}
1052
1053#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1055pub struct KanbanBoardProps {
1056 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1058 pub columns: Vec<KanbanColumnProps>,
1059 #[serde(default, skip_serializing_if = "Option::is_none")]
1064 pub data_path: Option<String>,
1065 #[serde(default, skip_serializing_if = "Option::is_none")]
1066 pub mobile_default_column: Option<String>,
1067 #[serde(default, skip_serializing_if = "Option::is_none")]
1071 pub empty_label: Option<String>,
1072}
1073
1074#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1079pub struct CalendarCellProps {
1080 pub day: u8,
1081 #[serde(default)]
1082 pub is_today: bool,
1083 #[serde(default)]
1084 pub is_current_month: bool,
1085 #[serde(default)]
1086 pub event_count: u32,
1087 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1090 pub dot_colors: Vec<String>,
1091}
1092
1093#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1095#[serde(rename_all = "snake_case")]
1096pub enum ActionCardVariant {
1097 #[default]
1098 Default,
1099 Setup,
1100 Danger,
1101}
1102
1103#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1108pub struct ActionCardProps {
1109 pub title: String,
1110 pub description: String,
1111 #[serde(default, skip_serializing_if = "Option::is_none")]
1112 pub icon: Option<String>,
1113 #[serde(default)]
1114 pub variant: ActionCardVariant,
1115 #[serde(default, skip_serializing_if = "Option::is_none")]
1117 pub href: Option<String>,
1118}
1119
1120#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1125pub struct ProductTileProps {
1126 pub product_id: String,
1127 pub name: String,
1128 pub price: String,
1129 pub field: String,
1130 #[serde(default, skip_serializing_if = "Option::is_none")]
1131 pub default_quantity: Option<u32>,
1132}
1133
1134fn deserialize_actions_lax<'de, D: serde::Deserializer<'de>>(
1140 d: D,
1141) -> Result<Vec<String>, D::Error> {
1142 use serde::de::Error;
1143 let v = serde_json::Value::deserialize(d)?;
1144 match v {
1145 serde_json::Value::Null => Ok(Vec::new()),
1146 serde_json::Value::String(s) if s.is_empty() => Ok(Vec::new()),
1147 serde_json::Value::Array(arr) => arr
1148 .into_iter()
1149 .map(|item| {
1150 item.as_str()
1151 .map(String::from)
1152 .ok_or_else(|| D::Error::custom("PageHeader.actions: expected string in array"))
1153 })
1154 .collect(),
1155 other => Err(D::Error::custom(format!(
1156 "PageHeader.actions: expected null, empty string, or array of strings; got {other:?}"
1157 ))),
1158 }
1159}
1160
1161#[cfg(test)]
1162mod schema_smoke_tests {
1163 use super::*;
1174
1175 fn assert_schema_nonempty_object<T: schemars::JsonSchema>(type_label: &str) {
1176 let schema = schemars::schema_for!(T);
1177 let value = serde_json::to_value(&schema).expect("schema serializes to JSON");
1178 assert!(
1179 value.is_object(),
1180 "{type_label}: schema must be a JSON object"
1181 );
1182 let props = value
1183 .get("properties")
1184 .and_then(|p| p.as_object())
1185 .map(|o| !o.is_empty())
1186 .unwrap_or(false);
1187 assert!(
1188 props,
1189 "{type_label}: schema must have a non-empty `properties` field"
1190 );
1191 }
1192
1193 #[test]
1194 fn schema_for_card_props_generates() {
1195 assert_schema_nonempty_object::<CardProps>("CardProps");
1196 }
1197
1198 #[test]
1199 fn schema_for_table_props_generates() {
1200 assert_schema_nonempty_object::<TableProps>("TableProps");
1201 }
1202
1203 #[test]
1204 fn schema_for_form_props_generates() {
1205 assert_schema_nonempty_object::<FormProps>("FormProps");
1206 }
1207
1208 #[test]
1209 fn schema_for_button_props_generates() {
1210 assert_schema_nonempty_object::<ButtonProps>("ButtonProps");
1211 }
1212
1213 #[test]
1214 fn schema_for_input_props_generates() {
1215 assert_schema_nonempty_object::<InputProps>("InputProps");
1216 }
1217
1218 #[test]
1219 fn schema_for_select_props_generates() {
1220 assert_schema_nonempty_object::<SelectProps>("SelectProps");
1221 }
1222
1223 #[test]
1224 fn schema_for_alert_props_generates() {
1225 assert_schema_nonempty_object::<AlertProps>("AlertProps");
1226 }
1227
1228 #[test]
1229 fn schema_for_badge_props_generates() {
1230 assert_schema_nonempty_object::<BadgeProps>("BadgeProps");
1231 }
1232
1233 #[test]
1234 fn schema_for_modal_props_generates() {
1235 assert_schema_nonempty_object::<ModalProps>("ModalProps");
1236 }
1237
1238 #[test]
1239 fn schema_for_text_props_generates() {
1240 assert_schema_nonempty_object::<TextProps>("TextProps");
1241 }
1242
1243 #[test]
1244 fn schema_for_checkbox_props_generates() {
1245 assert_schema_nonempty_object::<CheckboxProps>("CheckboxProps");
1246 }
1247
1248 #[test]
1249 fn schema_for_switch_props_generates() {
1250 assert_schema_nonempty_object::<SwitchProps>("SwitchProps");
1251 }
1252
1253 #[test]
1254 fn schema_for_separator_props_generates() {
1255 assert_schema_nonempty_object::<SeparatorProps>("SeparatorProps");
1256 }
1257
1258 #[test]
1259 fn schema_for_description_list_props_generates() {
1260 assert_schema_nonempty_object::<DescriptionListProps>("DescriptionListProps");
1261 }
1262
1263 #[test]
1264 fn schema_for_tab_generates() {
1265 assert_schema_nonempty_object::<Tab>("Tab");
1266 }
1267
1268 #[test]
1269 fn schema_for_tabs_props_generates() {
1270 assert_schema_nonempty_object::<TabsProps>("TabsProps");
1271 }
1272
1273 #[test]
1274 fn schema_for_breadcrumb_props_generates() {
1275 assert_schema_nonempty_object::<BreadcrumbProps>("BreadcrumbProps");
1276 }
1277
1278 #[test]
1279 fn schema_for_pagination_props_generates() {
1280 assert_schema_nonempty_object::<PaginationProps>("PaginationProps");
1281 }
1282
1283 #[test]
1284 fn schema_for_progress_props_generates() {
1285 assert_schema_nonempty_object::<ProgressProps>("ProgressProps");
1286 }
1287
1288 #[test]
1289 fn schema_for_image_props_generates() {
1290 assert_schema_nonempty_object::<ImageProps>("ImageProps");
1291 }
1292
1293 #[test]
1294 fn image_inline_svg_factory_roundtrips_via_serde() {
1295 let p = ImageProps::inline_svg("<svg/>", "alt");
1296 let json = serde_json::to_value(&p).expect("serialization must not fail");
1297 let parsed: ImageProps =
1298 serde_json::from_value(json).expect("deserialization must not fail");
1299 assert_eq!(parsed.inline_svg, Some("<svg/>".to_string()));
1300 assert_eq!(parsed.alt, "alt");
1301 assert_eq!(parsed.src, "");
1302 }
1303
1304 #[test]
1305 fn schema_for_avatar_props_generates() {
1306 assert_schema_nonempty_object::<AvatarProps>("AvatarProps");
1307 }
1308
1309 #[test]
1310 fn schema_for_skeleton_props_generates() {
1311 assert_schema_nonempty_object::<SkeletonProps>("SkeletonProps");
1312 }
1313
1314 #[test]
1315 fn schema_for_stat_card_props_generates() {
1316 assert_schema_nonempty_object::<StatCardProps>("StatCardProps");
1317 }
1318
1319 #[test]
1320 fn schema_for_checklist_props_generates() {
1321 assert_schema_nonempty_object::<ChecklistProps>("ChecklistProps");
1322 }
1323
1324 #[test]
1325 fn schema_for_toast_props_generates() {
1326 assert_schema_nonempty_object::<ToastProps>("ToastProps");
1327 }
1328
1329 #[test]
1330 fn schema_for_notification_dropdown_props_generates() {
1331 assert_schema_nonempty_object::<NotificationDropdownProps>("NotificationDropdownProps");
1332 }
1333
1334 #[test]
1335 fn schema_for_sidebar_props_generates() {
1336 assert_schema_nonempty_object::<SidebarProps>("SidebarProps");
1337 }
1338
1339 #[test]
1340 fn schema_for_header_props_generates() {
1341 assert_schema_nonempty_object::<HeaderProps>("HeaderProps");
1342 }
1343
1344 #[test]
1345 fn schema_for_grid_props_generates() {
1346 assert_schema_nonempty_object::<GridProps>("GridProps");
1347 }
1348
1349 #[test]
1350 fn schema_for_collapsible_props_generates() {
1351 assert_schema_nonempty_object::<CollapsibleProps>("CollapsibleProps");
1352 }
1353
1354 #[test]
1355 fn schema_for_empty_state_props_generates() {
1356 assert_schema_nonempty_object::<EmptyStateProps>("EmptyStateProps");
1357 }
1358
1359 #[test]
1360 fn schema_for_form_section_props_generates() {
1361 assert_schema_nonempty_object::<FormSectionProps>("FormSectionProps");
1362 }
1363
1364 #[test]
1365 fn schema_for_page_header_props_generates() {
1366 assert_schema_nonempty_object::<PageHeaderProps>("PageHeaderProps");
1367 }
1368
1369 #[test]
1370 fn schema_for_button_group_props_generates() {
1371 assert_schema_nonempty_object::<ButtonGroupProps>("ButtonGroupProps");
1372 }
1373
1374 #[test]
1375 fn schema_for_dropdown_menu_action_generates() {
1376 assert_schema_nonempty_object::<DropdownMenuAction>("DropdownMenuAction");
1377 }
1378
1379 #[test]
1380 fn schema_for_dropdown_menu_props_generates() {
1381 assert_schema_nonempty_object::<DropdownMenuProps>("DropdownMenuProps");
1382 }
1383
1384 #[test]
1385 fn schema_for_data_table_props_generates() {
1386 assert_schema_nonempty_object::<DataTableProps>("DataTableProps");
1387 }
1388
1389 #[test]
1390 fn schema_for_kanban_column_props_generates() {
1391 assert_schema_nonempty_object::<KanbanColumnProps>("KanbanColumnProps");
1392 }
1393
1394 #[test]
1395 fn schema_for_kanban_board_props_generates() {
1396 assert_schema_nonempty_object::<KanbanBoardProps>("KanbanBoardProps");
1397 }
1398
1399 #[test]
1400 fn schema_for_calendar_cell_props_generates() {
1401 assert_schema_nonempty_object::<CalendarCellProps>("CalendarCellProps");
1402 }
1403
1404 #[test]
1405 fn schema_for_action_card_props_generates() {
1406 assert_schema_nonempty_object::<ActionCardProps>("ActionCardProps");
1407 }
1408
1409 #[test]
1410 fn schema_for_product_tile_props_generates() {
1411 assert_schema_nonempty_object::<ProductTileProps>("ProductTileProps");
1412 }
1413
1414 #[test]
1415 fn card_props_round_trips_footer() {
1416 let original = CardProps {
1417 title: "Hero".to_string(),
1418 description: None,
1419 subtitle: None,
1420 badge: None,
1421 max_width: None,
1422 footer: vec!["btn1".to_string(), "btn2".to_string()],
1423 variant: CardVariant::Bordered,
1424 };
1425 let json = serde_json::to_string(&original).unwrap();
1426 let parsed: CardProps = serde_json::from_str(&json).unwrap();
1427 assert_eq!(original.footer, parsed.footer);
1428 }
1429
1430 #[test]
1431 fn tab_round_trips_children() {
1432 let original = Tab {
1433 value: "overview".to_string(),
1434 label: "Overview".to_string(),
1435 children: vec!["panel1".to_string()],
1436 };
1437 let json = serde_json::to_string(&original).unwrap();
1438 let parsed: Tab = serde_json::from_str(&json).unwrap();
1439 assert_eq!(original.children, parsed.children);
1440 }
1441
1442 #[test]
1443 fn card_props_omits_empty_footer_in_json() {
1444 let card = CardProps {
1445 title: "Card".to_string(),
1446 description: None,
1447 subtitle: None,
1448 badge: None,
1449 max_width: None,
1450 footer: Vec::new(),
1451 variant: CardVariant::Bordered,
1452 };
1453 let json = serde_json::to_string(&card).unwrap();
1454 assert!(
1455 !json.contains("\"footer\""),
1456 "empty footer must be skipped, got: {json}"
1457 );
1458 }
1459
1460 #[test]
1461 fn card_props_round_trips_badge() {
1462 let original = CardProps {
1463 title: "Hero".to_string(),
1464 description: None,
1465 subtitle: None,
1466 badge: Some("Scade tra 9m".to_string()),
1467 max_width: None,
1468 footer: Vec::new(),
1469 variant: CardVariant::Bordered,
1470 };
1471 let json = serde_json::to_string(&original).unwrap();
1472 let parsed: CardProps = serde_json::from_str(&json).unwrap();
1473 assert_eq!(original.badge, parsed.badge);
1474 }
1475
1476 #[test]
1477 fn card_props_omits_empty_badge_in_json() {
1478 let card = CardProps {
1479 title: "Card".to_string(),
1480 description: None,
1481 subtitle: None,
1482 badge: None,
1483 max_width: None,
1484 footer: Vec::new(),
1485 variant: CardVariant::Bordered,
1486 };
1487 let json = serde_json::to_string(&card).unwrap();
1488 assert!(
1489 !json.contains("\"badge\""),
1490 "empty badge must be skipped, got: {json}"
1491 );
1492 }
1493
1494 #[test]
1495 fn card_props_round_trips_subtitle() {
1496 let original = CardProps {
1497 title: "Hero".to_string(),
1498 description: None,
1499 subtitle: Some("Marco Rossi".to_string()),
1500 badge: None,
1501 max_width: None,
1502 footer: Vec::new(),
1503 variant: CardVariant::Bordered,
1504 };
1505 let json = serde_json::to_string(&original).unwrap();
1506 let parsed: CardProps = serde_json::from_str(&json).unwrap();
1507 assert_eq!(original.subtitle, parsed.subtitle);
1508 }
1509
1510 #[test]
1511 fn card_props_omits_empty_subtitle_in_json() {
1512 let card = CardProps {
1513 title: "Card".to_string(),
1514 description: None,
1515 subtitle: None,
1516 badge: None,
1517 max_width: None,
1518 footer: Vec::new(),
1519 variant: CardVariant::Bordered,
1520 };
1521 let json = serde_json::to_string(&card).unwrap();
1522 assert!(
1523 !json.contains("\"subtitle\""),
1524 "empty subtitle must be skipped, got: {json}"
1525 );
1526 }
1527
1528 #[test]
1529 fn card_props_schema_includes_badge() {
1530 let schema = schemars::schema_for!(CardProps);
1531 let value = serde_json::to_value(&schema).expect("schema serializes to JSON");
1532 let props = value
1533 .get("properties")
1534 .and_then(|p| p.as_object())
1535 .expect("schema has a properties object");
1536 assert!(
1537 props.contains_key("badge"),
1538 "CardProps schema must expose a `badge` property; got keys: {:?}",
1539 props.keys().collect::<Vec<_>>()
1540 );
1541 let badge_schema = props.get("badge").expect("badge entry");
1546 let badge_json = badge_schema.to_string();
1547 assert!(
1548 badge_json.contains("\"string\""),
1549 "badge schema entry must mention string type; got: {badge_json}"
1550 );
1551 }
1552
1553 #[test]
1554 fn card_props_schema_includes_subtitle() {
1555 let schema = schemars::schema_for!(CardProps);
1556 let value = serde_json::to_value(&schema).expect("schema serializes to JSON");
1557 let props = value
1558 .get("properties")
1559 .and_then(|p| p.as_object())
1560 .expect("schema has a properties object");
1561 assert!(
1562 props.contains_key("subtitle"),
1563 "CardProps schema must expose a `subtitle` property; got keys: {:?}",
1564 props.keys().collect::<Vec<_>>()
1565 );
1566 let subtitle_schema = props.get("subtitle").expect("subtitle entry");
1571 let subtitle_json = subtitle_schema.to_string();
1572 assert!(
1573 subtitle_json.contains("\"string\""),
1574 "subtitle schema entry must mention string type; got: {subtitle_json}"
1575 );
1576 }
1577
1578 #[test]
1579 fn schema_for_checkbox_list_props_generates() {
1580 assert_schema_nonempty_object::<CheckboxListProps>("CheckboxListProps");
1581 }
1582
1583 #[test]
1584 fn checkbox_list_props_serde_roundtrip() {
1585 let json = serde_json::json!({
1586 "field": "services",
1587 "options": [{"value": "a", "label": "Alpha"}, {"value": "b", "label": "Beta"}],
1588 "selected_path": "/preselected"
1589 });
1590 let parsed: CheckboxListProps = serde_json::from_value(json.clone()).expect("decode");
1591 assert_eq!(parsed.field, "services");
1592 assert_eq!(parsed.options.len(), 2);
1593 assert_eq!(parsed.selected_path.as_deref(), Some("/preselected"));
1594 let reserialized = serde_json::to_value(&parsed).expect("encode");
1595 assert!(reserialized.get("label").is_none());
1597 assert!(reserialized.get("disabled").is_none());
1598 }
1599
1600 #[test]
1601 fn schema_for_rich_text_editor_props_generates() {
1602 assert_schema_nonempty_object::<RichTextEditorProps>("RichTextEditorProps");
1603 }
1604
1605 #[test]
1606 fn rich_text_editor_props_serde_roundtrip() {
1607 let json = serde_json::json!({
1608 "field": "body",
1609 "label": "Body"
1610 });
1611 let parsed: RichTextEditorProps = serde_json::from_value(json).expect("decode");
1612 assert_eq!(parsed.field, "body");
1613 assert_eq!(parsed.label, "Body");
1614 assert!(parsed.placeholder.is_none());
1615 assert!(parsed.default_value.is_none());
1616 assert!(parsed.data_path.is_none());
1617 assert!(parsed.error.is_none());
1618 let reserialized = serde_json::to_value(&parsed).expect("encode");
1619 assert!(reserialized.get("placeholder").is_none());
1621 assert!(reserialized.get("error").is_none());
1622 }
1623}
1624
1625#[cfg(test)]
1626mod strum_tests {
1627 use super::*;
1628
1629 #[test]
1633 fn variant_enums_strum_matches_serde_wire_format() {
1634 fn check<T: AsRef<str> + serde::Serialize>(variants: &[T], label: &str) {
1635 for v in variants {
1636 let json = serde_json::to_string(v).expect("serialize");
1637 let json_stripped = json.trim_matches('"');
1638 assert_eq!(
1639 v.as_ref(),
1640 json_stripped,
1641 "strum AsRefStr drifted from serde for {label} variant"
1642 );
1643 }
1644 }
1645 check(
1646 &[
1647 AlertVariant::Info,
1648 AlertVariant::Success,
1649 AlertVariant::Warning,
1650 AlertVariant::Error,
1651 ],
1652 "AlertVariant",
1653 );
1654 check(
1655 &[
1656 BadgeVariant::Default,
1657 BadgeVariant::Secondary,
1658 BadgeVariant::Destructive,
1659 BadgeVariant::Outline,
1660 ],
1661 "BadgeVariant",
1662 );
1663 check(
1664 &[
1665 ButtonVariant::Default,
1666 ButtonVariant::Secondary,
1667 ButtonVariant::Destructive,
1668 ButtonVariant::Outline,
1669 ButtonVariant::Ghost,
1670 ButtonVariant::Link,
1671 ],
1672 "ButtonVariant",
1673 );
1674 check(
1675 &[
1676 ToastVariant::Info,
1677 ToastVariant::Success,
1678 ToastVariant::Warning,
1679 ToastVariant::Error,
1680 ],
1681 "ToastVariant",
1682 );
1683 }
1684
1685 #[test]
1686 fn alert_variant_as_ref_str_matches_wire_format() {
1687 assert_eq!(AlertVariant::Success.as_ref(), "success");
1688 assert_eq!(AlertVariant::Warning.as_ref(), "warning");
1689 assert_eq!(AlertVariant::Info.as_ref(), "info");
1690 assert_eq!(AlertVariant::Error.as_ref(), "error");
1691 }
1692}
1693
1694#[cfg(test)]
1695mod card_variant_tests {
1696 use super::*;
1697
1698 #[test]
1699 fn card_variant_default_is_bordered() {
1700 assert_eq!(CardVariant::default(), CardVariant::Bordered);
1701 }
1702
1703 #[test]
1704 fn card_variant_serializes_snake_case() {
1705 assert_eq!(
1706 serde_json::to_value(CardVariant::Bordered).unwrap(),
1707 serde_json::json!("bordered")
1708 );
1709 assert_eq!(
1710 serde_json::to_value(CardVariant::Elevated).unwrap(),
1711 serde_json::json!("elevated")
1712 );
1713 }
1714
1715 #[test]
1716 fn card_variant_deserializes_snake_case() {
1717 assert_eq!(
1718 serde_json::from_value::<CardVariant>(serde_json::json!("bordered")).unwrap(),
1719 CardVariant::Bordered
1720 );
1721 assert_eq!(
1722 serde_json::from_value::<CardVariant>(serde_json::json!("elevated")).unwrap(),
1723 CardVariant::Elevated
1724 );
1725 }
1726
1727 #[test]
1728 fn card_props_without_variant_defaults_to_bordered() {
1729 let v = serde_json::json!({"title": "x"});
1730 let p: CardProps = serde_json::from_value(v).unwrap();
1731 assert_eq!(p.variant, CardVariant::Bordered);
1732 }
1733
1734 #[test]
1735 fn card_props_with_elevated_variant() {
1736 let v = serde_json::json!({"title": "x", "variant": "elevated"});
1737 let p: CardProps = serde_json::from_value(v).unwrap();
1738 assert_eq!(p.variant, CardVariant::Elevated);
1739 }
1740
1741 #[test]
1742 fn card_props_roundtrip_preserves_variant() {
1743 let p = CardProps {
1744 title: "x".into(),
1745 description: None,
1746 subtitle: None,
1747 badge: None,
1748 max_width: None,
1749 footer: vec![],
1750 variant: CardVariant::Elevated,
1751 };
1752 let j = serde_json::to_value(&p).unwrap();
1753 let back: CardProps = serde_json::from_value(j).unwrap();
1754 assert_eq!(back.variant, CardVariant::Elevated);
1755 }
1756}
1757
1758#[cfg(test)]
1759mod kanban_board_props_tests {
1760 use super::*;
1761
1762 #[test]
1763 fn kanban_board_props_serde_static_columns() {
1764 let v = serde_json::json!({
1765 "columns": [{"title": "To Do", "items": [], "id": "todo", "count": 0}]
1766 });
1767 let p: KanbanBoardProps = serde_json::from_value(v).unwrap();
1768 assert_eq!(p.columns.len(), 1);
1769 assert!(p.data_path.is_none());
1770 }
1771
1772 #[test]
1773 fn kanban_board_props_serde_data_path() {
1774 let v = serde_json::json!({"data_path": "/columns"});
1775 let p: KanbanBoardProps = serde_json::from_value(v).unwrap();
1776 assert!(p.columns.is_empty());
1777 assert_eq!(p.data_path.as_deref(), Some("/columns"));
1778 }
1779
1780 #[test]
1781 fn kanban_board_props_serde_neither() {
1782 let v = serde_json::json!({});
1783 let p: KanbanBoardProps = serde_json::from_value(v).unwrap();
1784 assert!(p.columns.is_empty());
1785 assert!(p.data_path.is_none());
1786 }
1787
1788 #[test]
1789 fn kanban_board_props_empty_columns_skipped_on_serialize() {
1790 let p = KanbanBoardProps {
1791 columns: vec![],
1792 data_path: Some("/x".into()),
1793 mobile_default_column: None,
1794 empty_label: None,
1795 };
1796 let j = serde_json::to_value(&p).unwrap();
1797 assert!(
1798 j.get("columns").is_none(),
1799 "empty columns must be skipped, got: {j}"
1800 );
1801 assert_eq!(j.get("data_path").and_then(|v| v.as_str()), Some("/x"));
1802 }
1803}
1804
1805#[cfg(test)]
1806mod page_header_actions_tests {
1807 use super::*;
1808
1809 #[test]
1810 fn page_header_actions_missing_field() {
1811 let v = serde_json::json!({"title": "X"});
1812 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
1813 assert!(p.actions.is_empty());
1814 }
1815
1816 #[test]
1817 fn page_header_actions_null() {
1818 let v = serde_json::json!({"title": "X", "actions": null});
1819 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
1820 assert!(p.actions.is_empty());
1821 }
1822
1823 #[test]
1824 fn page_header_actions_empty_string() {
1825 let v = serde_json::json!({"title": "X", "actions": ""});
1826 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
1827 assert!(p.actions.is_empty());
1828 }
1829
1830 #[test]
1831 fn page_header_actions_empty_array() {
1832 let v = serde_json::json!({"title": "X", "actions": []});
1833 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
1834 assert!(p.actions.is_empty());
1835 }
1836
1837 #[test]
1838 fn page_header_actions_non_empty_array() {
1839 let v = serde_json::json!({"title": "X", "actions": ["a", "b"]});
1840 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
1841 assert_eq!(p.actions, vec!["a".to_string(), "b".to_string()]);
1842 }
1843
1844 #[test]
1845 fn page_header_actions_non_empty_string_rejected() {
1846 let v = serde_json::json!({"title": "X", "actions": "not-empty"});
1847 let result: Result<PageHeaderProps, _> = serde_json::from_value(v);
1848 assert!(result.is_err(), "non-empty string must be rejected");
1849 }
1850
1851 #[test]
1852 fn page_header_actions_non_string_array_rejected() {
1853 let v = serde_json::json!({"title": "X", "actions": [1, 2, 3]});
1854 let result: Result<PageHeaderProps, _> = serde_json::from_value(v);
1855 assert!(result.is_err(), "array of non-strings must be rejected");
1856 }
1857}