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)]
971pub struct KanbanColumnProps {
972 pub id: String,
973 pub title: String,
974 pub count: u32,
975 #[serde(default, skip_serializing_if = "Vec::is_empty")]
977 pub children: Vec<String>,
978}
979
980#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
982pub struct KanbanBoardProps {
983 #[serde(default, skip_serializing_if = "Vec::is_empty")]
985 pub columns: Vec<KanbanColumnProps>,
986 #[serde(default, skip_serializing_if = "Option::is_none")]
991 pub data_path: Option<String>,
992 #[serde(default, skip_serializing_if = "Option::is_none")]
993 pub mobile_default_column: Option<String>,
994 #[serde(default, skip_serializing_if = "Option::is_none")]
998 pub empty_label: Option<String>,
999}
1000
1001#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1006pub struct CalendarCellProps {
1007 pub day: u8,
1008 #[serde(default)]
1009 pub is_today: bool,
1010 #[serde(default)]
1011 pub is_current_month: bool,
1012 #[serde(default)]
1013 pub event_count: u32,
1014 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1017 pub dot_colors: Vec<String>,
1018}
1019
1020#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1022#[serde(rename_all = "snake_case")]
1023pub enum ActionCardVariant {
1024 #[default]
1025 Default,
1026 Setup,
1027 Danger,
1028}
1029
1030#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1035pub struct ActionCardProps {
1036 pub title: String,
1037 pub description: String,
1038 #[serde(default, skip_serializing_if = "Option::is_none")]
1039 pub icon: Option<String>,
1040 #[serde(default)]
1041 pub variant: ActionCardVariant,
1042 #[serde(default, skip_serializing_if = "Option::is_none")]
1044 pub href: Option<String>,
1045}
1046
1047#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1052pub struct ProductTileProps {
1053 pub product_id: String,
1054 pub name: String,
1055 pub price: String,
1056 pub field: String,
1057 #[serde(default, skip_serializing_if = "Option::is_none")]
1058 pub default_quantity: Option<u32>,
1059}
1060
1061fn deserialize_actions_lax<'de, D: serde::Deserializer<'de>>(
1067 d: D,
1068) -> Result<Vec<String>, D::Error> {
1069 use serde::de::Error;
1070 let v = serde_json::Value::deserialize(d)?;
1071 match v {
1072 serde_json::Value::Null => Ok(Vec::new()),
1073 serde_json::Value::String(s) if s.is_empty() => Ok(Vec::new()),
1074 serde_json::Value::Array(arr) => arr
1075 .into_iter()
1076 .map(|item| {
1077 item.as_str()
1078 .map(String::from)
1079 .ok_or_else(|| D::Error::custom("PageHeader.actions: expected string in array"))
1080 })
1081 .collect(),
1082 other => Err(D::Error::custom(format!(
1083 "PageHeader.actions: expected null, empty string, or array of strings; got {other:?}"
1084 ))),
1085 }
1086}
1087
1088#[cfg(test)]
1089mod schema_smoke_tests {
1090 use super::*;
1101
1102 fn assert_schema_nonempty_object<T: schemars::JsonSchema>(type_label: &str) {
1103 let schema = schemars::schema_for!(T);
1104 let value = serde_json::to_value(&schema).expect("schema serializes to JSON");
1105 assert!(
1106 value.is_object(),
1107 "{type_label}: schema must be a JSON object"
1108 );
1109 let props = value
1110 .get("properties")
1111 .and_then(|p| p.as_object())
1112 .map(|o| !o.is_empty())
1113 .unwrap_or(false);
1114 assert!(
1115 props,
1116 "{type_label}: schema must have a non-empty `properties` field"
1117 );
1118 }
1119
1120 #[test]
1121 fn schema_for_card_props_generates() {
1122 assert_schema_nonempty_object::<CardProps>("CardProps");
1123 }
1124
1125 #[test]
1126 fn schema_for_table_props_generates() {
1127 assert_schema_nonempty_object::<TableProps>("TableProps");
1128 }
1129
1130 #[test]
1131 fn schema_for_form_props_generates() {
1132 assert_schema_nonempty_object::<FormProps>("FormProps");
1133 }
1134
1135 #[test]
1136 fn schema_for_button_props_generates() {
1137 assert_schema_nonempty_object::<ButtonProps>("ButtonProps");
1138 }
1139
1140 #[test]
1141 fn schema_for_input_props_generates() {
1142 assert_schema_nonempty_object::<InputProps>("InputProps");
1143 }
1144
1145 #[test]
1146 fn schema_for_select_props_generates() {
1147 assert_schema_nonempty_object::<SelectProps>("SelectProps");
1148 }
1149
1150 #[test]
1151 fn schema_for_alert_props_generates() {
1152 assert_schema_nonempty_object::<AlertProps>("AlertProps");
1153 }
1154
1155 #[test]
1156 fn schema_for_badge_props_generates() {
1157 assert_schema_nonempty_object::<BadgeProps>("BadgeProps");
1158 }
1159
1160 #[test]
1161 fn schema_for_modal_props_generates() {
1162 assert_schema_nonempty_object::<ModalProps>("ModalProps");
1163 }
1164
1165 #[test]
1166 fn schema_for_text_props_generates() {
1167 assert_schema_nonempty_object::<TextProps>("TextProps");
1168 }
1169
1170 #[test]
1171 fn schema_for_checkbox_props_generates() {
1172 assert_schema_nonempty_object::<CheckboxProps>("CheckboxProps");
1173 }
1174
1175 #[test]
1176 fn schema_for_switch_props_generates() {
1177 assert_schema_nonempty_object::<SwitchProps>("SwitchProps");
1178 }
1179
1180 #[test]
1181 fn schema_for_separator_props_generates() {
1182 assert_schema_nonempty_object::<SeparatorProps>("SeparatorProps");
1183 }
1184
1185 #[test]
1186 fn schema_for_description_list_props_generates() {
1187 assert_schema_nonempty_object::<DescriptionListProps>("DescriptionListProps");
1188 }
1189
1190 #[test]
1191 fn schema_for_tab_generates() {
1192 assert_schema_nonempty_object::<Tab>("Tab");
1193 }
1194
1195 #[test]
1196 fn schema_for_tabs_props_generates() {
1197 assert_schema_nonempty_object::<TabsProps>("TabsProps");
1198 }
1199
1200 #[test]
1201 fn schema_for_breadcrumb_props_generates() {
1202 assert_schema_nonempty_object::<BreadcrumbProps>("BreadcrumbProps");
1203 }
1204
1205 #[test]
1206 fn schema_for_pagination_props_generates() {
1207 assert_schema_nonempty_object::<PaginationProps>("PaginationProps");
1208 }
1209
1210 #[test]
1211 fn schema_for_progress_props_generates() {
1212 assert_schema_nonempty_object::<ProgressProps>("ProgressProps");
1213 }
1214
1215 #[test]
1216 fn schema_for_image_props_generates() {
1217 assert_schema_nonempty_object::<ImageProps>("ImageProps");
1218 }
1219
1220 #[test]
1221 fn image_inline_svg_factory_roundtrips_via_serde() {
1222 let p = ImageProps::inline_svg("<svg/>", "alt");
1223 let json = serde_json::to_value(&p).expect("serialization must not fail");
1224 let parsed: ImageProps =
1225 serde_json::from_value(json).expect("deserialization must not fail");
1226 assert_eq!(parsed.inline_svg, Some("<svg/>".to_string()));
1227 assert_eq!(parsed.alt, "alt");
1228 assert_eq!(parsed.src, "");
1229 }
1230
1231 #[test]
1232 fn schema_for_avatar_props_generates() {
1233 assert_schema_nonempty_object::<AvatarProps>("AvatarProps");
1234 }
1235
1236 #[test]
1237 fn schema_for_skeleton_props_generates() {
1238 assert_schema_nonempty_object::<SkeletonProps>("SkeletonProps");
1239 }
1240
1241 #[test]
1242 fn schema_for_stat_card_props_generates() {
1243 assert_schema_nonempty_object::<StatCardProps>("StatCardProps");
1244 }
1245
1246 #[test]
1247 fn schema_for_checklist_props_generates() {
1248 assert_schema_nonempty_object::<ChecklistProps>("ChecklistProps");
1249 }
1250
1251 #[test]
1252 fn schema_for_toast_props_generates() {
1253 assert_schema_nonempty_object::<ToastProps>("ToastProps");
1254 }
1255
1256 #[test]
1257 fn schema_for_notification_dropdown_props_generates() {
1258 assert_schema_nonempty_object::<NotificationDropdownProps>("NotificationDropdownProps");
1259 }
1260
1261 #[test]
1262 fn schema_for_sidebar_props_generates() {
1263 assert_schema_nonempty_object::<SidebarProps>("SidebarProps");
1264 }
1265
1266 #[test]
1267 fn schema_for_header_props_generates() {
1268 assert_schema_nonempty_object::<HeaderProps>("HeaderProps");
1269 }
1270
1271 #[test]
1272 fn schema_for_grid_props_generates() {
1273 assert_schema_nonempty_object::<GridProps>("GridProps");
1274 }
1275
1276 #[test]
1277 fn schema_for_collapsible_props_generates() {
1278 assert_schema_nonempty_object::<CollapsibleProps>("CollapsibleProps");
1279 }
1280
1281 #[test]
1282 fn schema_for_empty_state_props_generates() {
1283 assert_schema_nonempty_object::<EmptyStateProps>("EmptyStateProps");
1284 }
1285
1286 #[test]
1287 fn schema_for_form_section_props_generates() {
1288 assert_schema_nonempty_object::<FormSectionProps>("FormSectionProps");
1289 }
1290
1291 #[test]
1292 fn schema_for_page_header_props_generates() {
1293 assert_schema_nonempty_object::<PageHeaderProps>("PageHeaderProps");
1294 }
1295
1296 #[test]
1297 fn schema_for_button_group_props_generates() {
1298 assert_schema_nonempty_object::<ButtonGroupProps>("ButtonGroupProps");
1299 }
1300
1301 #[test]
1302 fn schema_for_dropdown_menu_action_generates() {
1303 assert_schema_nonempty_object::<DropdownMenuAction>("DropdownMenuAction");
1304 }
1305
1306 #[test]
1307 fn schema_for_dropdown_menu_props_generates() {
1308 assert_schema_nonempty_object::<DropdownMenuProps>("DropdownMenuProps");
1309 }
1310
1311 #[test]
1312 fn schema_for_data_table_props_generates() {
1313 assert_schema_nonempty_object::<DataTableProps>("DataTableProps");
1314 }
1315
1316 #[test]
1317 fn schema_for_kanban_column_props_generates() {
1318 assert_schema_nonempty_object::<KanbanColumnProps>("KanbanColumnProps");
1319 }
1320
1321 #[test]
1322 fn schema_for_kanban_board_props_generates() {
1323 assert_schema_nonempty_object::<KanbanBoardProps>("KanbanBoardProps");
1324 }
1325
1326 #[test]
1327 fn schema_for_calendar_cell_props_generates() {
1328 assert_schema_nonempty_object::<CalendarCellProps>("CalendarCellProps");
1329 }
1330
1331 #[test]
1332 fn schema_for_action_card_props_generates() {
1333 assert_schema_nonempty_object::<ActionCardProps>("ActionCardProps");
1334 }
1335
1336 #[test]
1337 fn schema_for_product_tile_props_generates() {
1338 assert_schema_nonempty_object::<ProductTileProps>("ProductTileProps");
1339 }
1340
1341 #[test]
1342 fn card_props_round_trips_footer() {
1343 let original = CardProps {
1344 title: "Hero".to_string(),
1345 description: None,
1346 subtitle: None,
1347 badge: None,
1348 max_width: None,
1349 footer: vec!["btn1".to_string(), "btn2".to_string()],
1350 variant: CardVariant::Bordered,
1351 };
1352 let json = serde_json::to_string(&original).unwrap();
1353 let parsed: CardProps = serde_json::from_str(&json).unwrap();
1354 assert_eq!(original.footer, parsed.footer);
1355 }
1356
1357 #[test]
1358 fn tab_round_trips_children() {
1359 let original = Tab {
1360 value: "overview".to_string(),
1361 label: "Overview".to_string(),
1362 children: vec!["panel1".to_string()],
1363 };
1364 let json = serde_json::to_string(&original).unwrap();
1365 let parsed: Tab = serde_json::from_str(&json).unwrap();
1366 assert_eq!(original.children, parsed.children);
1367 }
1368
1369 #[test]
1370 fn card_props_omits_empty_footer_in_json() {
1371 let card = CardProps {
1372 title: "Card".to_string(),
1373 description: None,
1374 subtitle: None,
1375 badge: None,
1376 max_width: None,
1377 footer: Vec::new(),
1378 variant: CardVariant::Bordered,
1379 };
1380 let json = serde_json::to_string(&card).unwrap();
1381 assert!(
1382 !json.contains("\"footer\""),
1383 "empty footer must be skipped, got: {json}"
1384 );
1385 }
1386
1387 #[test]
1388 fn card_props_round_trips_badge() {
1389 let original = CardProps {
1390 title: "Hero".to_string(),
1391 description: None,
1392 subtitle: None,
1393 badge: Some("Scade tra 9m".to_string()),
1394 max_width: None,
1395 footer: Vec::new(),
1396 variant: CardVariant::Bordered,
1397 };
1398 let json = serde_json::to_string(&original).unwrap();
1399 let parsed: CardProps = serde_json::from_str(&json).unwrap();
1400 assert_eq!(original.badge, parsed.badge);
1401 }
1402
1403 #[test]
1404 fn card_props_omits_empty_badge_in_json() {
1405 let card = CardProps {
1406 title: "Card".to_string(),
1407 description: None,
1408 subtitle: None,
1409 badge: None,
1410 max_width: None,
1411 footer: Vec::new(),
1412 variant: CardVariant::Bordered,
1413 };
1414 let json = serde_json::to_string(&card).unwrap();
1415 assert!(
1416 !json.contains("\"badge\""),
1417 "empty badge must be skipped, got: {json}"
1418 );
1419 }
1420
1421 #[test]
1422 fn card_props_round_trips_subtitle() {
1423 let original = CardProps {
1424 title: "Hero".to_string(),
1425 description: None,
1426 subtitle: Some("Marco Rossi".to_string()),
1427 badge: None,
1428 max_width: None,
1429 footer: Vec::new(),
1430 variant: CardVariant::Bordered,
1431 };
1432 let json = serde_json::to_string(&original).unwrap();
1433 let parsed: CardProps = serde_json::from_str(&json).unwrap();
1434 assert_eq!(original.subtitle, parsed.subtitle);
1435 }
1436
1437 #[test]
1438 fn card_props_omits_empty_subtitle_in_json() {
1439 let card = CardProps {
1440 title: "Card".to_string(),
1441 description: None,
1442 subtitle: None,
1443 badge: None,
1444 max_width: None,
1445 footer: Vec::new(),
1446 variant: CardVariant::Bordered,
1447 };
1448 let json = serde_json::to_string(&card).unwrap();
1449 assert!(
1450 !json.contains("\"subtitle\""),
1451 "empty subtitle must be skipped, got: {json}"
1452 );
1453 }
1454
1455 #[test]
1456 fn card_props_schema_includes_badge() {
1457 let schema = schemars::schema_for!(CardProps);
1458 let value = serde_json::to_value(&schema).expect("schema serializes to JSON");
1459 let props = value
1460 .get("properties")
1461 .and_then(|p| p.as_object())
1462 .expect("schema has a properties object");
1463 assert!(
1464 props.contains_key("badge"),
1465 "CardProps schema must expose a `badge` property; got keys: {:?}",
1466 props.keys().collect::<Vec<_>>()
1467 );
1468 let badge_schema = props.get("badge").expect("badge entry");
1473 let badge_json = badge_schema.to_string();
1474 assert!(
1475 badge_json.contains("\"string\""),
1476 "badge schema entry must mention string type; got: {badge_json}"
1477 );
1478 }
1479
1480 #[test]
1481 fn card_props_schema_includes_subtitle() {
1482 let schema = schemars::schema_for!(CardProps);
1483 let value = serde_json::to_value(&schema).expect("schema serializes to JSON");
1484 let props = value
1485 .get("properties")
1486 .and_then(|p| p.as_object())
1487 .expect("schema has a properties object");
1488 assert!(
1489 props.contains_key("subtitle"),
1490 "CardProps schema must expose a `subtitle` property; got keys: {:?}",
1491 props.keys().collect::<Vec<_>>()
1492 );
1493 let subtitle_schema = props.get("subtitle").expect("subtitle entry");
1498 let subtitle_json = subtitle_schema.to_string();
1499 assert!(
1500 subtitle_json.contains("\"string\""),
1501 "subtitle schema entry must mention string type; got: {subtitle_json}"
1502 );
1503 }
1504
1505 #[test]
1506 fn schema_for_checkbox_list_props_generates() {
1507 assert_schema_nonempty_object::<CheckboxListProps>("CheckboxListProps");
1508 }
1509
1510 #[test]
1511 fn checkbox_list_props_serde_roundtrip() {
1512 let json = serde_json::json!({
1513 "field": "services",
1514 "options": [{"value": "a", "label": "Alpha"}, {"value": "b", "label": "Beta"}],
1515 "selected_path": "/preselected"
1516 });
1517 let parsed: CheckboxListProps = serde_json::from_value(json.clone()).expect("decode");
1518 assert_eq!(parsed.field, "services");
1519 assert_eq!(parsed.options.len(), 2);
1520 assert_eq!(parsed.selected_path.as_deref(), Some("/preselected"));
1521 let reserialized = serde_json::to_value(&parsed).expect("encode");
1522 assert!(reserialized.get("label").is_none());
1524 assert!(reserialized.get("disabled").is_none());
1525 }
1526
1527 #[test]
1528 fn schema_for_rich_text_editor_props_generates() {
1529 assert_schema_nonempty_object::<RichTextEditorProps>("RichTextEditorProps");
1530 }
1531
1532 #[test]
1533 fn rich_text_editor_props_serde_roundtrip() {
1534 let json = serde_json::json!({
1535 "field": "body",
1536 "label": "Body"
1537 });
1538 let parsed: RichTextEditorProps = serde_json::from_value(json).expect("decode");
1539 assert_eq!(parsed.field, "body");
1540 assert_eq!(parsed.label, "Body");
1541 assert!(parsed.placeholder.is_none());
1542 assert!(parsed.default_value.is_none());
1543 assert!(parsed.data_path.is_none());
1544 assert!(parsed.error.is_none());
1545 let reserialized = serde_json::to_value(&parsed).expect("encode");
1546 assert!(reserialized.get("placeholder").is_none());
1548 assert!(reserialized.get("error").is_none());
1549 }
1550}
1551
1552#[cfg(test)]
1553mod strum_tests {
1554 use super::*;
1555
1556 #[test]
1560 fn variant_enums_strum_matches_serde_wire_format() {
1561 fn check<T: AsRef<str> + serde::Serialize>(variants: &[T], label: &str) {
1562 for v in variants {
1563 let json = serde_json::to_string(v).expect("serialize");
1564 let json_stripped = json.trim_matches('"');
1565 assert_eq!(
1566 v.as_ref(),
1567 json_stripped,
1568 "strum AsRefStr drifted from serde for {label} variant"
1569 );
1570 }
1571 }
1572 check(
1573 &[
1574 AlertVariant::Info,
1575 AlertVariant::Success,
1576 AlertVariant::Warning,
1577 AlertVariant::Error,
1578 ],
1579 "AlertVariant",
1580 );
1581 check(
1582 &[
1583 BadgeVariant::Default,
1584 BadgeVariant::Secondary,
1585 BadgeVariant::Destructive,
1586 BadgeVariant::Outline,
1587 ],
1588 "BadgeVariant",
1589 );
1590 check(
1591 &[
1592 ButtonVariant::Default,
1593 ButtonVariant::Secondary,
1594 ButtonVariant::Destructive,
1595 ButtonVariant::Outline,
1596 ButtonVariant::Ghost,
1597 ButtonVariant::Link,
1598 ],
1599 "ButtonVariant",
1600 );
1601 check(
1602 &[
1603 ToastVariant::Info,
1604 ToastVariant::Success,
1605 ToastVariant::Warning,
1606 ToastVariant::Error,
1607 ],
1608 "ToastVariant",
1609 );
1610 }
1611
1612 #[test]
1613 fn alert_variant_as_ref_str_matches_wire_format() {
1614 assert_eq!(AlertVariant::Success.as_ref(), "success");
1615 assert_eq!(AlertVariant::Warning.as_ref(), "warning");
1616 assert_eq!(AlertVariant::Info.as_ref(), "info");
1617 assert_eq!(AlertVariant::Error.as_ref(), "error");
1618 }
1619}
1620
1621#[cfg(test)]
1622mod card_variant_tests {
1623 use super::*;
1624
1625 #[test]
1626 fn card_variant_default_is_bordered() {
1627 assert_eq!(CardVariant::default(), CardVariant::Bordered);
1628 }
1629
1630 #[test]
1631 fn card_variant_serializes_snake_case() {
1632 assert_eq!(
1633 serde_json::to_value(CardVariant::Bordered).unwrap(),
1634 serde_json::json!("bordered")
1635 );
1636 assert_eq!(
1637 serde_json::to_value(CardVariant::Elevated).unwrap(),
1638 serde_json::json!("elevated")
1639 );
1640 }
1641
1642 #[test]
1643 fn card_variant_deserializes_snake_case() {
1644 assert_eq!(
1645 serde_json::from_value::<CardVariant>(serde_json::json!("bordered")).unwrap(),
1646 CardVariant::Bordered
1647 );
1648 assert_eq!(
1649 serde_json::from_value::<CardVariant>(serde_json::json!("elevated")).unwrap(),
1650 CardVariant::Elevated
1651 );
1652 }
1653
1654 #[test]
1655 fn card_props_without_variant_defaults_to_bordered() {
1656 let v = serde_json::json!({"title": "x"});
1657 let p: CardProps = serde_json::from_value(v).unwrap();
1658 assert_eq!(p.variant, CardVariant::Bordered);
1659 }
1660
1661 #[test]
1662 fn card_props_with_elevated_variant() {
1663 let v = serde_json::json!({"title": "x", "variant": "elevated"});
1664 let p: CardProps = serde_json::from_value(v).unwrap();
1665 assert_eq!(p.variant, CardVariant::Elevated);
1666 }
1667
1668 #[test]
1669 fn card_props_roundtrip_preserves_variant() {
1670 let p = CardProps {
1671 title: "x".into(),
1672 description: None,
1673 subtitle: None,
1674 badge: None,
1675 max_width: None,
1676 footer: vec![],
1677 variant: CardVariant::Elevated,
1678 };
1679 let j = serde_json::to_value(&p).unwrap();
1680 let back: CardProps = serde_json::from_value(j).unwrap();
1681 assert_eq!(back.variant, CardVariant::Elevated);
1682 }
1683}
1684
1685#[cfg(test)]
1686mod kanban_board_props_tests {
1687 use super::*;
1688
1689 #[test]
1690 fn kanban_board_props_serde_static_columns() {
1691 let v = serde_json::json!({
1692 "columns": [{"title": "To Do", "items": [], "id": "todo", "count": 0}]
1693 });
1694 let p: KanbanBoardProps = serde_json::from_value(v).unwrap();
1695 assert_eq!(p.columns.len(), 1);
1696 assert!(p.data_path.is_none());
1697 }
1698
1699 #[test]
1700 fn kanban_board_props_serde_data_path() {
1701 let v = serde_json::json!({"data_path": "/columns"});
1702 let p: KanbanBoardProps = serde_json::from_value(v).unwrap();
1703 assert!(p.columns.is_empty());
1704 assert_eq!(p.data_path.as_deref(), Some("/columns"));
1705 }
1706
1707 #[test]
1708 fn kanban_board_props_serde_neither() {
1709 let v = serde_json::json!({});
1710 let p: KanbanBoardProps = serde_json::from_value(v).unwrap();
1711 assert!(p.columns.is_empty());
1712 assert!(p.data_path.is_none());
1713 }
1714
1715 #[test]
1716 fn kanban_board_props_empty_columns_skipped_on_serialize() {
1717 let p = KanbanBoardProps {
1718 columns: vec![],
1719 data_path: Some("/x".into()),
1720 mobile_default_column: None,
1721 empty_label: None,
1722 };
1723 let j = serde_json::to_value(&p).unwrap();
1724 assert!(
1725 j.get("columns").is_none(),
1726 "empty columns must be skipped, got: {j}"
1727 );
1728 assert_eq!(j.get("data_path").and_then(|v| v.as_str()), Some("/x"));
1729 }
1730}
1731
1732#[cfg(test)]
1733mod page_header_actions_tests {
1734 use super::*;
1735
1736 #[test]
1737 fn page_header_actions_missing_field() {
1738 let v = serde_json::json!({"title": "X"});
1739 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
1740 assert!(p.actions.is_empty());
1741 }
1742
1743 #[test]
1744 fn page_header_actions_null() {
1745 let v = serde_json::json!({"title": "X", "actions": null});
1746 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
1747 assert!(p.actions.is_empty());
1748 }
1749
1750 #[test]
1751 fn page_header_actions_empty_string() {
1752 let v = serde_json::json!({"title": "X", "actions": ""});
1753 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
1754 assert!(p.actions.is_empty());
1755 }
1756
1757 #[test]
1758 fn page_header_actions_empty_array() {
1759 let v = serde_json::json!({"title": "X", "actions": []});
1760 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
1761 assert!(p.actions.is_empty());
1762 }
1763
1764 #[test]
1765 fn page_header_actions_non_empty_array() {
1766 let v = serde_json::json!({"title": "X", "actions": ["a", "b"]});
1767 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
1768 assert_eq!(p.actions, vec!["a".to_string(), "b".to_string()]);
1769 }
1770
1771 #[test]
1772 fn page_header_actions_non_empty_string_rejected() {
1773 let v = serde_json::json!({"title": "X", "actions": "not-empty"});
1774 let result: Result<PageHeaderProps, _> = serde_json::from_value(v);
1775 assert!(result.is_err(), "non-empty string must be rejected");
1776 }
1777
1778 #[test]
1779 fn page_header_actions_non_string_array_rejected() {
1780 let v = serde_json::json!({"title": "X", "actions": [1, 2, 3]});
1781 let result: Result<PageHeaderProps, _> = serde_json::from_value(v);
1782 assert!(result.is_err(), "array of non-strings must be rejected");
1783 }
1784}