1use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8
9use crate::action::Action;
10
11#[derive(
13 Debug,
14 Clone,
15 Copy,
16 Default,
17 PartialEq,
18 Eq,
19 Serialize,
20 Deserialize,
21 JsonSchema,
22 strum::AsRefStr,
23 strum::VariantArray,
24)]
25#[serde(rename_all = "snake_case")]
26#[strum(serialize_all = "snake_case")]
27pub enum Variant {
28 #[default]
29 Primary,
30 Secondary,
31 Outline,
32 Ghost,
33 Destructive,
34}
35
36#[derive(
39 Debug,
40 Clone,
41 Copy,
42 Default,
43 PartialEq,
44 Eq,
45 Serialize,
46 Deserialize,
47 JsonSchema,
48 strum::AsRefStr,
49 strum::VariantArray,
50)]
51#[serde(rename_all = "snake_case")]
52#[strum(serialize_all = "snake_case")]
53pub enum Tone {
54 #[default]
55 Neutral,
56 Success,
57 Warning,
58 Destructive,
59}
60
61#[derive(
63 Debug,
64 Clone,
65 Copy,
66 Default,
67 PartialEq,
68 Eq,
69 Serialize,
70 Deserialize,
71 JsonSchema,
72 strum::AsRefStr,
73 strum::VariantArray,
74)]
75#[serde(rename_all = "snake_case")]
76#[strum(serialize_all = "snake_case")]
77pub enum Size {
78 Sm,
79 #[default]
80 Md,
81 Lg,
82}
83
84#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
86#[serde(rename_all = "snake_case")]
87pub enum IconPosition {
88 #[default]
89 Left,
90 Right,
91}
92
93#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
95#[serde(rename_all = "snake_case")]
96pub enum SortDirection {
97 #[default]
98 Asc,
99 Desc,
100}
101
102#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
104#[serde(rename_all = "snake_case")]
105pub enum Orientation {
106 #[default]
107 Horizontal,
108 Vertical,
109}
110
111#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
113#[serde(rename_all = "snake_case")]
114pub enum InputType {
115 #[default]
116 Text,
117 Email,
118 Password,
119 Number,
120 Textarea,
121 Hidden,
122 Date,
123 Time,
124 Url,
125 Tel,
126 Search,
127 File,
128}
129
130#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
132#[serde(rename_all = "snake_case")]
133pub enum TextElement {
134 #[default]
135 P,
136 H1,
137 H2,
138 H3,
139 Span,
140 Div,
141 Section,
142}
143
144#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
149#[serde(rename_all = "snake_case")]
150pub enum ColumnFormat {
151 Date,
152 DateTime,
153 Currency,
154 Boolean,
155 Badge,
156 Image,
158 Icon,
163}
164
165#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
167#[serde(rename_all = "snake_case")]
168pub enum ColumnAlign {
169 #[default]
170 Left,
171 Center,
172 Right,
173}
174
175#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
177pub struct Column {
178 pub key: String,
179 pub label: String,
180 #[serde(default, skip_serializing_if = "Option::is_none")]
181 pub format: Option<ColumnFormat>,
182 #[serde(default, skip_serializing_if = "Option::is_none")]
185 pub align: Option<ColumnAlign>,
186}
187
188#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
190pub struct SelectOption {
191 pub value: String,
192 pub label: String,
193}
194
195#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
202#[serde(rename_all = "snake_case")]
203pub enum CardAppearance {
204 #[default]
205 Bordered,
206 Elevated,
207}
208
209#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
211pub struct CardProps {
212 pub title: String,
213 #[serde(default, skip_serializing_if = "Option::is_none")]
214 pub description: Option<String>,
215 #[serde(default, skip_serializing_if = "Option::is_none")]
219 pub subtitle: Option<String>,
220 #[serde(default, skip_serializing_if = "Option::is_none")]
224 pub badge: Option<String>,
225 #[serde(default, skip_serializing_if = "Option::is_none")]
226 pub max_width: Option<FormMaxWidth>,
227 #[serde(default, skip_serializing_if = "Vec::is_empty")]
229 pub footer: Vec<String>,
230 #[serde(default)]
231 pub appearance: CardAppearance,
232}
233
234#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
236pub struct TableProps {
237 pub columns: Vec<Column>,
238 pub data_path: String,
239 #[serde(default, skip_serializing_if = "Option::is_none")]
240 pub row_actions: Option<Vec<Action>>,
241 #[serde(default, skip_serializing_if = "Option::is_none")]
242 pub empty_message: Option<String>,
243 #[serde(default, skip_serializing_if = "Option::is_none")]
244 pub sortable: Option<bool>,
245 #[serde(default, skip_serializing_if = "Option::is_none")]
246 pub sort_column: Option<String>,
247 #[serde(default, skip_serializing_if = "Option::is_none")]
248 pub sort_direction: Option<SortDirection>,
249}
250
251#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
253#[serde(rename_all = "snake_case")]
254pub enum FormMaxWidth {
255 #[default]
256 Default,
257 Narrow,
258 Wide,
259}
260
261#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
263pub struct FormProps {
264 pub action: Action,
265 #[serde(default, skip_serializing_if = "Option::is_none")]
266 pub method: Option<crate::action::HttpMethod>,
267 #[serde(default, skip_serializing_if = "Option::is_none")]
271 pub guard: Option<String>,
272 #[serde(default, skip_serializing_if = "Option::is_none")]
274 pub max_width: Option<FormMaxWidth>,
275 #[serde(default, skip_serializing_if = "Option::is_none")]
279 pub id: Option<String>,
280 #[serde(default, skip_serializing_if = "Option::is_none")]
285 pub enctype: Option<String>,
286}
287
288#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
290#[serde(rename_all = "snake_case")]
291pub enum ButtonType {
292 #[default]
293 Button,
294 Submit,
295}
296
297#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
299pub struct ButtonProps {
300 pub label: String,
301 #[serde(default)]
302 pub variant: Variant,
303 #[serde(default)]
304 pub size: Size,
305 #[serde(default, skip_serializing_if = "Option::is_none")]
306 pub disabled: Option<bool>,
307 #[serde(default, skip_serializing_if = "Option::is_none")]
308 pub icon: Option<String>,
309 #[serde(default, skip_serializing_if = "Option::is_none")]
310 pub icon_position: Option<IconPosition>,
311 #[serde(default, skip_serializing_if = "Option::is_none")]
312 pub button_type: Option<ButtonType>,
313 #[serde(default, skip_serializing_if = "Option::is_none")]
317 pub form: Option<String>,
318}
319
320#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
322pub struct InputProps {
323 pub field: String,
325 pub label: String,
326 #[serde(default)]
327 pub input_type: InputType,
328 #[serde(default, skip_serializing_if = "Option::is_none")]
329 pub placeholder: Option<String>,
330 #[serde(default, skip_serializing_if = "Option::is_none")]
331 pub required: Option<bool>,
332 #[serde(default, skip_serializing_if = "Option::is_none")]
333 pub disabled: Option<bool>,
334 #[serde(default, skip_serializing_if = "Option::is_none")]
335 pub error: Option<String>,
336 #[serde(default, skip_serializing_if = "Option::is_none")]
337 pub description: Option<String>,
338 #[serde(default, skip_serializing_if = "Option::is_none")]
339 pub default_value: Option<String>,
340 #[serde(default, skip_serializing_if = "Option::is_none")]
342 pub data_path: Option<String>,
343 #[serde(default, skip_serializing_if = "Option::is_none")]
345 pub step: Option<String>,
346 #[serde(default, skip_serializing_if = "Option::is_none")]
350 pub list: Option<String>,
351 #[serde(default, skip_serializing_if = "Option::is_none")]
356 pub accept: Option<String>,
357}
358
359#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
370pub struct RichTextEditorProps {
371 pub field: String,
372 pub label: String,
373 #[serde(default, skip_serializing_if = "Option::is_none")]
374 pub placeholder: Option<String>,
375 #[serde(default, skip_serializing_if = "Option::is_none")]
376 pub default_value: Option<String>,
377 #[serde(default, skip_serializing_if = "Option::is_none")]
378 pub data_path: Option<String>,
379 #[serde(default, skip_serializing_if = "Option::is_none")]
380 pub error: Option<String>,
381}
382
383#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
385pub struct SelectProps {
386 pub field: String,
388 pub label: String,
389 pub options: Vec<SelectOption>,
390 #[serde(default, skip_serializing_if = "Option::is_none")]
391 pub placeholder: Option<String>,
392 #[serde(default, skip_serializing_if = "Option::is_none")]
393 pub required: Option<bool>,
394 #[serde(default, skip_serializing_if = "Option::is_none")]
395 pub disabled: Option<bool>,
396 #[serde(default, skip_serializing_if = "Option::is_none")]
397 pub error: Option<String>,
398 #[serde(default, skip_serializing_if = "Option::is_none")]
399 pub description: Option<String>,
400 #[serde(default, skip_serializing_if = "Option::is_none")]
401 pub default_value: Option<String>,
402 #[serde(default, skip_serializing_if = "Option::is_none")]
404 pub data_path: Option<String>,
405}
406
407#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
409pub struct AlertProps {
410 pub message: String,
411 #[serde(default)]
412 pub tone: Tone,
413 #[serde(default, skip_serializing_if = "Option::is_none")]
414 pub title: Option<String>,
415}
416
417#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
419pub struct BadgeProps {
420 pub label: String,
421 #[serde(default)]
422 pub tone: Tone,
423}
424
425#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
427pub struct ModalProps {
428 pub id: String,
429 pub title: String,
430 #[serde(default, skip_serializing_if = "Option::is_none")]
431 pub description: Option<String>,
432 #[serde(default, skip_serializing_if = "Option::is_none")]
433 pub trigger_label: Option<String>,
434 #[serde(default, skip_serializing_if = "Vec::is_empty")]
436 pub footer: Vec<String>,
437}
438
439#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
441pub struct TextProps {
442 pub content: String,
443 #[serde(default)]
444 pub element: TextElement,
445}
446
447#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
449pub struct CheckboxProps {
450 pub field: String,
452 #[serde(default, skip_serializing_if = "Option::is_none")]
455 pub value: Option<String>,
456 pub label: String,
457 #[serde(default, skip_serializing_if = "Option::is_none")]
458 pub description: Option<String>,
459 #[serde(default, skip_serializing_if = "Option::is_none")]
460 pub checked: Option<bool>,
461 #[serde(default, skip_serializing_if = "Option::is_none")]
463 pub data_path: Option<String>,
464 #[serde(default, skip_serializing_if = "Option::is_none")]
465 pub required: Option<bool>,
466 #[serde(default, skip_serializing_if = "Option::is_none")]
467 pub disabled: Option<bool>,
468 #[serde(default, skip_serializing_if = "Option::is_none")]
469 pub error: Option<String>,
470}
471
472#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
478pub struct CheckboxListProps {
479 pub field: String,
481 #[serde(default, skip_serializing_if = "Vec::is_empty")]
484 pub options: Vec<SelectOption>,
485 #[serde(default, skip_serializing_if = "Option::is_none")]
487 pub options_path: Option<String>,
488 #[serde(default, skip_serializing_if = "Option::is_none")]
490 pub selected_path: Option<String>,
491 #[serde(default, skip_serializing_if = "Option::is_none")]
492 pub label: Option<String>,
493 #[serde(default, skip_serializing_if = "Option::is_none")]
494 pub description: Option<String>,
495 #[serde(default, skip_serializing_if = "Option::is_none")]
496 pub disabled: Option<bool>,
497 #[serde(default, skip_serializing_if = "Option::is_none")]
498 pub error: Option<String>,
499}
500
501#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
503pub struct SwitchProps {
504 pub field: String,
506 pub label: String,
507 #[serde(default, skip_serializing_if = "Option::is_none")]
508 pub description: Option<String>,
509 #[serde(default, skip_serializing_if = "Option::is_none")]
510 pub checked: Option<bool>,
511 #[serde(default, skip_serializing_if = "Option::is_none")]
513 pub data_path: Option<String>,
514 #[serde(default, skip_serializing_if = "Option::is_none")]
515 pub required: Option<bool>,
516 #[serde(default, skip_serializing_if = "Option::is_none")]
517 pub disabled: Option<bool>,
518 #[serde(default, skip_serializing_if = "Option::is_none")]
519 pub error: Option<String>,
520 #[serde(default, skip_serializing_if = "Option::is_none")]
523 pub action: Option<Action>,
524 #[serde(default, skip_serializing_if = "Option::is_none")]
527 pub compact: Option<bool>,
528}
529
530#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
532pub struct SeparatorProps {
533 #[serde(default, skip_serializing_if = "Option::is_none")]
534 pub orientation: Option<Orientation>,
535}
536
537#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
539pub struct DescriptionItem {
540 pub label: String,
541 pub value: String,
542 #[serde(default, skip_serializing_if = "Option::is_none")]
543 pub format: Option<ColumnFormat>,
544}
545
546#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
548pub struct DescriptionListProps {
549 #[serde(default, skip_serializing_if = "Vec::is_empty")]
550 pub items: Vec<DescriptionItem>,
551 #[serde(default, skip_serializing_if = "Option::is_none")]
552 pub columns: Option<u8>,
553 #[serde(default, skip_serializing_if = "Option::is_none")]
557 pub data_path: Option<String>,
558}
559
560#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
562pub struct Tab {
563 pub value: String,
564 pub label: String,
565 #[serde(default, skip_serializing_if = "Vec::is_empty")]
567 pub children: Vec<String>,
568}
569
570#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
572pub struct TabsProps {
573 pub default_tab: String,
574 pub tabs: Vec<Tab>,
575}
576
577#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
579pub struct BreadcrumbItem {
580 pub label: String,
581 #[serde(default, skip_serializing_if = "Option::is_none")]
582 pub url: Option<String>,
583}
584
585#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
587pub struct BreadcrumbProps {
588 pub items: Vec<BreadcrumbItem>,
589}
590
591#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
593pub struct PaginationProps {
594 pub current_page: u32,
595 pub per_page: u32,
596 pub total: u32,
597 #[serde(default, skip_serializing_if = "Option::is_none")]
598 pub base_url: Option<String>,
599}
600
601#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
603pub struct ProgressProps {
604 pub value: u8,
606 #[serde(default, skip_serializing_if = "Option::is_none")]
607 pub max: Option<u8>,
608 #[serde(default, skip_serializing_if = "Option::is_none")]
609 pub label: Option<String>,
610}
611
612#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
614pub struct ImageProps {
615 #[serde(default)]
616 pub src: String,
617 pub alt: String,
618 #[serde(default, skip_serializing_if = "Option::is_none")]
619 pub aspect_ratio: Option<String>,
620 #[serde(default, skip_serializing_if = "Option::is_none")]
625 pub placeholder_label: Option<String>,
626 #[serde(default, skip_serializing_if = "Option::is_none")]
634 pub inline_svg: Option<String>,
635 #[serde(default, skip_serializing_if = "Option::is_none")]
639 pub data_path: Option<String>,
640}
641
642impl ImageProps {
643 pub fn inline_svg(svg: impl Into<String>, alt: impl Into<String>) -> Self {
649 Self {
650 src: String::new(),
651 alt: alt.into(),
652 aspect_ratio: None,
653 placeholder_label: None,
654 inline_svg: Some(svg.into()),
655 data_path: None,
656 }
657 }
658}
659
660#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
662pub struct AvatarProps {
663 #[serde(default, skip_serializing_if = "Option::is_none")]
664 pub src: Option<String>,
665 pub alt: String,
666 #[serde(default, skip_serializing_if = "Option::is_none")]
667 pub fallback: Option<String>,
668 #[serde(default, skip_serializing_if = "Option::is_none")]
669 pub size: Option<Size>,
670}
671
672#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
674pub struct SkeletonProps {
675 #[serde(default, skip_serializing_if = "Option::is_none")]
676 pub width: Option<String>,
677 #[serde(default, skip_serializing_if = "Option::is_none")]
678 pub height: Option<String>,
679 #[serde(default, skip_serializing_if = "Option::is_none")]
680 pub rounded: Option<bool>,
681}
682
683#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
700pub struct RawHtmlProps {
701 #[serde(default)]
703 pub html: String,
704}
705
706#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
712pub struct StreamTextProps {
713 #[serde(default)]
716 pub sse_url: String,
717 #[serde(default, skip_serializing_if = "Option::is_none")]
719 pub placeholder: Option<String>,
720 #[serde(default, skip_serializing_if = "Option::is_none")]
722 pub loading_text: Option<String>,
723}
724
725#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
727pub struct ChecklistItem {
728 pub label: String,
729 #[serde(default)]
730 pub checked: bool,
731 #[serde(default, skip_serializing_if = "Option::is_none")]
732 pub href: Option<String>,
733}
734
735#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
737pub struct NotificationItem {
738 #[serde(default, skip_serializing_if = "Option::is_none")]
739 pub icon: Option<String>,
740 pub text: String,
741 #[serde(default, skip_serializing_if = "Option::is_none")]
742 pub timestamp: Option<String>,
743 #[serde(default)]
744 pub read: bool,
745 #[serde(default, skip_serializing_if = "Option::is_none")]
746 pub action_url: Option<String>,
747}
748
749#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
751pub struct SidebarNavItem {
752 pub label: String,
753 pub href: String,
754 #[serde(default, skip_serializing_if = "Option::is_none")]
755 pub icon: Option<String>,
756 #[serde(default)]
757 pub active: bool,
758 #[serde(default, skip_serializing_if = "Option::is_none")]
761 pub disabled: Option<bool>,
762}
763
764#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
766pub struct SidebarGroup {
767 pub label: String,
768 #[serde(default)]
769 pub collapsed: bool,
770 pub items: Vec<SidebarNavItem>,
771}
772
773#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
775pub struct StatCardProps {
776 pub label: String,
777 pub value: String,
778 #[serde(default)]
781 pub tone: Tone,
782 #[serde(default, skip_serializing_if = "Option::is_none")]
783 pub icon: Option<String>,
784 #[serde(default, skip_serializing_if = "Option::is_none")]
785 pub subtitle: Option<String>,
786 #[serde(default, skip_serializing_if = "Option::is_none")]
788 pub sse_target: Option<String>,
789 #[serde(default, skip_serializing_if = "Option::is_none")]
794 pub value_path: Option<String>,
795}
796
797#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
799pub struct ChecklistProps {
800 pub title: String,
801 pub items: Vec<ChecklistItem>,
802 #[serde(default = "default_true")]
803 pub dismissible: bool,
804 #[serde(default, skip_serializing_if = "Option::is_none")]
805 pub dismiss_label: Option<String>,
806 #[serde(default, skip_serializing_if = "Option::is_none")]
808 pub data_key: Option<String>,
809}
810
811fn default_true() -> bool {
812 true
813}
814
815#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
820pub struct ToastProps {
821 pub message: String,
822 #[serde(default)]
823 pub tone: Tone,
824 #[serde(default, skip_serializing_if = "Option::is_none")]
827 pub timeout: Option<u32>,
828 #[serde(default = "default_true")]
831 pub dismissible: bool,
832}
833
834#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
836pub struct NotificationDropdownProps {
837 pub notifications: Vec<NotificationItem>,
838 #[serde(default, skip_serializing_if = "Option::is_none")]
839 pub empty_text: Option<String>,
840}
841
842#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
844pub struct SidebarProps {
845 #[serde(default, skip_serializing_if = "Vec::is_empty")]
846 pub fixed_top: Vec<SidebarNavItem>,
847 #[serde(default, skip_serializing_if = "Vec::is_empty")]
848 pub groups: Vec<SidebarGroup>,
849 #[serde(default, skip_serializing_if = "Vec::is_empty")]
850 pub fixed_bottom: Vec<SidebarNavItem>,
851}
852
853#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
855pub struct HeaderProps {
856 pub business_name: String,
857 #[serde(default, skip_serializing_if = "Option::is_none")]
859 pub notification_count: Option<u32>,
860 #[serde(default, skip_serializing_if = "Option::is_none")]
861 pub user_name: Option<String>,
862 #[serde(default, skip_serializing_if = "Option::is_none")]
863 pub user_avatar: Option<String>,
864 #[serde(default, skip_serializing_if = "Option::is_none")]
865 pub logout_url: Option<String>,
866}
867
868#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
870#[serde(rename_all = "snake_case")]
871pub enum GapSize {
872 None,
873 Sm,
874 #[default]
875 Md,
876 Lg,
877 Xl,
878}
879
880#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
882pub struct GridProps {
883 #[serde(default = "default_grid_columns")]
885 pub columns: u8,
886 #[serde(default, skip_serializing_if = "Option::is_none")]
888 pub md_columns: Option<u8>,
889 #[serde(default, skip_serializing_if = "Option::is_none")]
891 pub lg_columns: Option<u8>,
892 #[serde(default)]
894 pub gap: GapSize,
895 #[serde(default, skip_serializing_if = "Option::is_none")]
898 pub scrollable: Option<bool>,
899 #[serde(default, skip_serializing_if = "Vec::is_empty")]
905 pub spans: Vec<u8>,
906 #[serde(default, skip_serializing_if = "Option::is_none")]
913 pub fill: Option<bool>,
914}
915
916fn default_grid_columns() -> u8 {
917 2
918}
919
920#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
922pub struct CollapsibleProps {
923 pub title: String,
924 #[serde(default)]
925 pub expanded: bool,
926}
927
928#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
930pub struct EmptyStateProps {
931 pub title: String,
932 #[serde(default, skip_serializing_if = "Option::is_none")]
933 pub description: Option<String>,
934 #[serde(default, skip_serializing_if = "Option::is_none")]
935 pub action: Option<Action>,
936 #[serde(default, skip_serializing_if = "Option::is_none")]
937 pub action_label: Option<String>,
938}
939
940#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
942#[serde(rename_all = "snake_case")]
943pub enum FormSectionLayout {
944 #[default]
945 Stacked,
946 TwoColumn,
947}
948
949#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
951pub struct FormSectionProps {
952 pub title: String,
953 #[serde(default, skip_serializing_if = "Option::is_none")]
954 pub description: Option<String>,
955 #[serde(default, skip_serializing_if = "Option::is_none")]
957 pub layout: Option<FormSectionLayout>,
958}
959
960#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
962pub struct PageHeaderProps {
963 pub title: String,
964 #[serde(default, skip_serializing_if = "Vec::is_empty")]
965 pub breadcrumb: Vec<BreadcrumbItem>,
966 #[serde(
968 default,
969 deserialize_with = "deserialize_actions_lax",
970 skip_serializing_if = "Vec::is_empty"
971 )]
972 pub actions: Vec<String>,
973}
974
975#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Default)]
977pub struct ButtonGroupProps {
978 #[serde(default)]
980 pub gap: GapSize,
981}
982
983#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
994pub struct ActionItem {
995 pub label: String,
996 pub action: Action,
997 #[serde(default)]
1000 pub destructive: bool,
1001 #[serde(default, skip_serializing_if = "Option::is_none")]
1002 pub variant: Option<Variant>,
1003 #[serde(default, skip_serializing_if = "Option::is_none")]
1004 pub icon: Option<String>,
1005 #[serde(default, skip_serializing_if = "Option::is_none")]
1009 pub visible_if: Option<String>,
1010}
1011
1012#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1023pub struct ActionGroupProps {
1024 pub items: Vec<ActionItem>,
1025 pub menu_id: String,
1028 #[serde(default, skip_serializing_if = "Option::is_none")]
1030 pub max_inline: Option<u8>,
1031 #[serde(default, skip_serializing_if = "Option::is_none")]
1033 pub overflow_label: Option<String>,
1034 #[serde(default, skip_serializing_if = "Option::is_none")]
1036 pub row_key: Option<String>,
1037}
1038
1039#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Default)]
1055pub struct SegmentedControlProps {
1056 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1058 pub items: Vec<SegmentedItem>,
1059 #[serde(default, skip_serializing_if = "Option::is_none")]
1062 pub data_path: Option<String>,
1063 #[serde(default)]
1065 pub size: Size,
1066 #[serde(default, skip_serializing_if = "Option::is_none")]
1069 pub aria_label: Option<String>,
1070}
1071
1072#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Default)]
1074pub struct SegmentedItem {
1075 pub label: String,
1077 pub href: String,
1079 #[serde(default)]
1081 pub active: bool,
1082 #[serde(default, skip_serializing_if = "Option::is_none")]
1085 pub aria_label: Option<String>,
1086}
1087
1088#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Default)]
1104pub struct SidebarLayoutProps {
1105 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1107 pub items: Vec<SidebarLayoutItem>,
1108 #[serde(default, skip_serializing_if = "Option::is_none")]
1110 pub data_path: Option<String>,
1111 pub active: String,
1114 #[serde(default, skip_serializing_if = "Option::is_none")]
1116 pub aria_label: Option<String>,
1117}
1118
1119#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Default)]
1121pub struct SidebarLayoutItem {
1122 pub slug: String,
1125 pub label: String,
1127 pub url: String,
1130}
1131
1132#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1141pub struct DetailPageProps {
1142 pub title: String,
1143 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1144 pub breadcrumb: Vec<BreadcrumbItem>,
1145 #[serde(
1147 default,
1148 deserialize_with = "deserialize_actions_lax",
1149 skip_serializing_if = "Vec::is_empty"
1150 )]
1151 pub actions: Vec<String>,
1152 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1155 pub info: Vec<String>,
1156}
1157
1158#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1160pub struct DropdownMenuAction {
1161 pub label: String,
1162 pub action: Action,
1163 #[serde(default)]
1164 pub destructive: bool,
1165 #[serde(default, skip_serializing_if = "Option::is_none")]
1172 pub visible_if: Option<String>,
1173}
1174
1175#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1178pub struct DataTableProps {
1179 pub columns: Vec<Column>,
1180 pub data_path: String,
1181 #[serde(default, skip_serializing_if = "Option::is_none")]
1182 pub row_actions: Option<Vec<DropdownMenuAction>>,
1183 #[serde(default, skip_serializing_if = "Option::is_none")]
1184 pub empty_message: Option<String>,
1185 #[serde(default, skip_serializing_if = "Option::is_none")]
1186 pub row_key: Option<String>,
1187 #[serde(default, skip_serializing_if = "Option::is_none")]
1189 pub row_href: Option<String>,
1190}
1191
1192#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1196pub struct MediaCardGridProps {
1197 pub data_path: String,
1198 pub title_key: String,
1200 #[serde(default, skip_serializing_if = "Option::is_none")]
1202 pub description_key: Option<String>,
1203 #[serde(default, skip_serializing_if = "Option::is_none")]
1205 pub image_key: Option<String>,
1206 #[serde(default, skip_serializing_if = "Option::is_none")]
1208 pub image_href_key: Option<String>,
1209 #[serde(default, skip_serializing_if = "Option::is_none")]
1211 pub image_aspect_ratio: Option<String>,
1212 #[serde(default, skip_serializing_if = "Option::is_none")]
1215 pub image_position: Option<String>,
1216 #[serde(default, skip_serializing_if = "Option::is_none")]
1218 pub badge_key: Option<String>,
1219 #[serde(default, skip_serializing_if = "Option::is_none")]
1221 pub badge_tone_key: Option<String>,
1222 #[serde(default, skip_serializing_if = "Option::is_none")]
1224 pub row_key: Option<String>,
1225 #[serde(default, skip_serializing_if = "Option::is_none")]
1226 pub row_actions: Option<Vec<DropdownMenuAction>>,
1227 #[serde(default, skip_serializing_if = "Option::is_none")]
1228 pub empty_message: Option<String>,
1229 #[serde(default, skip_serializing_if = "Option::is_none")]
1231 pub columns: Option<u8>,
1232}
1233
1234#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1242pub struct KanbanColumnProps {
1243 pub id: String,
1244 pub title: String,
1245 #[serde(default)]
1246 pub count: u32,
1247 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1249 pub children: Vec<String>,
1250}
1251
1252#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1265pub struct KanbanBoardProps {
1266 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1268 pub columns: Vec<KanbanColumnProps>,
1269 #[serde(default, skip_serializing_if = "Option::is_none")]
1272 pub items_path: Option<String>,
1273 #[serde(default, skip_serializing_if = "Option::is_none")]
1275 pub group_by: Option<String>,
1276 #[serde(default, skip_serializing_if = "Option::is_none")]
1278 pub card_title_key: Option<String>,
1279 #[serde(default, skip_serializing_if = "Option::is_none")]
1281 pub card_description_key: Option<String>,
1282 #[serde(default, skip_serializing_if = "Option::is_none")]
1285 pub row_actions: Option<Vec<DropdownMenuAction>>,
1286 #[serde(default, skip_serializing_if = "Option::is_none")]
1289 pub row_key: Option<String>,
1290 #[serde(default, skip_serializing_if = "Option::is_none")]
1291 pub mobile_default_column: Option<String>,
1292 #[serde(default, skip_serializing_if = "Option::is_none")]
1296 pub empty_label: Option<String>,
1297}
1298
1299#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1304pub struct CalendarCellProps {
1305 pub day: u8,
1306 #[serde(default)]
1307 pub is_today: bool,
1308 #[serde(default)]
1309 pub is_current_month: bool,
1310 #[serde(default)]
1311 pub event_count: u32,
1312 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1315 pub dot_colors: Vec<String>,
1316 #[serde(default)]
1320 pub closed: bool,
1321}
1322
1323#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1328pub struct ActionCardProps {
1329 pub title: String,
1330 pub description: String,
1331 #[serde(default, skip_serializing_if = "Option::is_none")]
1332 pub icon: Option<String>,
1333 #[serde(default)]
1334 pub tone: Tone,
1335 #[serde(default, skip_serializing_if = "Option::is_none")]
1337 pub href: Option<String>,
1338}
1339
1340#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1345pub struct ProductTileProps {
1346 pub product_id: String,
1347 pub name: String,
1348 pub price: String,
1349 pub field: String,
1350 #[serde(default, skip_serializing_if = "Option::is_none")]
1351 pub default_quantity: Option<u32>,
1352}
1353
1354fn deserialize_actions_lax<'de, D: serde::Deserializer<'de>>(
1360 d: D,
1361) -> Result<Vec<String>, D::Error> {
1362 use serde::de::Error;
1363 let v = serde_json::Value::deserialize(d)?;
1364 match v {
1365 serde_json::Value::Null => Ok(Vec::new()),
1366 serde_json::Value::String(s) if s.is_empty() => Ok(Vec::new()),
1367 serde_json::Value::Array(arr) => arr
1368 .into_iter()
1369 .map(|item| {
1370 item.as_str()
1371 .map(String::from)
1372 .ok_or_else(|| D::Error::custom("PageHeader.actions: expected string in array"))
1373 })
1374 .collect(),
1375 other => Err(D::Error::custom(format!(
1376 "PageHeader.actions: expected null, empty string, or array of strings; got {other:?}"
1377 ))),
1378 }
1379}
1380
1381#[cfg(test)]
1382mod schema_smoke_tests {
1383 use super::*;
1394
1395 fn assert_schema_nonempty_object<T: schemars::JsonSchema>(type_label: &str) {
1396 let schema = schemars::schema_for!(T);
1397 let value = serde_json::to_value(&schema).expect("schema serializes to JSON");
1398 assert!(
1399 value.is_object(),
1400 "{type_label}: schema must be a JSON object"
1401 );
1402 let props = value
1403 .get("properties")
1404 .and_then(|p| p.as_object())
1405 .map(|o| !o.is_empty())
1406 .unwrap_or(false);
1407 assert!(
1408 props,
1409 "{type_label}: schema must have a non-empty `properties` field"
1410 );
1411 }
1412
1413 #[test]
1414 fn schema_for_card_props_generates() {
1415 assert_schema_nonempty_object::<CardProps>("CardProps");
1416 }
1417
1418 #[test]
1419 fn schema_for_table_props_generates() {
1420 assert_schema_nonempty_object::<TableProps>("TableProps");
1421 }
1422
1423 #[test]
1424 fn schema_for_form_props_generates() {
1425 assert_schema_nonempty_object::<FormProps>("FormProps");
1426 }
1427
1428 #[test]
1429 fn schema_for_button_props_generates() {
1430 assert_schema_nonempty_object::<ButtonProps>("ButtonProps");
1431 }
1432
1433 #[test]
1434 fn schema_for_input_props_generates() {
1435 assert_schema_nonempty_object::<InputProps>("InputProps");
1436 }
1437
1438 #[test]
1439 fn schema_for_select_props_generates() {
1440 assert_schema_nonempty_object::<SelectProps>("SelectProps");
1441 }
1442
1443 #[test]
1444 fn schema_for_alert_props_generates() {
1445 assert_schema_nonempty_object::<AlertProps>("AlertProps");
1446 }
1447
1448 #[test]
1449 fn schema_for_badge_props_generates() {
1450 assert_schema_nonempty_object::<BadgeProps>("BadgeProps");
1451 }
1452
1453 #[test]
1454 fn schema_for_modal_props_generates() {
1455 assert_schema_nonempty_object::<ModalProps>("ModalProps");
1456 }
1457
1458 #[test]
1459 fn schema_for_text_props_generates() {
1460 assert_schema_nonempty_object::<TextProps>("TextProps");
1461 }
1462
1463 #[test]
1464 fn schema_for_checkbox_props_generates() {
1465 assert_schema_nonempty_object::<CheckboxProps>("CheckboxProps");
1466 }
1467
1468 #[test]
1469 fn schema_for_switch_props_generates() {
1470 assert_schema_nonempty_object::<SwitchProps>("SwitchProps");
1471 }
1472
1473 #[test]
1474 fn schema_for_separator_props_generates() {
1475 assert_schema_nonempty_object::<SeparatorProps>("SeparatorProps");
1476 }
1477
1478 #[test]
1479 fn schema_for_description_list_props_generates() {
1480 assert_schema_nonempty_object::<DescriptionListProps>("DescriptionListProps");
1481 }
1482
1483 #[test]
1484 fn schema_for_tab_generates() {
1485 assert_schema_nonempty_object::<Tab>("Tab");
1486 }
1487
1488 #[test]
1489 fn schema_for_tabs_props_generates() {
1490 assert_schema_nonempty_object::<TabsProps>("TabsProps");
1491 }
1492
1493 #[test]
1494 fn schema_for_breadcrumb_props_generates() {
1495 assert_schema_nonempty_object::<BreadcrumbProps>("BreadcrumbProps");
1496 }
1497
1498 #[test]
1499 fn schema_for_pagination_props_generates() {
1500 assert_schema_nonempty_object::<PaginationProps>("PaginationProps");
1501 }
1502
1503 #[test]
1504 fn schema_for_progress_props_generates() {
1505 assert_schema_nonempty_object::<ProgressProps>("ProgressProps");
1506 }
1507
1508 #[test]
1509 fn schema_for_image_props_generates() {
1510 assert_schema_nonempty_object::<ImageProps>("ImageProps");
1511 }
1512
1513 #[test]
1514 fn image_inline_svg_factory_roundtrips_via_serde() {
1515 let p = ImageProps::inline_svg("<svg/>", "alt");
1516 let json = serde_json::to_value(&p).expect("serialization must not fail");
1517 let parsed: ImageProps =
1518 serde_json::from_value(json).expect("deserialization must not fail");
1519 assert_eq!(parsed.inline_svg, Some("<svg/>".to_string()));
1520 assert_eq!(parsed.alt, "alt");
1521 assert_eq!(parsed.src, "");
1522 }
1523
1524 #[test]
1525 fn schema_for_avatar_props_generates() {
1526 assert_schema_nonempty_object::<AvatarProps>("AvatarProps");
1527 }
1528
1529 #[test]
1530 fn schema_for_skeleton_props_generates() {
1531 assert_schema_nonempty_object::<SkeletonProps>("SkeletonProps");
1532 }
1533
1534 #[test]
1535 fn schema_for_stat_card_props_generates() {
1536 assert_schema_nonempty_object::<StatCardProps>("StatCardProps");
1537 }
1538
1539 #[test]
1540 fn schema_for_checklist_props_generates() {
1541 assert_schema_nonempty_object::<ChecklistProps>("ChecklistProps");
1542 }
1543
1544 #[test]
1545 fn schema_for_toast_props_generates() {
1546 assert_schema_nonempty_object::<ToastProps>("ToastProps");
1547 }
1548
1549 #[test]
1550 fn schema_for_notification_dropdown_props_generates() {
1551 assert_schema_nonempty_object::<NotificationDropdownProps>("NotificationDropdownProps");
1552 }
1553
1554 #[test]
1555 fn schema_for_sidebar_props_generates() {
1556 assert_schema_nonempty_object::<SidebarProps>("SidebarProps");
1557 }
1558
1559 #[test]
1560 fn schema_for_header_props_generates() {
1561 assert_schema_nonempty_object::<HeaderProps>("HeaderProps");
1562 }
1563
1564 #[test]
1565 fn schema_for_grid_props_generates() {
1566 assert_schema_nonempty_object::<GridProps>("GridProps");
1567 }
1568
1569 #[test]
1570 fn schema_for_collapsible_props_generates() {
1571 assert_schema_nonempty_object::<CollapsibleProps>("CollapsibleProps");
1572 }
1573
1574 #[test]
1575 fn schema_for_empty_state_props_generates() {
1576 assert_schema_nonempty_object::<EmptyStateProps>("EmptyStateProps");
1577 }
1578
1579 #[test]
1580 fn schema_for_form_section_props_generates() {
1581 assert_schema_nonempty_object::<FormSectionProps>("FormSectionProps");
1582 }
1583
1584 #[test]
1585 fn schema_for_page_header_props_generates() {
1586 assert_schema_nonempty_object::<PageHeaderProps>("PageHeaderProps");
1587 }
1588
1589 #[test]
1590 fn schema_for_button_group_props_generates() {
1591 assert_schema_nonempty_object::<ButtonGroupProps>("ButtonGroupProps");
1592 }
1593
1594 #[test]
1595 fn schema_for_action_item_generates() {
1596 assert_schema_nonempty_object::<ActionItem>("ActionItem");
1597 }
1598
1599 #[test]
1600 fn schema_for_action_group_props_generates() {
1601 assert_schema_nonempty_object::<ActionGroupProps>("ActionGroupProps");
1602 }
1603
1604 #[test]
1605 fn schema_for_dropdown_menu_action_generates() {
1606 assert_schema_nonempty_object::<DropdownMenuAction>("DropdownMenuAction");
1607 }
1608
1609 #[test]
1610 fn schema_for_data_table_props_generates() {
1611 assert_schema_nonempty_object::<DataTableProps>("DataTableProps");
1612 }
1613
1614 #[test]
1615 fn schema_for_kanban_column_props_generates() {
1616 assert_schema_nonempty_object::<KanbanColumnProps>("KanbanColumnProps");
1617 }
1618
1619 #[test]
1620 fn schema_for_kanban_board_props_generates() {
1621 assert_schema_nonempty_object::<KanbanBoardProps>("KanbanBoardProps");
1622 }
1623
1624 #[test]
1625 fn schema_for_calendar_cell_props_generates() {
1626 assert_schema_nonempty_object::<CalendarCellProps>("CalendarCellProps");
1627 }
1628
1629 #[test]
1630 fn schema_for_action_card_props_generates() {
1631 assert_schema_nonempty_object::<ActionCardProps>("ActionCardProps");
1632 }
1633
1634 #[test]
1635 fn schema_for_product_tile_props_generates() {
1636 assert_schema_nonempty_object::<ProductTileProps>("ProductTileProps");
1637 }
1638
1639 #[test]
1640 fn card_props_round_trips_footer() {
1641 let original = CardProps {
1642 title: "Hero".to_string(),
1643 description: None,
1644 subtitle: None,
1645 badge: None,
1646 max_width: None,
1647 footer: vec!["btn1".to_string(), "btn2".to_string()],
1648 appearance: CardAppearance::Bordered,
1649 };
1650 let json = serde_json::to_string(&original).unwrap();
1651 let parsed: CardProps = serde_json::from_str(&json).unwrap();
1652 assert_eq!(original.footer, parsed.footer);
1653 }
1654
1655 #[test]
1656 fn tab_round_trips_children() {
1657 let original = Tab {
1658 value: "overview".to_string(),
1659 label: "Overview".to_string(),
1660 children: vec!["panel1".to_string()],
1661 };
1662 let json = serde_json::to_string(&original).unwrap();
1663 let parsed: Tab = serde_json::from_str(&json).unwrap();
1664 assert_eq!(original.children, parsed.children);
1665 }
1666
1667 #[test]
1668 fn card_props_omits_empty_footer_in_json() {
1669 let card = CardProps {
1670 title: "Card".to_string(),
1671 description: None,
1672 subtitle: None,
1673 badge: None,
1674 max_width: None,
1675 footer: Vec::new(),
1676 appearance: CardAppearance::Bordered,
1677 };
1678 let json = serde_json::to_string(&card).unwrap();
1679 assert!(
1680 !json.contains("\"footer\""),
1681 "empty footer must be skipped, got: {json}"
1682 );
1683 }
1684
1685 #[test]
1686 fn card_props_round_trips_badge() {
1687 let original = CardProps {
1688 title: "Hero".to_string(),
1689 description: None,
1690 subtitle: None,
1691 badge: Some("Scade tra 9m".to_string()),
1692 max_width: None,
1693 footer: Vec::new(),
1694 appearance: CardAppearance::Bordered,
1695 };
1696 let json = serde_json::to_string(&original).unwrap();
1697 let parsed: CardProps = serde_json::from_str(&json).unwrap();
1698 assert_eq!(original.badge, parsed.badge);
1699 }
1700
1701 #[test]
1702 fn card_props_omits_empty_badge_in_json() {
1703 let card = CardProps {
1704 title: "Card".to_string(),
1705 description: None,
1706 subtitle: None,
1707 badge: None,
1708 max_width: None,
1709 footer: Vec::new(),
1710 appearance: CardAppearance::Bordered,
1711 };
1712 let json = serde_json::to_string(&card).unwrap();
1713 assert!(
1714 !json.contains("\"badge\""),
1715 "empty badge must be skipped, got: {json}"
1716 );
1717 }
1718
1719 #[test]
1720 fn card_props_round_trips_subtitle() {
1721 let original = CardProps {
1722 title: "Hero".to_string(),
1723 description: None,
1724 subtitle: Some("Marco Rossi".to_string()),
1725 badge: None,
1726 max_width: None,
1727 footer: Vec::new(),
1728 appearance: CardAppearance::Bordered,
1729 };
1730 let json = serde_json::to_string(&original).unwrap();
1731 let parsed: CardProps = serde_json::from_str(&json).unwrap();
1732 assert_eq!(original.subtitle, parsed.subtitle);
1733 }
1734
1735 #[test]
1736 fn card_props_omits_empty_subtitle_in_json() {
1737 let card = CardProps {
1738 title: "Card".to_string(),
1739 description: None,
1740 subtitle: None,
1741 badge: None,
1742 max_width: None,
1743 footer: Vec::new(),
1744 appearance: CardAppearance::Bordered,
1745 };
1746 let json = serde_json::to_string(&card).unwrap();
1747 assert!(
1748 !json.contains("\"subtitle\""),
1749 "empty subtitle must be skipped, got: {json}"
1750 );
1751 }
1752
1753 #[test]
1754 fn card_props_schema_includes_badge() {
1755 let schema = schemars::schema_for!(CardProps);
1756 let value = serde_json::to_value(&schema).expect("schema serializes to JSON");
1757 let props = value
1758 .get("properties")
1759 .and_then(|p| p.as_object())
1760 .expect("schema has a properties object");
1761 assert!(
1762 props.contains_key("badge"),
1763 "CardProps schema must expose a `badge` property; got keys: {:?}",
1764 props.keys().collect::<Vec<_>>()
1765 );
1766 let badge_schema = props.get("badge").expect("badge entry");
1771 let badge_json = badge_schema.to_string();
1772 assert!(
1773 badge_json.contains("\"string\""),
1774 "badge schema entry must mention string type; got: {badge_json}"
1775 );
1776 }
1777
1778 #[test]
1779 fn card_props_schema_includes_subtitle() {
1780 let schema = schemars::schema_for!(CardProps);
1781 let value = serde_json::to_value(&schema).expect("schema serializes to JSON");
1782 let props = value
1783 .get("properties")
1784 .and_then(|p| p.as_object())
1785 .expect("schema has a properties object");
1786 assert!(
1787 props.contains_key("subtitle"),
1788 "CardProps schema must expose a `subtitle` property; got keys: {:?}",
1789 props.keys().collect::<Vec<_>>()
1790 );
1791 let subtitle_schema = props.get("subtitle").expect("subtitle entry");
1796 let subtitle_json = subtitle_schema.to_string();
1797 assert!(
1798 subtitle_json.contains("\"string\""),
1799 "subtitle schema entry must mention string type; got: {subtitle_json}"
1800 );
1801 }
1802
1803 #[test]
1804 fn schema_for_checkbox_list_props_generates() {
1805 assert_schema_nonempty_object::<CheckboxListProps>("CheckboxListProps");
1806 }
1807
1808 #[test]
1809 fn checkbox_list_props_serde_roundtrip() {
1810 let json = serde_json::json!({
1811 "field": "services",
1812 "options": [{"value": "a", "label": "Alpha"}, {"value": "b", "label": "Beta"}],
1813 "selected_path": "/preselected"
1814 });
1815 let parsed: CheckboxListProps = serde_json::from_value(json.clone()).expect("decode");
1816 assert_eq!(parsed.field, "services");
1817 assert_eq!(parsed.options.len(), 2);
1818 assert_eq!(parsed.selected_path.as_deref(), Some("/preselected"));
1819 let reserialized = serde_json::to_value(&parsed).expect("encode");
1820 assert!(reserialized.get("label").is_none());
1822 assert!(reserialized.get("disabled").is_none());
1823 }
1824
1825 #[test]
1826 fn schema_for_rich_text_editor_props_generates() {
1827 assert_schema_nonempty_object::<RichTextEditorProps>("RichTextEditorProps");
1828 }
1829
1830 #[test]
1831 fn rich_text_editor_props_serde_roundtrip() {
1832 let json = serde_json::json!({
1833 "field": "body",
1834 "label": "Body"
1835 });
1836 let parsed: RichTextEditorProps = serde_json::from_value(json).expect("decode");
1837 assert_eq!(parsed.field, "body");
1838 assert_eq!(parsed.label, "Body");
1839 assert!(parsed.placeholder.is_none());
1840 assert!(parsed.default_value.is_none());
1841 assert!(parsed.data_path.is_none());
1842 assert!(parsed.error.is_none());
1843 let reserialized = serde_json::to_value(&parsed).expect("encode");
1844 assert!(reserialized.get("placeholder").is_none());
1846 assert!(reserialized.get("error").is_none());
1847 }
1848}
1849
1850#[cfg(test)]
1851mod strum_tests {
1852 use super::*;
1853
1854 use strum::VariantArray;
1855
1856 #[test]
1863 fn variant_enums_strum_matches_serde_wire_format() {
1864 fn check<T: AsRef<str> + serde::Serialize>(variants: &[T], label: &str) {
1865 for v in variants {
1866 let json = serde_json::to_string(v).expect("serialize");
1867 let json_stripped = json.trim_matches('"');
1868 assert_eq!(
1869 v.as_ref(),
1870 json_stripped,
1871 "strum AsRefStr drifted from serde for {label} variant"
1872 );
1873 }
1874 }
1875 check(Variant::VARIANTS, "Variant");
1876 check(Tone::VARIANTS, "Tone");
1877 check(Size::VARIANTS, "Size");
1878 }
1879
1880 #[test]
1884 fn canonical_enums_have_expected_variant_counts() {
1885 assert_eq!(Variant::VARIANTS.len(), 5);
1886 assert_eq!(Tone::VARIANTS.len(), 4);
1887 assert_eq!(Size::VARIANTS.len(), 3);
1888 }
1889
1890 #[test]
1891 fn tone_as_ref_str_matches_wire_format() {
1892 assert_eq!(Tone::Neutral.as_ref(), "neutral");
1893 assert_eq!(Tone::Success.as_ref(), "success");
1894 assert_eq!(Tone::Warning.as_ref(), "warning");
1895 assert_eq!(Tone::Destructive.as_ref(), "destructive");
1896 }
1897}
1898
1899#[cfg(test)]
1900mod canonical_enum_tests {
1901 use super::*;
1902
1903 #[test]
1906 fn variant_default_is_primary() {
1907 assert_eq!(Variant::default(), Variant::Primary);
1908 }
1909
1910 #[test]
1911 fn tone_default_is_neutral() {
1912 assert_eq!(Tone::default(), Tone::Neutral);
1913 }
1914
1915 #[test]
1916 fn size_default_is_md() {
1917 assert_eq!(Size::default(), Size::Md);
1918 }
1919
1920 #[test]
1923 fn variant_serde_snake_case_roundtrip() {
1924 use strum::VariantArray;
1925 for v in Variant::VARIANTS {
1926 let json = serde_json::to_value(v).unwrap();
1927 assert_eq!(json, serde_json::json!(v.as_ref()));
1928 let back: Variant = serde_json::from_value(json).unwrap();
1929 assert_eq!(back, *v);
1930 }
1931 assert_eq!(
1932 serde_json::from_str::<Variant>("\"primary\"").unwrap(),
1933 Variant::Primary
1934 );
1935 }
1936
1937 #[test]
1938 fn tone_serde_snake_case_roundtrip() {
1939 use strum::VariantArray;
1940 for t in Tone::VARIANTS {
1941 let json = serde_json::to_value(t).unwrap();
1942 assert_eq!(json, serde_json::json!(t.as_ref()));
1943 let back: Tone = serde_json::from_value(json).unwrap();
1944 assert_eq!(back, *t);
1945 }
1946 }
1947
1948 #[test]
1949 fn size_serde_snake_case_roundtrip() {
1950 use strum::VariantArray;
1951 for s in Size::VARIANTS {
1952 let json = serde_json::to_value(s).unwrap();
1953 assert_eq!(json, serde_json::json!(s.as_ref()));
1954 let back: Size = serde_json::from_value(json).unwrap();
1955 assert_eq!(back, *s);
1956 }
1957 assert!(serde_json::from_str::<Size>("\"md\"").is_ok());
1958 assert!(serde_json::from_str::<Size>("\"sm\"").is_ok());
1959 assert!(serde_json::from_str::<Size>("\"lg\"").is_ok());
1960 }
1961
1962 #[test]
1965 fn retired_size_values_are_rejected() {
1966 assert!(
1967 serde_json::from_str::<Size>("\"xs\"").is_err(),
1968 "size 'xs' was retired (migrate to 'sm')"
1969 );
1970 assert!(
1971 serde_json::from_str::<Size>("\"default\"").is_err(),
1972 "size 'default' was retired (migrate to 'md')"
1973 );
1974 }
1975
1976 #[test]
1977 fn retired_variant_values_are_rejected() {
1978 assert!(
1979 serde_json::from_str::<Variant>("\"default\"").is_err(),
1980 "variant 'default' was retired (migrate to 'primary')"
1981 );
1982 assert!(
1983 serde_json::from_str::<Variant>("\"link\"").is_err(),
1984 "variant 'link' was removed (migrate to 'ghost')"
1985 );
1986 }
1987
1988 #[test]
1989 fn retired_tone_values_are_rejected() {
1990 assert!(
1991 serde_json::from_str::<Tone>("\"info\"").is_err(),
1992 "tone 'info' was retired (migrate to 'neutral')"
1993 );
1994 assert!(
1995 serde_json::from_str::<Tone>("\"error\"").is_err(),
1996 "tone 'error' was retired (migrate to 'destructive')"
1997 );
1998 }
1999
2000 #[test]
2001 fn button_spec_with_link_variant_fails_to_decode() {
2002 let v = serde_json::json!({"variant": "link", "label": "x"});
2003 assert!(
2004 serde_json::from_value::<ButtonProps>(v).is_err(),
2005 "Button variant 'link' must fail decode (migrate to 'ghost')"
2006 );
2007 }
2008
2009 #[test]
2012 fn button_props_defaults_to_primary_md() {
2013 let v = serde_json::json!({"label": "x"});
2014 let p: ButtonProps = serde_json::from_value(v).unwrap();
2015 assert_eq!(p.variant, Variant::Primary);
2016 assert_eq!(p.size, Size::Md);
2017 }
2018
2019 #[test]
2020 fn alert_props_without_tone_defaults_to_neutral() {
2021 let v = serde_json::json!({"message": "x"});
2022 let p: AlertProps = serde_json::from_value(v).unwrap();
2023 assert_eq!(p.tone, Tone::Neutral);
2024 }
2025
2026 #[test]
2027 fn alert_props_with_tone_neutral_decodes() {
2028 let v = serde_json::json!({"message": "x", "tone": "neutral"});
2029 let p: AlertProps = serde_json::from_value(v).unwrap();
2030 assert_eq!(p.tone, Tone::Neutral);
2031 }
2032
2033 #[test]
2034 fn badge_props_without_tone_defaults_to_neutral() {
2035 let v = serde_json::json!({"label": "x"});
2036 let p: BadgeProps = serde_json::from_value(v).unwrap();
2037 assert_eq!(p.tone, Tone::Neutral);
2038 }
2039
2040 #[test]
2041 fn toast_props_without_tone_defaults_to_neutral() {
2042 let v = serde_json::json!({"message": "x"});
2043 let p: ToastProps = serde_json::from_value(v).unwrap();
2044 assert_eq!(p.tone, Tone::Neutral);
2045 }
2046
2047 #[test]
2048 fn action_card_props_with_success_tone_decodes() {
2049 let v = serde_json::json!({"title": "x", "description": "y", "tone": "success"});
2050 let p: ActionCardProps = serde_json::from_value(v).unwrap();
2051 assert_eq!(p.tone, Tone::Success);
2052 }
2053
2054 #[test]
2055 fn stat_card_props_without_tone_defaults_to_neutral() {
2056 let v = serde_json::json!({"label": "x", "value": "1"});
2057 let p: StatCardProps = serde_json::from_value(v).unwrap();
2058 assert_eq!(p.tone, Tone::Neutral);
2059 }
2060
2061 #[test]
2062 fn stat_card_props_roundtrip_preserves_tone() {
2063 let v = serde_json::json!({"label": "x", "value": "1", "tone": "warning"});
2064 let p: StatCardProps = serde_json::from_value(v).unwrap();
2065 assert_eq!(p.tone, Tone::Warning);
2066 let j = serde_json::to_value(&p).unwrap();
2067 let back: StatCardProps = serde_json::from_value(j).unwrap();
2068 assert_eq!(back.tone, Tone::Warning);
2069 }
2070}
2071
2072#[cfg(test)]
2073mod card_appearance_tests {
2074 use super::*;
2075
2076 #[test]
2077 fn card_appearance_default_is_bordered() {
2078 assert_eq!(CardAppearance::default(), CardAppearance::Bordered);
2079 }
2080
2081 #[test]
2082 fn card_appearance_serializes_snake_case() {
2083 assert_eq!(
2084 serde_json::to_value(CardAppearance::Bordered).unwrap(),
2085 serde_json::json!("bordered")
2086 );
2087 assert_eq!(
2088 serde_json::to_value(CardAppearance::Elevated).unwrap(),
2089 serde_json::json!("elevated")
2090 );
2091 }
2092
2093 #[test]
2094 fn card_appearance_deserializes_snake_case() {
2095 assert_eq!(
2096 serde_json::from_value::<CardAppearance>(serde_json::json!("bordered")).unwrap(),
2097 CardAppearance::Bordered
2098 );
2099 assert_eq!(
2100 serde_json::from_value::<CardAppearance>(serde_json::json!("elevated")).unwrap(),
2101 CardAppearance::Elevated
2102 );
2103 }
2104
2105 #[test]
2106 fn card_props_without_appearance_defaults_to_bordered() {
2107 let v = serde_json::json!({"title": "x"});
2108 let p: CardProps = serde_json::from_value(v).unwrap();
2109 assert_eq!(p.appearance, CardAppearance::Bordered);
2110 }
2111
2112 #[test]
2113 fn card_props_with_elevated_appearance() {
2114 let v = serde_json::json!({"title": "x", "appearance": "elevated"});
2115 let p: CardProps = serde_json::from_value(v).unwrap();
2116 assert_eq!(p.appearance, CardAppearance::Elevated);
2117 }
2118
2119 #[test]
2120 fn card_props_roundtrip_preserves_appearance() {
2121 let p = CardProps {
2122 title: "x".into(),
2123 description: None,
2124 subtitle: None,
2125 badge: None,
2126 max_width: None,
2127 footer: vec![],
2128 appearance: CardAppearance::Elevated,
2129 };
2130 let j = serde_json::to_value(&p).unwrap();
2131 let back: CardProps = serde_json::from_value(j).unwrap();
2132 assert_eq!(back.appearance, CardAppearance::Elevated);
2133 }
2134}
2135
2136#[cfg(test)]
2137mod kanban_board_props_tests {
2138 use super::*;
2139
2140 #[test]
2141 fn kanban_board_props_serde_static_columns() {
2142 let v = serde_json::json!({
2143 "columns": [{"title": "To Do", "id": "todo", "count": 0}]
2144 });
2145 let p: KanbanBoardProps = serde_json::from_value(v).unwrap();
2146 assert_eq!(p.columns.len(), 1);
2147 assert!(p.items_path.is_none());
2148 assert!(p.group_by.is_none());
2149 }
2150
2151 #[test]
2152 fn kanban_board_props_serde_data_bound() {
2153 let v = serde_json::json!({
2154 "columns": [{"title": "Open", "id": "open"}],
2155 "items_path": "/data/order",
2156 "group_by": "status",
2157 "card_title_key": "name"
2158 });
2159 let p: KanbanBoardProps = serde_json::from_value(v).unwrap();
2160 assert_eq!(p.columns.len(), 1);
2161 assert_eq!(p.items_path.as_deref(), Some("/data/order"));
2162 assert_eq!(p.group_by.as_deref(), Some("status"));
2163 assert_eq!(p.card_title_key.as_deref(), Some("name"));
2164 }
2165
2166 #[test]
2167 fn kanban_board_props_serde_neither() {
2168 let v = serde_json::json!({});
2169 let p: KanbanBoardProps = serde_json::from_value(v).unwrap();
2170 assert!(p.columns.is_empty());
2171 assert!(p.items_path.is_none());
2172 assert!(p.group_by.is_none());
2173 }
2174
2175 #[test]
2176 fn kanban_board_props_empty_columns_skipped_on_serialize() {
2177 let p = KanbanBoardProps {
2178 columns: vec![],
2179 items_path: Some("/data/order".into()),
2180 group_by: Some("status".into()),
2181 card_title_key: None,
2182 card_description_key: None,
2183 row_actions: None,
2184 row_key: None,
2185 mobile_default_column: None,
2186 empty_label: None,
2187 };
2188 let j = serde_json::to_value(&p).unwrap();
2189 assert!(
2190 j.get("columns").is_none(),
2191 "empty columns must be skipped, got: {j}"
2192 );
2193 assert_eq!(
2194 j.get("items_path").and_then(|v| v.as_str()),
2195 Some("/data/order")
2196 );
2197 }
2198}
2199
2200#[cfg(test)]
2201mod page_header_actions_tests {
2202 use super::*;
2203
2204 #[test]
2205 fn page_header_actions_missing_field() {
2206 let v = serde_json::json!({"title": "X"});
2207 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
2208 assert!(p.actions.is_empty());
2209 }
2210
2211 #[test]
2212 fn page_header_actions_null() {
2213 let v = serde_json::json!({"title": "X", "actions": null});
2214 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
2215 assert!(p.actions.is_empty());
2216 }
2217
2218 #[test]
2219 fn page_header_actions_empty_string() {
2220 let v = serde_json::json!({"title": "X", "actions": ""});
2221 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
2222 assert!(p.actions.is_empty());
2223 }
2224
2225 #[test]
2226 fn page_header_actions_empty_array() {
2227 let v = serde_json::json!({"title": "X", "actions": []});
2228 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
2229 assert!(p.actions.is_empty());
2230 }
2231
2232 #[test]
2233 fn page_header_actions_non_empty_array() {
2234 let v = serde_json::json!({"title": "X", "actions": ["a", "b"]});
2235 let p: PageHeaderProps = serde_json::from_value(v).unwrap();
2236 assert_eq!(p.actions, vec!["a".to_string(), "b".to_string()]);
2237 }
2238
2239 #[test]
2240 fn page_header_actions_non_empty_string_rejected() {
2241 let v = serde_json::json!({"title": "X", "actions": "not-empty"});
2242 let result: Result<PageHeaderProps, _> = serde_json::from_value(v);
2243 assert!(result.is_err(), "non-empty string must be rejected");
2244 }
2245
2246 #[test]
2247 fn page_header_actions_non_string_array_rejected() {
2248 let v = serde_json::json!({"title": "X", "actions": [1, 2, 3]});
2249 let result: Result<PageHeaderProps, _> = serde_json::from_value(v);
2250 assert!(result.is_err(), "array of non-strings must be rejected");
2251 }
2252}