Skip to main content

dioxus_showcase/
lib.rs

1//! Public facade crate for dioxus-showcase.
2use dioxus::prelude::*;
3use dioxus_showcase_core::StoryDefinition;
4
5pub mod prelude {
6    pub use crate::{GeneratedStory, ShowcaseStoryFactory};
7    pub use crate::{StoryArg, StoryArgs, StoryProps, StoryVariant};
8    pub use dioxus_showcase_core::{
9        ProviderDefinition, ShowcaseRegistry, StoryDefinition, StoryEntry,
10    };
11    pub use dioxus_showcase_macros::{provider, showcase, story, StoryProps};
12}
13
14pub use dioxus_showcase_core as core;
15pub use dioxus_showcase_macros as macros;
16
17pub type StoryProvider = fn(Element) -> Element;
18
19/// Converts a display title like `Atoms/Button` into a stable route slug.
20pub fn slugify_title(title: &str) -> String {
21    let mut out = String::with_capacity(title.len());
22    let mut prev_dash = false;
23
24    for ch in title.chars() {
25        let normalized = ch.to_ascii_lowercase();
26        if normalized.is_ascii_alphanumeric() {
27            out.push(normalized);
28            prev_dash = false;
29            continue;
30        }
31
32        if !prev_dash {
33            out.push('-');
34            prev_dash = true;
35        }
36    }
37
38    out.trim_matches('-').to_owned()
39}
40
41/// Produces a single default story value for one component parameter.
42pub trait StoryArg: Sized {
43    /// Returns the default value used when a story parameter needs a seed value.
44    fn story_arg() -> Self;
45}
46
47/// Produces one or more story values for aggregate props types.
48pub trait StoryArgs: Sized {
49    /// Returns all available aggregate values for a story source.
50    fn stories() -> Vec<Self>;
51}
52
53impl<T: StoryArg> StoryArgs for T {
54    /// Wraps a single `StoryArg` value in a one-item story list.
55    fn stories() -> Vec<Self> {
56        vec![T::story_arg()]
57    }
58}
59
60/// Produces one or more named prop sets for a showcase component.
61pub trait StoryProps: Sized {
62    /// Returns the prop variants rendered as separate showcase stories.
63    fn stories() -> Vec<StoryVariant<Self>>;
64}
65
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct StoryVariant<T> {
68    pub name: Option<String>,
69    pub value: T,
70}
71
72impl<T> StoryVariant<T> {
73    /// Creates an unnamed story variant.
74    pub fn unnamed(value: T) -> Self {
75        Self { name: None, value }
76    }
77
78    /// Creates a named story variant appended to the base title.
79    pub fn named(name: impl Into<String>, value: T) -> Self {
80        Self { name: Some(name.into()), value }
81    }
82}
83
84macro_rules! impl_story_arg_with_default {
85    ($($ty:ty),* $(,)?) => {
86        $(
87            impl StoryArg for $ty {
88                fn story_arg() -> Self {
89                    Self::default()
90                }
91            }
92        )*
93    };
94}
95
96impl_story_arg_with_default!(
97    bool, char, i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64
98);
99
100impl StoryArg for &'static str {
101    /// Uses placeholder copy for string controls.
102    fn story_arg() -> Self {
103        "Lorem Ipsum"
104    }
105}
106
107impl StoryArg for String {
108    /// Uses placeholder copy for owned string controls.
109    fn story_arg() -> Self {
110        "Lorem Ipsum".to_string()
111    }
112}
113
114impl<T> StoryArg for Option<T> {
115    /// Defaults optional values to `None`.
116    fn story_arg() -> Self {
117        None
118    }
119}
120
121impl<T> StoryArg for Vec<T> {
122    /// Defaults vector values to an empty collection.
123    fn story_arg() -> Self {
124        Vec::new()
125    }
126}
127
128impl StoryArg for Element {
129    /// Produces placeholder markup for element slot arguments.
130    fn story_arg() -> Self {
131        rsx! {
132            div { "Story content" }
133        }
134    }
135}
136
137impl<T: Sized + 'static> StoryArg for EventHandler<T> {
138    /// Produces a no-op callback for event handler props.
139    fn story_arg() -> Self {
140        Callback::new(|_| {})
141    }
142}
143
144pub struct GeneratedStory {
145    pub definition: StoryDefinition,
146    pub render: Box<dyn Fn() -> Element>,
147}
148
149pub trait ShowcaseStoryFactory {
150    /// Creates the generated story list for one annotated source item.
151    fn create(source_path: &str, module_path: &str) -> Vec<GeneratedStory>;
152}
153
154/// Wraps rendered story content with the currently registered provider chain.
155#[component]
156pub fn StoryPreviewContent(children: Element) -> Element {
157    let providers = try_use_context::<Vec<StoryProvider>>().unwrap_or_default();
158    let mut wrapped = children;
159    for provider in providers.into_iter().rev() {
160        wrapped = provider(wrapped);
161    }
162    wrapped
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168    #[derive(Default)]
169    struct DemoArgs;
170
171    impl StoryArg for DemoArgs {
172        fn story_arg() -> Self {
173            Default::default()
174        }
175    }
176
177    #[test]
178    fn story_arg_trait_is_implementable() {
179        fn assert_story_arg<T: StoryArg>() {}
180        assert_story_arg::<DemoArgs>();
181    }
182
183    #[test]
184    fn story_args_trait_is_derived_from_story_arg() {
185        fn assert_story_args<T: StoryArgs>() {}
186        assert_story_args::<DemoArgs>();
187    }
188
189    #[derive(Default)]
190    struct DemoProps;
191
192    impl StoryProps for DemoProps {
193        fn stories() -> Vec<StoryVariant<Self>> {
194            vec![StoryVariant::unnamed(Default::default())]
195        }
196    }
197
198    #[test]
199    fn story_props_trait_supports_named_variants() {
200        let stories = DemoProps::stories();
201        assert_eq!(stories.len(), 1);
202        assert_eq!(stories[0].name, None);
203    }
204
205    #[test]
206    fn prelude_reexports_core_types() {
207        let definition = prelude::StoryDefinition {
208            id: "id".to_owned(),
209            title: "title".to_owned(),
210            source_path: "showcase/button.stories.rs".to_owned(),
211            module_path: "module::story".to_owned(),
212            renderer_symbol: "story_renderer".to_owned(),
213            tags: vec!["tag".to_owned()],
214        };
215
216        let entry = prelude::StoryEntry { definition, renderer_symbol: "story_renderer" };
217
218        let mut registry = prelude::ShowcaseRegistry::default();
219        registry.register(entry);
220        assert_eq!(registry.story_count(), 1);
221    }
222
223    #[test]
224    fn element_story_arg_defaults_to_placeholder_markup() {
225        let element: Element = StoryArg::story_arg();
226        assert!(element.is_ok());
227    }
228}