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)]
128#[serde(rename_all = "snake_case")]
129pub enum ColumnFormat {
130 Date,
131 DateTime,
132 Currency,
133 Boolean,
134}
135
136#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
138pub struct Column {
139 pub key: String,
140 pub label: String,
141 #[serde(default, skip_serializing_if = "Option::is_none")]
142 pub format: Option<ColumnFormat>,
143}
144
145#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
147pub struct SelectOption {
148 pub value: String,
149 pub label: String,
150}
151
152#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
159#[serde(rename_all = "snake_case")]
160pub enum CardVariant {
161 #[default]
162 Bordered,
163 Elevated,
164}
165
166#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
168pub struct CardProps {
169 pub title: String,
170 #[serde(default, skip_serializing_if = "Option::is_none")]
171 pub description: Option<String>,
172 #[serde(default, skip_serializing_if = "Option::is_none")]
176 pub subtitle: Option<String>,
177 #[serde(default, skip_serializing_if = "Option::is_none")]
181 pub badge: Option<String>,
182 #[serde(default, skip_serializing_if = "Option::is_none")]
183 pub max_width: Option<FormMaxWidth>,
184 #[serde(default, skip_serializing_if = "Vec::is_empty")]
186 pub footer: Vec<String>,
187 #[serde(default)]
188 pub variant: CardVariant,
189}
190
191#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
193pub struct TableProps {
194 pub columns: Vec<Column>,
195 pub data_path: String,
196 #[serde(default, skip_serializing_if = "Option::is_none")]
197 pub row_actions: Option<Vec<Action>>,
198 #[serde(default, skip_serializing_if = "Option::is_none")]
199 pub empty_message: Option<String>,
200 #[serde(default, skip_serializing_if = "Option::is_none")]
201 pub sortable: Option<bool>,
202 #[serde(default, skip_serializing_if = "Option::is_none")]
203 pub sort_column: Option<String>,
204 #[serde(default, skip_serializing_if = "Option::is_none")]
205 pub sort_direction: Option<SortDirection>,
206}
207
208#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
210#[serde(rename_all = "snake_case")]
211pub enum FormMaxWidth {
212 #[default]
213 Default,
214 Narrow,
215 Wide,
216}
217
218#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
220pub struct FormProps {
221 pub action: Action,
222 #[serde(default, skip_serializing_if = "Option::is_none")]
223 pub method: Option<crate::action::HttpMethod>,
224 #[serde(default, skip_serializing_if = "Option::is_none")]
228 pub guard: Option<String>,
229 #[serde(default, skip_serializing_if = "Option::is_none")]
231 pub max_width: Option<FormMaxWidth>,
232 #[serde(default, skip_serializing_if = "Option::is_none")]
236 pub id: Option<String>,
237 #[serde(default, skip_serializing_if = "Option::is_none")]
242 pub enctype: Option<String>,
243}
244
245#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
247#[serde(rename_all = "snake_case")]
248pub enum ButtonType {
249 #[default]
250 Button,
251 Submit,
252}
253
254#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
256pub struct ButtonProps {
257 pub label: String,
258 #[serde(default)]
259 pub variant: ButtonVariant,
260 #[serde(default)]
261 pub size: Size,
262 #[serde(default, skip_serializing_if = "Option::is_none")]
263 pub disabled: Option<bool>,
264 #[serde(default, skip_serializing_if = "Option::is_none")]
265 pub icon: Option<String>,
266 #[serde(default, skip_serializing_if = "Option::is_none")]
267 pub icon_position: Option<IconPosition>,
268 #[serde(default, skip_serializing_if = "Option::is_none")]
269 pub button_type: Option<ButtonType>,
270 #[serde(default, skip_serializing_if = "Option::is_none")]
274 pub form: Option<String>,
275}
276
277#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
279pub struct InputProps {
280 pub field: String,
282 pub label: String,
283 #[serde(default)]
284 pub input_type: InputType,
285 #[serde(default, skip_serializing_if = "Option::is_none")]
286 pub placeholder: Option<String>,
287 #[serde(default, skip_serializing_if = "Option::is_none")]
288 pub required: Option<bool>,
289 #[serde(default, skip_serializing_if = "Option::is_none")]
290 pub disabled: Option<bool>,
291 #[serde(default, skip_serializing_if = "Option::is_none")]
292 pub error: Option<String>,
293 #[serde(default, skip_serializing_if = "Option::is_none")]
294 pub description: Option<String>,
295 #[serde(default, skip_serializing_if = "Option::is_none")]
296 pub default_value: Option<String>,
297 #[serde(default, skip_serializing_if = "Option::is_none")]
299 pub data_path: Option<String>,
300 #[serde(default, skip_serializing_if = "Option::is_none")]
302 pub step: Option<String>,
303 #[serde(default, skip_serializing_if = "Option::is_none")]
307 pub list: Option<String>,
308 #[serde(default, skip_serializing_if = "Option::is_none")]
313 pub accept: Option<String>,
314}
315
316#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
327pub struct RichTextEditorProps {
328 pub field: String,
329 pub label: String,
330 #[serde(default, skip_serializing_if = "Option::is_none")]
331 pub placeholder: Option<String>,
332 #[serde(default, skip_serializing_if = "Option::is_none")]
333 pub default_value: Option<String>,
334 #[serde(default, skip_serializing_if = "Option::is_none")]
335 pub data_path: Option<String>,
336 #[serde(default, skip_serializing_if = "Option::is_none")]
337 pub error: Option<String>,
338}
339
340#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
342pub struct SelectProps {
343 pub field: String,
345 pub label: String,
346 pub options: Vec<SelectOption>,
347 #[serde(default, skip_serializing_if = "Option::is_none")]
348 pub placeholder: Option<String>,
349 #[serde(default, skip_serializing_if = "Option::is_none")]
350 pub required: Option<bool>,
351 #[serde(default, skip_serializing_if = "Option::is_none")]
352 pub disabled: Option<bool>,
353 #[serde(default, skip_serializing_if = "Option::is_none")]
354 pub error: Option<String>,
355 #[serde(default, skip_serializing_if = "Option::is_none")]
356 pub description: Option<String>,
357 #[serde(default, skip_serializing_if = "Option::is_none")]
358 pub default_value: Option<String>,
359 #[serde(default, skip_serializing_if = "Option::is_none")]
361 pub data_path: Option<String>,
362}
363
364#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
366pub struct AlertProps {
367 pub message: String,
368 #[serde(default)]
369 pub variant: AlertVariant,
370 #[serde(default, skip_serializing_if = "Option::is_none")]
371 pub title: Option<String>,
372}
373
374#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
376pub struct BadgeProps {
377 pub label: String,
378 #[serde(default)]
379 pub variant: BadgeVariant,
380}
381
382#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
384pub struct ModalProps {
385 pub id: String,
386 pub title: String,
387 #[serde(default, skip_serializing_if = "Option::is_none")]
388 pub description: Option<String>,
389 #[serde(default, skip_serializing_if = "Option::is_none")]
390 pub trigger_label: Option<String>,
391 #[serde(default, skip_serializing_if = "Vec::is_empty")]
393 pub footer: Vec<String>,
394}
395
396#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
398pub struct TextProps {
399 pub content: String,
400 #[serde(default)]
401 pub element: TextElement,
402}
403
404#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
406pub struct CheckboxProps {
407 pub field: String,
409 #[serde(default, skip_serializing_if = "Option::is_none")]
412 pub value: Option<String>,
413 pub label: String,
414 #[serde(default, skip_serializing_if = "Option::is_none")]
415 pub description: Option<String>,
416 #[serde(default, skip_serializing_if = "Option::is_none")]
417 pub checked: Option<bool>,
418 #[serde(default, skip_serializing_if = "Option::is_none")]
420 pub data_path: Option<String>,
421 #[serde(default, skip_serializing_if = "Option::is_none")]
422 pub required: Option<bool>,
423 #[serde(default, skip_serializing_if = "Option::is_none")]
424 pub disabled: Option<bool>,
425 #[serde(default, skip_serializing_if = "Option::is_none")]
426 pub error: Option<String>,
427}
428
429#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
435pub struct CheckboxListProps {
436 pub field: String,
438 #[serde(default, skip_serializing_if = "Vec::is_empty")]
441 pub options: Vec<SelectOption>,
442 #[serde(default, skip_serializing_if = "Option::is_none")]
444 pub options_path: Option<String>,
445 #[serde(default, skip_serializing_if = "Option::is_none")]
447 pub selected_path: Option<String>,
448 #[serde(default, skip_serializing_if = "Option::is_none")]
449 pub label: Option<String>,
450 #[serde(default, skip_serializing_if = "Option::is_none")]
451 pub description: Option<String>,
452 #[serde(default, skip_serializing_if = "Option::is_none")]
453 pub disabled: Option<bool>,
454 #[serde(default, skip_serializing_if = "Option::is_none")]
455 pub error: Option<String>,
456}
457
458#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
460pub struct SwitchProps {
461 pub field: String,
463 pub label: String,
464 #[serde(default, skip_serializing_if = "Option::is_none")]
465 pub description: Option<String>,
466 #[serde(default, skip_serializing_if = "Option::is_none")]
467 pub checked: Option<bool>,
468 #[serde(default, skip_serializing_if = "Option::is_none")]
470 pub data_path: Option<String>,
471 #[serde(default, skip_serializing_if = "Option::is_none")]
472 pub required: Option<bool>,
473 #[serde(default, skip_serializing_if = "Option::is_none")]
474 pub disabled: Option<bool>,
475 #[serde(default, skip_serializing_if = "Option::is_none")]
476 pub error: Option<String>,
477 #[serde(default, skip_serializing_if = "Option::is_none")]
480 pub action: Option<Action>,
481 #[serde(default, skip_serializing_if = "Option::is_none")]
484 pub compact: Option<bool>,
485}
486
487#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
489pub struct SeparatorProps {
490 #[serde(default, skip_serializing_if = "Option::is_none")]
491 pub orientation: Option<Orientation>,
492}
493
494#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
496pub struct DescriptionItem {
497 pub label: String,
498 pub value: String,
499 #[serde(default, skip_serializing_if = "Option::is_none")]
500 pub format: Option<ColumnFormat>,
501}
502
503#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
505pub struct DescriptionListProps {
506 #[serde(default, skip_serializing_if = "Vec::is_empty")]
507 pub items: Vec<DescriptionItem>,
508 #[serde(default, skip_serializing_if = "Option::is_none")]
509 pub columns: Option<u8>,
510 #[serde(default, skip_serializing_if = "Option::is_none")]
514 pub data_path: Option<String>,
515}
516
517#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
519pub struct Tab {
520 pub value: String,
521 pub label: String,
522 #[serde(default, skip_serializing_if = "Vec::is_empty")]
524 pub children: Vec<String>,
525}
526
527#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
529pub struct TabsProps {
530 pub default_tab: String,
531 pub tabs: Vec<Tab>,
532}
533
534#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
536pub struct BreadcrumbItem {
537 pub label: String,
538 #[serde(default, skip_serializing_if = "Option::is_none")]
539 pub url: Option<String>,
540}
541
542#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
544pub struct BreadcrumbProps {
545 pub items: Vec<BreadcrumbItem>,
546}
547
548#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
550pub struct PaginationProps {
551 pub current_page: u32,
552 pub per_page: u32,
553 pub total: u32,
554 #[serde(default, skip_serializing_if = "Option::is_none")]
555 pub base_url: Option<String>,
556}
557
558#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
560pub struct ProgressProps {
561 pub value: u8,
563 #[serde(default, skip_serializing_if = "Option::is_none")]
564 pub max: Option<u8>,
565 #[serde(default, skip_serializing_if = "Option::is_none")]
566 pub label: Option<String>,
567}
568
569#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
571pub struct ImageProps {
572 #[serde(default)]
573 pub src: String,
574 pub alt: String,
575 #[serde(default, skip_serializing_if = "Option::is_none")]
576 pub aspect_ratio: Option<String>,
577 #[serde(default, skip_serializing_if = "Option::is_none")]
582 pub placeholder_label: Option<String>,
583 #[serde(default, skip_serializing_if = "Option::is_none")]
591 pub inline_svg: Option<String>,
592 #[serde(default, skip_serializing_if = "Option::is_none")]
596 pub data_path: Option<String>,
597}
598
599impl ImageProps {
600 pub fn inline_svg(svg: impl Into<String>, alt: impl Into<String>) -> Self {
606 Self {
607 src: String::new(),
608 alt: alt.into(),
609 aspect_ratio: None,
610 placeholder_label: None,
611 inline_svg: Some(svg.into()),
612 data_path: None,
613 }
614 }
615}
616
617#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
619pub struct AvatarProps {
620 #[serde(default, skip_serializing_if = "Option::is_none")]
621 pub src: Option<String>,
622 pub alt: String,
623 #[serde(default, skip_serializing_if = "Option::is_none")]
624 pub fallback: Option<String>,
625 #[serde(default, skip_serializing_if = "Option::is_none")]
626 pub size: Option<Size>,
627}
628
629#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
631pub struct SkeletonProps {
632 #[serde(default, skip_serializing_if = "Option::is_none")]
633 pub width: Option<String>,
634 #[serde(default, skip_serializing_if = "Option::is_none")]
635 pub height: Option<String>,
636 #[serde(default, skip_serializing_if = "Option::is_none")]
637 pub rounded: Option<bool>,
638}
639
640#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
657pub struct RawHtmlProps {
658 #[serde(default)]
660 pub html: String,
661}
662
663#[derive(
665 Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, strum::AsRefStr,
666)]
667#[serde(rename_all = "snake_case")]
668#[strum(serialize_all = "snake_case")]
669pub enum ToastVariant {
670 #[default]
671 Info,
672 Success,
673 Warning,
674 Error,
675}
676
677#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
679pub struct ChecklistItem {
680 pub label: String,
681 #[serde(default)]
682 pub checked: bool,
683 #[serde(default, skip_serializing_if = "Option::is_none")]
684 pub href: Option<String>,
685}
686
687#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
689pub struct NotificationItem {
690 #[serde(default, skip_serializing_if = "Option::is_none")]
691 pub icon: Option<String>,
692 pub text: String,
693 #[serde(default, skip_serializing_if = "Option::is_none")]
694 pub timestamp: Option<String>,
695 #[serde(default)]
696 pub read: bool,
697 #[serde(default, skip_serializing_if = "Option::is_none")]
698 pub action_url: Option<String>,
699}
700
701#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
703pub struct SidebarNavItem {
704 pub label: String,
705 pub href: String,
706 #[serde(default, skip_serializing_if = "Option::is_none")]
707 pub icon: Option<String>,
708 #[serde(default)]
709 pub active: bool,
710 #[serde(default, skip_serializing_if = "Option::is_none")]
713 pub disabled: Option<bool>,
714}
715
716#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
718pub struct SidebarGroup {
719 pub label: String,
720 #[serde(default)]
721 pub collapsed: bool,
722 pub items: Vec<SidebarNavItem>,
723}
724
725#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
727pub struct StatCardProps {
728 pub label: String,
729 pub value: String,
730 #[serde(default, skip_serializing_if = "Option::is_none")]
731 pub icon: Option<String>,
732 #[serde(default, skip_serializing_if = "Option::is_none")]
733 pub subtitle: Option<String>,
734 #[serde(default, skip_serializing_if = "Option::is_none")]
736 pub sse_target: Option<String>,
737}
738
739#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
741pub struct ChecklistProps {
742 pub title: String,
743 pub items: Vec<ChecklistItem>,
744 #[serde(default = "default_true")]
745 pub dismissible: bool,
746 #[serde(default, skip_serializing_if = "Option::is_none")]
747 pub dismiss_label: Option<String>,
748 #[serde(default, skip_serializing_if = "Option::is_none")]
750 pub data_key: Option<String>,
751}
752
753fn default_true() -> bool {
754 true
755}
756
757#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
762pub struct ToastProps {
763 pub message: String,
764 #[serde(default)]
765 pub variant: ToastVariant,
766 #[serde(default, skip_serializing_if = "Option::is_none")]
768 pub timeout: Option<u32>,
769 #[serde(default = "default_true")]
770 pub dismissible: bool,
771}
772
773#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
775pub struct NotificationDropdownProps {
776 pub notifications: Vec<NotificationItem>,
777 #[serde(default, skip_serializing_if = "Option::is_none")]
778 pub empty_text: Option<String>,
779}
780
781#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
783pub struct SidebarProps {
784 #[serde(default, skip_serializing_if = "Vec::is_empty")]
785 pub fixed_top: Vec<SidebarNavItem>,
786 #[serde(default, skip_serializing_if = "Vec::is_empty")]
787 pub groups: Vec<SidebarGroup>,
788 #[serde(default, skip_serializing_if = "Vec::is_empty")]
789 pub fixed_bottom: Vec<SidebarNavItem>,
790}
791
792#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
794pub struct HeaderProps {
795 pub business_name: String,
796 #[serde(default, skip_serializing_if = "Option::is_none")]
798 pub notification_count: Option<u32>,
799 #[serde(default, skip_serializing_if = "Option::is_none")]
800 pub user_name: Option<String>,
801 #[serde(default, skip_serializing_if = "Option::is_none")]
802 pub user_avatar: Option<String>,
803 #[serde(default, skip_serializing_if = "Option::is_none")]
804 pub logout_url: Option<String>,
805}
806
807#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
809#[serde(rename_all = "snake_case")]
810pub enum GapSize {
811 None,
812 Sm,
813 #[default]
814 Md,
815 Lg,
816 Xl,
817}
818
819#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
821pub struct GridProps {
822 #[serde(default = "default_grid_columns")]
824 pub columns: u8,
825 #[serde(default, skip_serializing_if = "Option::is_none")]
827 pub md_columns: Option<u8>,
828 #[serde(default, skip_serializing_if = "Option::is_none")]
830 pub lg_columns: Option<u8>,
831 #[serde(default)]
833 pub gap: GapSize,
834 #[serde(default, skip_serializing_if = "Option::is_none")]
837 pub scrollable: Option<bool>,
838}
839
840fn default_grid_columns() -> u8 {
841 2
842}
843
844#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
846pub struct CollapsibleProps {
847 pub title: String,
848 #[serde(default)]
849 pub expanded: bool,
850}
851
852#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
854pub struct EmptyStateProps {
855 pub title: String,
856 #[serde(default, skip_serializing_if = "Option::is_none")]
857 pub description: Option<String>,
858 #[serde(default, skip_serializing_if = "Option::is_none")]
859 pub action: Option<Action>,
860 #[serde(default, skip_serializing_if = "Option::is_none")]
861 pub action_label: Option<String>,
862}
863
864#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
866#[serde(rename_all = "snake_case")]
867pub enum FormSectionLayout {
868 #[default]
869 Stacked,
870 TwoColumn,
871}
872
873#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
875pub struct FormSectionProps {
876 pub title: String,
877 #[serde(default, skip_serializing_if = "Option::is_none")]
878 pub description: Option<String>,
879 #[serde(default, skip_serializing_if = "Option::is_none")]
881 pub layout: Option<FormSectionLayout>,
882}
883
884#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
886pub struct PageHeaderProps {
887 pub title: String,
888 #[serde(default, skip_serializing_if = "Vec::is_empty")]
889 pub breadcrumb: Vec<BreadcrumbItem>,
890 #[serde(
892 default,
893 deserialize_with = "deserialize_actions_lax",
894 skip_serializing_if = "Vec::is_empty"
895 )]
896 pub actions: Vec<String>,
897}
898
899#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Default)]
901pub struct ButtonGroupProps {
902 #[serde(default)]
904 pub gap: GapSize,
905}
906
907#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
916pub struct DetailPageProps {
917 pub title: String,
918 #[serde(default, skip_serializing_if = "Vec::is_empty")]
919 pub breadcrumb: Vec<BreadcrumbItem>,
920 #[serde(
922 default,
923 deserialize_with = "deserialize_actions_lax",
924 skip_serializing_if = "Vec::is_empty"
925 )]
926 pub actions: Vec<String>,
927 #[serde(default, skip_serializing_if = "Vec::is_empty")]
930 pub info: Vec<String>,
931}
932
933#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
935pub struct DropdownMenuAction {
936 pub label: String,
937 pub action: Action,
938 #[serde(default)]
939 pub destructive: bool,
940}
941
942#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
944pub struct DropdownMenuProps {
945 pub menu_id: String,
946 pub trigger_label: String,
947 pub items: Vec<DropdownMenuAction>,
948 #[serde(default, skip_serializing_if = "Option::is_none")]
949 pub trigger_variant: Option<ButtonVariant>,
950}
951
952#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
955pub struct DataTableProps {
956 pub columns: Vec<Column>,
957 pub data_path: String,
958 #[serde(default, skip_serializing_if = "Option::is_none")]
959 pub row_actions: Option<Vec<DropdownMenuAction>>,
960 #[serde(default, skip_serializing_if = "Option::is_none")]
961 pub empty_message: Option<String>,
962 #[serde(default, skip_serializing_if = "Option::is_none")]
963 pub row_key: Option<String>,
964 #[serde(default, skip_serializing_if = "Option::is_none")]
966 pub row_href: Option<String>,
967}
968
969#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
973pub struct MediaCardGridProps {
974 pub data_path: String,
975 pub title_key: String,
977 #[serde(default, skip_serializing_if = "Option::is_none")]
979 pub description_key: Option<String>,
980 #[serde(default, skip_serializing_if = "Option::is_none")]
982 pub image_key: Option<String>,
983 #[serde(default, skip_serializing_if = "Option::is_none")]
985 pub image_href_key: Option<String>,
986 #[serde(default, skip_serializing_if = "Option::is_none")]
988 pub image_aspect_ratio: Option<String>,
989 #[serde(default, skip_serializing_if = "Option::is_none")]
991 pub badge_key: Option<String>,
992 #[serde(default, skip_serializing_if = "Option::is_none")]
994 pub badge_variant_key: Option<String>,
995 #[serde(default, skip_serializing_if = "Option::is_none")]
997 pub row_key: Option<String>,
998 #[serde(default, skip_serializing_if = "Option::is_none")]
999 pub row_actions: Option<Vec<DropdownMenuAction>>,
1000 #[serde(default, skip_serializing_if = "Option::is_none")]
1001 pub empty_message: Option<String>,
1002 #[serde(default, skip_serializing_if = "Option::is_none")]
1004 pub columns: Option<u8>,
1005}
1006
1007#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1009pub struct KanbanColumnProps {
1010 pub id: String,
1011 pub title: String,
1012 pub count: u32,
1013 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1015 pub children: Vec<String>,
1016}
1017
1018#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1020pub struct KanbanBoardProps {
1021 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1023 pub columns: Vec<KanbanColumnProps>,
1024 #[serde(default, skip_serializing_if = "Option::is_none")]
1029 pub data_path: Option<String>,
1030 #[serde(default, skip_serializing_if = "Option::is_none")]
1031 pub mobile_default_column: Option<String>,
1032 #[serde(default, skip_serializing_if = "Option::is_none")]
1036 pub empty_label: Option<String>,
1037}
1038
1039#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1044pub struct CalendarCellProps {
1045 pub day: u8,
1046 #[serde(default)]
1047 pub is_today: bool,
1048 #[serde(default)]
1049 pub is_current_month: bool,
1050 #[serde(default)]
1051 pub event_count: u32,
1052 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1055 pub dot_colors: Vec<String>,
1056}
1057
1058#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1060#[serde(rename_all = "snake_case")]
1061pub enum ActionCardVariant {
1062 #[default]
1063 Default,
1064 Setup,
1065 Danger,
1066}
1067
1068#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1073pub struct ActionCardProps {
1074 pub title: String,
1075 pub description: String,
1076 #[serde(default, skip_serializing_if = "Option::is_none")]
1077 pub icon: Option<String>,
1078 #[serde(default)]
1079 pub variant: ActionCardVariant,
1080 #[serde(default, skip_serializing_if = "Option::is_none")]
1082 pub href: Option<String>,
1083}
1084
1085#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1090pub struct ProductTileProps {
1091 pub product_id: String,
1092 pub name: String,
1093 pub price: String,
1094 pub field: String,
1095 #[serde(default, skip_serializing_if = "Option::is_none")]
1096 pub default_quantity: Option<u32>,
1097}
1098
1099fn deserialize_actions_lax<'de, D: serde::Deserializer<'de>>(
1105 d: D,
1106) -> Result<Vec<String>, D::Error> {
1107 use serde::de::Error;
1108 let v = serde_json::Value::deserialize(d)?;
1109 match v {
1110 serde_json::Value::Null => Ok(Vec::new()),
1111 serde_json::Value::String(s) if s.is_empty() => Ok(Vec::new()),
1112 serde_json::Value::Array(arr) => arr
1113 .into_iter()
1114 .map(|item| {
1115 item.as_str()
1116 .map(String::from)
1117 .ok_or_else(|| D::Error::custom("PageHeader.actions: expected string in array"))
1118 })
1119 .collect(),
1120 other => Err(D::Error::custom(format!(
1121 "PageHeader.actions: expected null, empty string, or array of strings; got {other:?}"
1122 ))),
1123 }
1124}
1125
1126#[cfg(test)]
1127mod schema_smoke_tests {
1128 use super::*;
1139
1140 fn assert_schema_nonempty_object<T: schemars::JsonSchema>(type_label: &str) {
1141 let schema = schemars::schema_for!(T);
1142 let value = serde_json::to_value(&schema).expect("schema serializes to JSON");
1143 assert!(
1144 value.is_object(),
1145 "{type_label}: schema must be a JSON object"
1146 );
1147 let props = value
1148 .get("properties")
1149 .and_then(|p| p.as_object())
1150 .map(|o| !o.is_empty())
1151 .unwrap_or(false);
1152 assert!(
1153 props,
1154 "{type_label}: schema must have a non-empty `properties` field"
1155 );
1156 }
1157
1158 #[test]
1159 fn schema_for_card_props_generates() {
1160 assert_schema_nonempty_object::<CardProps>("CardProps");
1161 }
1162
1163 #[test]
1164 fn schema_for_table_props_generates() {
1165 assert_schema_nonempty_object::<TableProps>("TableProps");
1166 }
1167
1168 #[test]
1169 fn schema_for_form_props_generates() {
1170 assert_schema_nonempty_object::<FormProps>("FormProps");
1171 }
1172
1173 #[test]
1174 fn schema_for_button_props_generates() {
1175 assert_schema_nonempty_object::<ButtonProps>("ButtonProps");
1176 }
1177
1178 #[test]
1179 fn schema_for_input_props_generates() {
1180 assert_schema_nonempty_object::<InputProps>("InputProps");
1181 }
1182
1183 #[test]
1184 fn schema_for_select_props_generates() {
1185 assert_schema_nonempty_object::<SelectProps>("SelectProps");
1186 }
1187
1188 #[test]
1189 fn schema_for_alert_props_generates() {
1190 assert_schema_nonempty_object::<AlertProps>("AlertProps");
1191 }
1192
1193 #[test]
1194 fn schema_for_badge_props_generates() {
1195 assert_schema_nonempty_object::<BadgeProps>("BadgeProps");
1196 }
1197
1198 #[test]
1199 fn schema_for_modal_props_generates() {
1200 assert_schema_nonempty_object::<ModalProps>("ModalProps");
1201 }
1202
1203 #[test]
1204 fn schema_for_text_props_generates() {
1205 assert_schema_nonempty_object::<TextProps>("TextProps");
1206 }
1207
1208 #[test]
1209 fn schema_for_checkbox_props_generates() {
1210 assert_schema_nonempty_object::<CheckboxProps>("CheckboxProps");
1211 }
1212
1213 #[test]
1214 fn schema_for_switch_props_generates() {
1215 assert_schema_nonempty_object::<SwitchProps>("SwitchProps");
1216 }
1217
1218 #[test]
1219 fn schema_for_separator_props_generates() {
1220 assert_schema_nonempty_object::<SeparatorProps>("SeparatorProps");
1221 }
1222
1223 #[test]
1224 fn schema_for_description_list_props_generates() {
1225 assert_schema_nonempty_object::<DescriptionListProps>("DescriptionListProps");
1226 }
1227
1228 #[test]
1229 fn schema_for_tab_generates() {
1230 assert_schema_nonempty_object::<Tab>("Tab");
1231 }
1232
1233 #[test]
1234 fn schema_for_tabs_props_generates() {
1235 assert_schema_nonempty_object::<TabsProps>("TabsProps");
1236 }
1237
1238 #[test]
1239 fn schema_for_breadcrumb_props_generates() {
1240 assert_schema_nonempty_object::<BreadcrumbProps>("BreadcrumbProps");
1241 }
1242
1243 #[test]
1244 fn schema_for_pagination_props_generates() {
1245 assert_schema_nonempty_object::<PaginationProps>("PaginationProps");
1246 }
1247
1248 #[test]
1249 fn schema_for_progress_props_generates() {
1250 assert_schema_nonempty_object::<ProgressProps>("ProgressProps");
1251 }
1252
1253 #[test]
1254 fn schema_for_image_props_generates() {
1255 assert_schema_nonempty_object::<ImageProps>("ImageProps");
1256 }
1257
1258 #[test]
1259 fn image_inline_svg_factory_roundtrips_via_serde() {
1260 let p = ImageProps::inline_svg("<svg/>", "alt");
1261 let json = serde_json::to_value(&p).expect("serialization must not fail");
1262 let parsed: ImageProps =
1263 serde_json::from_value(json).expect("deserialization must not fail");
1264 assert_eq!(parsed.inline_svg, Some("<svg/>".to_string()));
1265 assert_eq!(parsed.alt, "alt");
1266 assert_eq!(parsed.src, "");
1267 }
1268
1269 #[test]
1270 fn schema_for_avatar_props_generates() {
1271 assert_schema_nonempty_object::<AvatarProps>("AvatarProps");
1272 }
1273
1274 #[test]
1275 fn schema_for_skeleton_props_generates() {
1276 assert_schema_nonempty_object::<SkeletonProps>("SkeletonProps");
1277 }
1278
1279 #[test]
1280 fn schema_for_stat_card_props_generates() {
1281 assert_schema_nonempty_object::<StatCardProps>("StatCardProps");
1282 }
1283
1284 #[test]
1285 fn schema_for_checklist_props_generates() {
1286 assert_schema_nonempty_object::<ChecklistProps>("ChecklistProps");
1287 }
1288
1289 #[test]
1290 fn schema_for_toast_props_generates() {
1291 assert_schema_nonempty_object::<ToastProps>("ToastProps");
1292 }
1293
1294 #[test]
1295 fn schema_for_notification_dropdown_props_generates() {
1296 assert_schema_nonempty_object::<NotificationDropdownProps>("NotificationDropdownProps");
1297 }
1298
1299 #[test]
1300 fn schema_for_sidebar_props_generates() {
1301 assert_schema_nonempty_object::<SidebarProps>("SidebarProps");
1302 }
1303
1304 #[test]
1305 fn schema_for_header_props_generates() {
1306 assert_schema_nonempty_object::<HeaderProps>("HeaderProps");
1307 }
1308
1309 #[test]
1310 fn schema_for_grid_props_generates() {
1311 assert_schema_nonempty_object::<GridProps>("GridProps");
1312 }
1313
1314 #[test]
1315 fn schema_for_collapsible_props_generates() {
1316 assert_schema_nonempty_object::<CollapsibleProps>("CollapsibleProps");
1317 }
1318
1319 #[test]
1320 fn schema_for_empty_state_props_generates() {
1321 assert_schema_nonempty_object::<EmptyStateProps>("EmptyStateProps");
1322 }
1323
1324 #[test]
1325 fn schema_for_form_section_props_generates() {
1326 assert_schema_nonempty_object::<FormSectionProps>("FormSectionProps");
1327 }
1328
1329 #[test]
1330 fn schema_for_page_header_props_generates() {
1331 assert_schema_nonempty_object::<PageHeaderProps>("PageHeaderProps");
1332 }
1333
1334 #[test]
1335 fn schema_for_button_group_props_generates() {
1336 assert_schema_nonempty_object::<ButtonGroupProps>("ButtonGroupProps");
1337 }
1338
1339 #[test]
1340 fn schema_for_dropdown_menu_action_generates() {
1341 assert_schema_nonempty_object::<DropdownMenuAction>("DropdownMenuAction");
1342 }
1343
1344 #[test]
1345 fn schema_for_dropdown_menu_props_generates() {
1346 assert_schema_nonempty_object::<DropdownMenuProps>("DropdownMenuProps");
1347 }
1348
1349 #[test]
1350 fn schema_for_data_table_props_generates() {
1351 assert_schema_nonempty_object::<DataTableProps>("DataTableProps");
1352 }
1353
1354 #[test]
1355 fn schema_for_kanban_column_props_generates() {
1356 assert_schema_nonempty_object::<KanbanColumnProps>("KanbanColumnProps");
1357 }
1358
1359 #[test]
1360 fn schema_for_kanban_board_props_generates() {
1361 assert_schema_nonempty_object::<KanbanBoardProps>("KanbanBoardProps");
1362 }
1363
1364 #[test]
1365 fn schema_for_calendar_cell_props_generates() {
1366 assert_schema_nonempty_object::<CalendarCellProps>("CalendarCellProps");
1367 }
1368
1369 #[test]
1370 fn schema_for_action_card_props_generates() {
1371 assert_schema_nonempty_object::<ActionCardProps>("ActionCardProps");
1372 }
1373
1374 #[test]
1375 fn schema_for_product_tile_props_generates() {
1376 assert_schema_nonempty_object::<ProductTileProps>("ProductTileProps");
1377 }
1378
1379 #[test]
1380 fn card_props_round_trips_footer() {
1381 let original = CardProps {
1382 title: "Hero".to_string(),
1383 description: None,
1384 subtitle: None,
1385 badge: None,
1386 max_width: None,
1387 footer: vec!["btn1".to_string(), "btn2".to_string()],
1388 variant: CardVariant::Bordered,
1389 };
1390 let json = serde_json::to_string(&original).unwrap();
1391 let parsed: CardProps = serde_json::from_str(&json).unwrap();
1392 assert_eq!(original.footer, parsed.footer);
1393 }
1394
1395 #[test]
1396 fn tab_round_trips_children() {
1397 let original = Tab {
1398 value: "overview".to_string(),
1399 label: "Overview".to_string(),
1400 children: vec!["panel1".to_string()],
1401 };
1402 let json = serde_json::to_string(&original).unwrap();
1403 let parsed: Tab = serde_json::from_str(&json).unwrap();
1404 assert_eq!(original.children, parsed.children);
1405 }
1406
1407 #[test]
1408 fn card_props_omits_empty_footer_in_json() {
1409 let card = CardProps {
1410 title: "Card".to_string(),
1411 description: None,
1412 subtitle: None,
1413 badge: None,
1414 max_width: None,
1415 footer: Vec::new(),
1416 variant: CardVariant::Bordered,
1417 };
1418 let json = serde_json::to_string(&card).unwrap();
1419 assert!(
1420 !json.contains("\"footer\""),
1421 "empty footer must be skipped, got: {json}"
1422 );
1423 }
1424
1425 #[test]
1426 fn card_props_round_trips_badge() {
1427 let original = CardProps {
1428 title: "Hero".to_string(),
1429 description: None,
1430 subtitle: None,
1431 badge: Some("Scade tra 9m".to_string()),
1432 max_width: None,
1433 footer: Vec::new(),
1434 variant: CardVariant::Bordered,
1435 };
1436 let json = serde_json::to_string(&original).unwrap();
1437 let parsed: CardProps = serde_json::from_str(&json).unwrap();
1438 assert_eq!(original.badge, parsed.badge);
1439 }
1440
1441 #[test]
1442 fn card_props_omits_empty_badge_in_json() {
1443 let card = CardProps {
1444 title: "Card".to_string(),
1445 description: None,
1446 subtitle: None,
1447 badge: None,
1448 max_width: None,
1449 footer: Vec::new(),
1450 variant: CardVariant::Bordered,
1451 };
1452 let json = serde_json::to_string(&card).unwrap();
1453 assert!(
1454 !json.contains("\"badge\""),
1455 "empty badge must be skipped, got: {json}"
1456 );
1457 }
1458
1459 #[test]
1460 fn card_props_round_trips_subtitle() {
1461 let original = CardProps {
1462 title: "Hero".to_string(),
1463 description: None,
1464 subtitle: Some("Marco Rossi".to_string()),
1465 badge: None,
1466 max_width: None,
1467 footer: Vec::new(),
1468 variant: CardVariant::Bordered,
1469 };
1470 let json = serde_json::to_string(&original).unwrap();
1471 let parsed: CardProps = serde_json::from_str(&json).unwrap();
1472 assert_eq!(original.subtitle, parsed.subtitle);
1473 }
1474
1475 #[test]
1476 fn card_props_omits_empty_subtitle_in_json() {
1477 let card = CardProps {
1478 title: "Card".to_string(),
1479 description: None,
1480 subtitle: None,
1481 badge: None,
1482 max_width: None,
1483 footer: Vec::new(),
1484 variant: CardVariant::Bordered,
1485 };
1486 let json = serde_json::to_string(&card).unwrap();
1487 assert!(
1488 !json.contains("\"subtitle\""),
1489 "empty subtitle must be skipped, got: {json}"
1490 );
1491 }
1492
1493 #[test]
1494 fn card_props_schema_includes_badge() {
1495 let schema = schemars::schema_for!(CardProps);
1496 let value = serde_json::to_value(&schema).expect("schema serializes to JSON");
1497 let props = value
1498 .get("properties")
1499 .and_then(|p| p.as_object())
1500 .expect("schema has a properties object");
1501 assert!(
1502 props.contains_key("badge"),
1503 "CardProps schema must expose a `badge` property; got keys: {:?}",
1504 props.keys().collect::<Vec<_>>()
1505 );
1506 let badge_schema = props.get("badge").expect("badge entry");
1511 let badge_json = badge_schema.to_string();
1512 assert!(
1513 badge_json.contains("\"string\""),
1514 "badge schema entry must mention string type; got: {badge_json}"
1515 );
1516 }
1517
1518 #[test]
1519 fn card_props_schema_includes_subtitle() {
1520 let schema = schemars::schema_for!(CardProps);
1521 let value = serde_json::to_value(&schema).expect("schema serializes to JSON");
1522 let props = value
1523 .get("properties")
1524 .and_then(|p| p.as_object())
1525 .expect("schema has a properties object");
1526 assert!(
1527 props.contains_key("subtitle"),
1528 "CardProps schema must expose a `subtitle` property; got keys: {:?}",
1529 props.keys().collect::<Vec<_>>()
1530 );
1531 let subtitle_schema = props.get("subtitle").expect("subtitle entry");
1536 let subtitle_json = subtitle_schema.to_string();
1537 assert!(
1538 subtitle_json.contains("\"string\""),
1539 "subtitle schema entry must mention string type; got: {subtitle_json}"
1540 );
1541 }
1542
1543 #[test]
1544 fn schema_for_checkbox_list_props_generates() {
1545 assert_schema_nonempty_object::<CheckboxListProps>("CheckboxListProps");
1546 }
1547
1548 #[test]
1549 fn checkbox_list_props_serde_roundtrip() {
1550 let json = serde_json::json!({
1551 "field": "services",
1552 "options": [{"value": "a", "label": "Alpha"}, {"value": "b", "label": "Beta"}],
1553 "selected_path": "/preselected"
1554 });
1555 let parsed: CheckboxListProps = serde_json::from_value(json.clone()).expect("decode");
1556 assert_eq!(parsed.field, "services");
1557 assert_eq!(parsed.options.len(), 2);
1558 assert_eq!(parsed.selected_path.as_deref(), Some("/preselected"));
1559 let reserialized = serde_json::to_value(&parsed).expect("encode");
1560 assert!(reserialized.get("label").is_none());
1562 assert!(reserialized.get("disabled").is_none());
1563 }
1564
1565 #[test]
1566 fn schema_for_rich_text_editor_props_generates() {
1567 assert_schema_nonempty_object::<RichTextEditorProps>("RichTextEditorProps");
1568 }
1569
1570 #[test]
1571 fn rich_text_editor_props_serde_roundtrip() {
1572 let json = serde_json::json!({
1573 "field": "body",
1574 "label": "Body"
1575 });
1576 let parsed: RichTextEditorProps = serde_json::from_value(json).expect("decode");
1577 assert_eq!(parsed.field, "body");
1578 assert_eq!(parsed.label, "Body");
1579 assert!(parsed.placeholder.is_none());
1580 assert!(parsed.default_value.is_none());
1581 assert!(parsed.data_path.is_none());
1582 assert!(parsed.error.is_none());
1583 let reserialized = serde_json::to_value(&parsed).expect("encode");
1584 assert!(reserialized.get("placeholder").is_none());
1586 assert!(reserialized.get("error").is_none());
1587 }
1588}
1589
1590#[cfg(test)]
1591mod strum_tests {
1592 use super::*;
1593
1594 #[test]
1598 fn variant_enums_strum_matches_serde_wire_format() {
1599 fn check<T: AsRef<str> + serde::Serialize>(variants: &[T], label: &str) {
1600 for v in variants {
1601 let json = serde_json::to_string(v).expect("serialize");
1602 let json_stripped = json.trim_matches('"');
1603 assert_eq!(
1604 v.as_ref(),
1605 json_stripped,
1606 "strum AsRefStr drifted from serde for {label} variant"
1607 );
1608 }
1609 }
1610 check(
1611 &[
1612 AlertVariant::Info,
1613 AlertVariant::Success,
1614 AlertVariant::Warning,
1615 AlertVariant::Error,
1616 ],
1617 "AlertVariant",
1618 );
1619 check(
1620 &[
1621 BadgeVariant::Default,
1622 BadgeVariant::Secondary,
1623 BadgeVariant::Destructive,
1624 BadgeVariant::Outline,
1625 ],
1626 "BadgeVariant",
1627 );
1628 check(
1629 &[
1630 ButtonVariant::Default,
1631 ButtonVariant::Secondary,
1632 ButtonVariant::Destructive,
1633 ButtonVariant::Outline,
1634 ButtonVariant::Ghost,
1635 ButtonVariant::Link,
1636 ],
1637 "ButtonVariant",
1638 );
1639 check(
1640 &[
1641 ToastVariant::Info,
1642 ToastVariant::Success,
1643 ToastVariant::Warning,
1644 ToastVariant::Error,
1645 ],
1646 "ToastVariant",
1647 );
1648 }
1649
1650 #[test]
1651 fn alert_variant_as_ref_str_matches_wire_format() {
1652 assert_eq!(AlertVariant::Success.as_ref(), "success");
1653 assert_eq!(AlertVariant::Warning.as_ref(), "warning");
1654 assert_eq!(AlertVariant::Info.as_ref(), "info");
1655 assert_eq!(AlertVariant::Error.as_ref(), "error");
1656 }
1657}
1658
1659#[cfg(test)]
1660mod card_variant_tests {
1661 use super::*;
1662
1663 #[test]
1664 fn card_variant_default_is_bordered() {
1665 assert_eq!(CardVariant::default(), CardVariant::Bordered);
1666 }
1667
1668 #[test]
1669 fn card_variant_serializes_snake_case() {
1670 assert_eq!(
1671 serde_json::to_value(CardVariant::Bordered).unwrap(),
1672 serde_json::json!("bordered")
1673 );
1674 assert_eq!(
1675 serde_json::to_value(CardVariant::Elevated).unwrap(),
1676 serde_json::json!("elevated")
1677 );
1678 }
1679
1680 #[test]
1681 fn card_variant_deserializes_snake_case() {
1682 assert_eq!(
1683 serde_json::from_value::<CardVariant>(serde_json::json!("bordered")).unwrap(),
1684 CardVariant::Bordered
1685 );
1686 assert_eq!(
1687 serde_json::from_value::<CardVariant>(serde_json::json!("elevated")).unwrap(),
1688 CardVariant::Elevated
1689 );
1690 }
1691
1692 #[test]
1693 fn card_props_without_variant_defaults_to_bordered() {
1694 let v = serde_json::json!({"title": "x"});
1695 let p: CardProps = serde_json::from_value(v).unwrap();
1696 assert_eq!(p.variant, CardVariant::Bordered);
1697 }
1698
1699 #[test]
1700 fn card_props_with_elevated_variant() {
1701 let v = serde_json::json!({"title": "x", "variant": "elevated"});
1702 let p: CardProps = serde_json::from_value(v).unwrap();
1703 assert_eq!(p.variant, CardVariant::Elevated);
1704 }
1705
1706 #[test]
1707 fn card_props_roundtrip_preserves_variant() {
1708 let p = CardProps {
1709 title: "x".into(),
1710 description: None,
1711 subtitle: None,
1712 badge: None,
1713 max_width: None,
1714 footer: vec![],
1715 variant: CardVariant::Elevated,
1716 };
1717 let j = serde_json::to_value(&p).unwrap();
1718 let back: CardProps = serde_json::from_value(j).unwrap();
1719 assert_eq!(back.variant, CardVariant::Elevated);
1720 }
1721}
1722
1723#[cfg(test)]
1724mod kanban_board_props_tests {
1725 use super::*;
1726
1727 #[test]
1728 fn kanban_board_props_serde_static_columns() {
1729 let v = serde_json::json!({
1730 "columns": [{"title": "To Do", "items": [], "id": "todo", "count": 0}]
1731 });
1732 let p: KanbanBoardProps = serde_json::from_value(v).unwrap();
1733 assert_eq!(p.columns.len(), 1);
1734 assert!(p.data_path.is_none());
1735 }
1736
1737 #[test]
1738 fn kanban_board_props_serde_data_path() {
1739 let v = serde_json::json!({"data_path": "/columns"});
1740 let p: KanbanBoardProps = serde_json::from_value(v).unwrap();
1741 assert!(p.columns.is_empty());
1742 assert_eq!(p.data_path.as_deref(), Some("/columns"));
1743 }
1744
1745 #[test]
1746 fn kanban_board_props_serde_neither() {
1747 let v = serde_json::json!({});
1748 let p: KanbanBoardProps = serde_json::from_value(v).unwrap();
1749 assert!(p.columns.is_empty());
1750 assert!(p.data_path.is_none());
1751 }
1752
1753 #[test]
1754 fn kanban_board_props_empty_columns_skipped_on_serialize() {
1755 let p = KanbanBoardProps {
1756 columns: vec![],
1757 data_path: Some("/x".into()),
1758 mobile_default_column: None,
1759 empty_label: None,
1760 };
1761 let j = serde_json::to_value(&p).unwrap();
1762 assert!(
1763 j.get("columns").is_none(),
1764 "empty columns must be skipped, got: {j}"
1765 );
1766 assert_eq!(j.get("data_path").and_then(|v| v.as_str()), Some("/x"));
1767 }
1768}
1769
1770#[cfg(test)]
1771mod page_header_actions_tests {
1772 use super::*;
1773
1774 #[test]
1775 fn page_header_actions_missing_field() {
1776 let v = serde_json::json!({"title": "X"});
1777 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
1778 assert!(p.actions.is_empty());
1779 }
1780
1781 #[test]
1782 fn page_header_actions_null() {
1783 let v = serde_json::json!({"title": "X", "actions": null});
1784 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
1785 assert!(p.actions.is_empty());
1786 }
1787
1788 #[test]
1789 fn page_header_actions_empty_string() {
1790 let v = serde_json::json!({"title": "X", "actions": ""});
1791 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
1792 assert!(p.actions.is_empty());
1793 }
1794
1795 #[test]
1796 fn page_header_actions_empty_array() {
1797 let v = serde_json::json!({"title": "X", "actions": []});
1798 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
1799 assert!(p.actions.is_empty());
1800 }
1801
1802 #[test]
1803 fn page_header_actions_non_empty_array() {
1804 let v = serde_json::json!({"title": "X", "actions": ["a", "b"]});
1805 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
1806 assert_eq!(p.actions, vec!["a".to_string(), "b".to_string()]);
1807 }
1808
1809 #[test]
1810 fn page_header_actions_non_empty_string_rejected() {
1811 let v = serde_json::json!({"title": "X", "actions": "not-empty"});
1812 let result: Result<PageHeaderProps, _> = serde_json::from_value(v);
1813 assert!(result.is_err(), "non-empty string must be rejected");
1814 }
1815
1816 #[test]
1817 fn page_header_actions_non_string_array_rejected() {
1818 let v = serde_json::json!({"title": "X", "actions": [1, 2, 3]});
1819 let result: Result<PageHeaderProps, _> = serde_json::from_value(v);
1820 assert!(result.is_err(), "array of non-strings must be rejected");
1821 }
1822}