radix_leptos_primitives/components/
pagination.rs

1use leptos::children::Children;
2use leptos::context::use_context;
3use leptos::prelude::*;
4
5/// Pagination page information
6#[derive(Clone, Debug, PartialEq)]
7pub struct PaginationPage {
8    pub number: usize,
9    pub label: Option<String>,
10    pub _disabled: bool,
11    pub _current: bool,
12}
13
14impl PaginationPage {
15    pub fn new(number: usize) -> Self {
16        Self {
17            number,
18            label: None,
19            _disabled: false,
20            _current: false,
21        }
22    }
23
24    pub fn with_label(mut self, label: String) -> Self {
25        self.label = Some(label);
26        self
27    }
28
29    pub fn withdisabled(mut self, disabled: bool) -> Self {
30        self._disabled = disabled;
31        self
32    }
33
34    pub fn withcurrent(mut self, current: bool) -> Self {
35        self._current = current;
36        self
37    }
38}
39
40/// Pagination size
41#[derive(Clone, Debug, PartialEq)]
42pub enum PaginationSize {
43    Small,
44    Medium,
45    Large,
46}
47
48impl PaginationSize {
49    pub fn as_str(&self) -> &'static str {
50        match self {
51            PaginationSize::Small => "small",
52            PaginationSize::Medium => "medium",
53            PaginationSize::Large => "large",
54        }
55    }
56}
57
58/// Pagination variant
59#[derive(Clone, Debug, PartialEq)]
60pub enum PaginationVariant {
61    Default,
62    Compact,
63    Detailed,
64}
65
66impl PaginationVariant {
67    pub fn as_str(&self) -> &'static str {
68        match self {
69            PaginationVariant::Default => "default",
70            PaginationVariant::Compact => "compact",
71            PaginationVariant::Detailed => "detailed",
72        }
73    }
74}
75
76/// Pagination context for state management
77#[derive(Clone)]
78pub struct PaginationContext {
79    pub current_page: Signal<usize>,
80    pub total_pages: usize,
81    pub page_size: usize,
82    pub total_items: usize,
83    pub size: PaginationSize,
84    pub variant: PaginationVariant,
85    pub _show_first_last: bool,
86    pub _show_prev_next: bool,
87    pub _show_page_numbers: bool,
88    pub pagination_id: String,
89    pub on_page_change: Option<Callback<usize>>,
90}
91
92/// Generate a simple unique ID for components
93fn generate_id(prefix: &str) -> String {
94    static COUNTER: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
95    let id = COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
96    format!("{}-{}", prefix, id)
97}
98
99/// Merge CSS classes
100fn merge_classes(existing: Option<&str>, additional: Option<&str>) -> Option<String> {
101    match (existing, additional) {
102        (Some(a), Some(b)) => Some(format!("{} {}", a, b)),
103        (Some(a), None) => Some(a.to_string()),
104        (None, Some(b)) => Some(b.to_string()),
105        (None, None) => None,
106    }
107}
108
109/// Calculate visible page range
110fn calculate_page_range(
111    current_page: usize,
112    total_pages: usize,
113    maxvisible: usize,
114) -> (usize, usize) {
115    if total_pages <= maxvisible {
116        return (1, total_pages);
117    }
118
119    let halfvisible = maxvisible / 2;
120    let mut start = current_page.saturating_sub(halfvisible);
121    let mut end = start + maxvisible - 1;
122
123    if end > total_pages {
124        end = total_pages;
125        start = end.saturating_sub(maxvisible - 1);
126    }
127
128    (start, end)
129}
130
131/// Main Pagination component
132#[component]
133pub fn Pagination(
134    /// Current page number (1-based)
135    #[prop(optional, default = 1)]
136    current_page: usize,
137    /// Total number of pages
138    #[prop(optional, default = 1)]
139    total_pages: usize,
140    /// Number of items per page
141    #[prop(optional, default = 10)]
142    page_size: usize,
143    /// Total number of items
144    #[prop(optional)]
145    total_items: Option<usize>,
146    /// Pagination size
147    #[prop(optional, default = PaginationSize::Medium)]
148    size: PaginationSize,
149    /// Pagination variant
150    #[prop(optional, default = PaginationVariant::Default)]
151    variant: PaginationVariant,
152    /// Whether to show first/last page buttons
153    #[prop(optional, default = true)]
154    _show_first_last: bool,
155    /// Whether to show previous/next buttons
156    #[prop(optional, default = true)]
157    _show_prev_next: bool,
158    /// Whether to show page numbers
159    #[prop(optional, default = true)]
160    _show_page_numbers: bool,
161    /// Page change event handler
162    #[prop(optional)]
163    on_page_change: Option<Callback<usize>>,
164    /// CSS classes
165    #[prop(optional)]
166    class: Option<String>,
167    /// Child content (pagination items, etc.)
168    children: Children,
169) -> impl IntoView {
170    let pagination_id = generate_id("pagination");
171
172    // Reactive state
173    let (current_page_signal, _setcurrent_page_signal) = signal(current_page);
174    let total_items_calculated = total_items.unwrap_or_else(|| total_pages * page_size);
175
176    // Create context
177    let context = PaginationContext {
178        current_page: current_page_signal.into(),
179        total_pages,
180        page_size,
181        total_items: total_items_calculated,
182        size: size.clone(),
183        variant: variant.clone(),
184        _show_first_last,
185        _show_prev_next,
186        _show_page_numbers,
187        pagination_id: pagination_id.clone(),
188        on_page_change,
189    };
190
191    // Build base classes
192    let base_classes = "radix-pagination";
193    let combined_class = merge_classes(Some(base_classes), class.as_deref())
194        .unwrap_or_else(|| base_classes.to_string());
195
196    // Provide the context
197    provide_context(context);
198
199    view! {
200        <nav
201            id=pagination_id
202            class=combined_class
203            data-current-page=current_page_signal.get()
204            data-total-pages=total_pages
205            data-page-size=page_size
206            data-total-items=total_items_calculated
207            data-size=size.as_str()
208            data-variant=variant.as_str()
209            data-show-first-last=_show_first_last
210            data-show-prev-next=_show_prev_next
211            data-show-page-numbers=_show_page_numbers
212            role="navigation"
213            aria-label="Pagination"
214        >
215            {children()}
216        </nav>
217    }
218}
219
220/// PaginationList component for the pagination items container
221#[component]
222pub fn PaginationList(
223    /// CSS classes
224    #[prop(optional)]
225    class: Option<String>,
226    /// CSS styles
227    #[prop(optional)]
228    style: Option<String>,
229    /// Child content (pagination items)
230    children: Children,
231) -> impl IntoView {
232    let _context =
233        use_context::<PaginationContext>().expect("PaginationList must be used within Pagination");
234    let list_id = generate_id("pagination-list");
235
236    // Build base classes
237    let base_classes = "radix-pagination-list";
238    let combined_class = merge_classes(Some(base_classes), class.as_deref())
239        .unwrap_or_else(|| base_classes.to_string());
240
241    view! {
242        <ul
243            id=list_id
244            class=combined_class
245            style=style.unwrap_or_default()
246            role="list"
247        >
248            {children()}
249        </ul>
250    }
251}
252
253/// PaginationItem component for individual pagination items
254#[component]
255pub fn PaginationItem(
256    /// The pagination item this component represents
257    #[prop(optional)]
258    page: Option<PaginationPage>,
259    /// Whether this item is current
260    #[prop(optional)]
261    current: Option<bool>,
262    /// Whether this item is disabled
263    #[prop(optional)]
264    disabled: Option<bool>,
265    /// CSS classes
266    #[prop(optional)]
267    class: Option<String>,
268    /// CSS styles
269    #[prop(optional)]
270    style: Option<String>,
271    /// Child content
272    children: Children,
273) -> impl IntoView {
274    let context =
275        use_context::<PaginationContext>().expect("PaginationItem must be used within Pagination");
276    let item_id = generate_id("pagination-item");
277
278    let page_clone = page.clone();
279    let handle_click = move |event: web_sys::MouseEvent| {
280        event.prevent_default();
281
282        if let Some(page) = page_clone.clone() {
283            if !page._disabled {
284                // Call the page change handler
285                if let Some(callback) = context.on_page_change.clone() {
286                    callback.run(page.number);
287                }
288            }
289        }
290    };
291
292    let page_forcurrent = page.clone();
293    let page_fordisabled = page.clone();
294
295    // Determine if this item is current
296    let iscurrent = Memo::new(move |_| {
297        if let Some(current) = current {
298            current
299        } else if let Some(page) = page_forcurrent.as_ref() {
300            page._current
301        } else {
302            false
303        }
304    });
305
306    // Determine if this item is disabled
307    let isdisabled = Memo::new(move |_| {
308        if let Some(disabled) = disabled {
309            disabled
310        } else if let Some(page) = page_fordisabled.as_ref() {
311            page._disabled
312        } else {
313            false
314        }
315    });
316
317    // Build base classes
318    let base_classes = "radix-pagination-item";
319    let combined_class = merge_classes(Some(base_classes), class.as_deref())
320        .unwrap_or_else(|| base_classes.to_string());
321
322    view! {
323        <li
324            id=item_id
325            class=combined_class
326            style=style.unwrap_or_default()
327            data-current=iscurrent.get()
328            data-disabled=isdisabled.get()
329            role="listitem"
330        >
331            <button
332                class="radix-pagination-button"
333                data-current=iscurrent.get()
334                data-disabled=isdisabled.get()
335                type="button"
336                role="button"
337                on:click=handle_click
338            >
339                {children()}
340            </button>
341        </li>
342    }
343}
344
345/// PaginationFirst component for first page button
346#[component]
347pub fn PaginationFirst(
348    /// Button text
349    #[prop(optional)]
350    text: Option<String>,
351    /// Button icon
352    #[prop(optional)]
353    icon: Option<String>,
354    /// CSS classes
355    #[prop(optional)]
356    class: Option<String>,
357    /// CSS styles
358    #[prop(optional)]
359    style: Option<String>,
360    /// Child content
361    children: Children,
362) -> impl IntoView {
363    let context =
364        use_context::<PaginationContext>().expect("PaginationFirst must be used within Pagination");
365    let first_id = generate_id("pagination-first");
366
367    let handle_click = move |event: web_sys::MouseEvent| {
368        event.prevent_default();
369
370        if context.current_page.get() > 1 {
371            // Call the page change handler
372            if let Some(callback) = context.on_page_change.clone() {
373                callback.run(1);
374            }
375        }
376    };
377
378    let isdisabled = Memo::new(move |_| context.current_page.get() <= 1);
379
380    // Build base classes
381    let base_classes = "radix-pagination-first";
382    let combined_class = merge_classes(Some(base_classes), class.as_deref())
383        .unwrap_or_else(|| base_classes.to_string());
384
385    view! {
386        <li
387            id=first_id
388            class=combined_class
389            style=style.unwrap_or_default()
390            data-disabled=isdisabled.get()
391            role="listitem"
392        >
393            <button
394                class="radix-pagination-button"
395                data-disabled=isdisabled.get()
396                type="button"
397                role="button"
398                on:click=handle_click
399            >
400                {icon.map(|icon_text| view! {
401                    <span class="radix-pagination-icon">{icon_text}</span>
402                })}
403                {text.map(|button_text| view! {
404                    <span class="radix-pagination-text">{button_text}</span>
405                })}
406                {children()}
407            </button>
408        </li>
409    }
410}
411
412/// PaginationPrevious component for previous page button
413#[component]
414pub fn PaginationPrevious(
415    /// Button text
416    #[prop(optional)]
417    text: Option<String>,
418    /// Button icon
419    #[prop(optional)]
420    icon: Option<String>,
421    /// CSS classes
422    #[prop(optional)]
423    class: Option<String>,
424    /// CSS styles
425    #[prop(optional)]
426    style: Option<String>,
427    /// Child content
428    children: Children,
429) -> impl IntoView {
430    let context = use_context::<PaginationContext>()
431        .expect("PaginationPrevious must be used within Pagination");
432    let prev_id = generate_id("pagination-previous");
433
434    let handle_click = move |event: web_sys::MouseEvent| {
435        event.prevent_default();
436
437        let current = context.current_page.get();
438        if current > 1 {
439            // Call the page change handler
440            if let Some(callback) = context.on_page_change.clone() {
441                callback.run(current - 1);
442            }
443        }
444    };
445
446    let isdisabled = Memo::new(move |_| context.current_page.get() <= 1);
447
448    // Build base classes
449    let base_classes = "radix-pagination-previous";
450    let combined_class = merge_classes(Some(base_classes), class.as_deref())
451        .unwrap_or_else(|| base_classes.to_string());
452
453    view! {
454        <li
455            id=prev_id
456            class=combined_class
457            style=style.unwrap_or_default()
458            data-disabled=isdisabled.get()
459            role="listitem"
460        >
461            <button
462                class="radix-pagination-button"
463                data-disabled=isdisabled.get()
464                type="button"
465                role="button"
466                on:click=handle_click
467            >
468                {icon.map(|icon_text| view! {
469                    <span class="radix-pagination-icon">{icon_text}</span>
470                })}
471                {text.map(|button_text| view! {
472                    <span class="radix-pagination-text">{button_text}</span>
473                })}
474                {children()}
475            </button>
476        </li>
477    }
478}
479
480/// PaginationNext component for next page button
481#[component]
482pub fn PaginationNext(
483    /// Button text
484    #[prop(optional)]
485    text: Option<String>,
486    /// Button icon
487    #[prop(optional)]
488    icon: Option<String>,
489    /// CSS classes
490    #[prop(optional)]
491    class: Option<String>,
492    /// CSS styles
493    #[prop(optional)]
494    style: Option<String>,
495    /// Child content
496    children: Children,
497) -> impl IntoView {
498    let context =
499        use_context::<PaginationContext>().expect("PaginationNext must be used within Pagination");
500    let next_id = generate_id("pagination-next");
501
502    let handle_click = move |event: web_sys::MouseEvent| {
503        event.prevent_default();
504
505        let current = context.current_page.get();
506        if current < context.total_pages {
507            // Call the page change handler
508            if let Some(callback) = context.on_page_change.clone() {
509                callback.run(current + 1);
510            }
511        }
512    };
513
514    let isdisabled = Memo::new(move |_| context.current_page.get() >= context.total_pages);
515
516    // Build base classes
517    let base_classes = "radix-pagination-next";
518    let combined_class = merge_classes(Some(base_classes), class.as_deref())
519        .unwrap_or_else(|| base_classes.to_string());
520
521    view! {
522        <li
523            id=next_id
524            class=combined_class
525            style=style.unwrap_or_default()
526            data-disabled=isdisabled.get()
527            role="listitem"
528        >
529            <button
530                class="radix-pagination-button"
531                data-disabled=isdisabled.get()
532                type="button"
533                role="button"
534                on:click=handle_click
535            >
536                {icon.map(|icon_text| view! {
537                    <span class="radix-pagination-icon">{icon_text}</span>
538                })}
539                {text.map(|button_text| view! {
540                    <span class="radix-pagination-text">{button_text}</span>
541                })}
542                {children()}
543            </button>
544        </li>
545    }
546}
547
548/// PaginationLast component for last page button
549#[component]
550pub fn PaginationLast(
551    /// Button text
552    #[prop(optional)]
553    text: Option<String>,
554    /// Button icon
555    #[prop(optional)]
556    icon: Option<String>,
557    /// CSS classes
558    #[prop(optional)]
559    class: Option<String>,
560    /// CSS styles
561    #[prop(optional)]
562    style: Option<String>,
563    /// Child content
564    children: Children,
565) -> impl IntoView {
566    let context =
567        use_context::<PaginationContext>().expect("PaginationLast must be used within Pagination");
568    let last_id = generate_id("pagination-last");
569
570    let handle_click = move |event: web_sys::MouseEvent| {
571        event.prevent_default();
572
573        if context.current_page.get() < context.total_pages {
574            // Call the page change handler
575            if let Some(callback) = context.on_page_change.clone() {
576                callback.run(context.total_pages);
577            }
578        }
579    };
580
581    let isdisabled = Memo::new(move |_| context.current_page.get() >= context.total_pages);
582
583    // Build base classes
584    let base_classes = "radix-pagination-last";
585    let combined_class = merge_classes(Some(base_classes), class.as_deref())
586        .unwrap_or_else(|| base_classes.to_string());
587
588    view! {
589        <li
590            id=last_id
591            class=combined_class
592            style=style.unwrap_or_default()
593            data-disabled=isdisabled.get()
594            role="listitem"
595        >
596            <button
597                class="radix-pagination-button"
598                data-disabled=isdisabled.get()
599                type="button"
600                role="button"
601                on:click=handle_click
602            >
603                {icon.map(|icon_text| view! {
604                    <span class="radix-pagination-icon">{icon_text}</span>
605                })}
606                {text.map(|button_text| view! {
607                    <span class="radix-pagination-text">{button_text}</span>
608                })}
609                {children()}
610            </button>
611        </li>
612    }
613}
614
615/// PaginationEllipsis component for truncated page ranges
616#[component]
617pub fn PaginationEllipsis(
618    /// Ellipsis text
619    #[prop(optional)]
620    text: Option<String>,
621    /// CSS classes
622    #[prop(optional)]
623    class: Option<String>,
624    /// CSS styles
625    #[prop(optional)]
626    style: Option<String>,
627    /// Child content
628    children: Children,
629) -> impl IntoView {
630    let ellipsis_id = generate_id("pagination-ellipsis");
631
632    // Build base classes
633    let base_classes = "radix-pagination-ellipsis";
634    let combined_class = merge_classes(Some(base_classes), class.as_deref())
635        .unwrap_or_else(|| base_classes.to_string());
636
637    view! {
638        <li
639            id=ellipsis_id
640            class=combined_class
641            style=style.unwrap_or_default()
642            role="separator"
643            aria-hidden="true"
644        >
645            <span class="radix-pagination-ellipsis-text">
646                {text.unwrap_or_else(|| "…".to_string())}
647            </span>
648            {children()}
649        </li>
650    }
651}
652
653/// PaginationInfo component for displaying pagination information
654#[component]
655pub fn PaginationInfo(
656    /// Information format
657    #[prop(optional)]
658    format: Option<String>,
659    /// CSS classes
660    #[prop(optional)]
661    class: Option<String>,
662    /// CSS styles
663    #[prop(optional)]
664    style: Option<String>,
665    /// Child content
666    children: Children,
667) -> impl IntoView {
668    let context =
669        use_context::<PaginationContext>().expect("PaginationInfo must be used within Pagination");
670    let info_id = generate_id("pagination-info");
671
672    // Calculate pagination information
673    let start_item = Memo::new(move |_| {
674        let current = context.current_page.get();
675        let page_size = context.page_size;
676        ((current - 1) * page_size) + 1
677    });
678
679    let end_item = Memo::new(move |_| {
680        let current = context.current_page.get();
681        let page_size = context.page_size;
682        let total_items = context.total_items;
683        std::cmp::min(current * page_size, total_items)
684    });
685
686    let total_items = context.total_items;
687
688    // Build base classes
689    let base_classes = "radix-pagination-info";
690    let combined_class = merge_classes(Some(base_classes), class.as_deref())
691        .unwrap_or_else(|| base_classes.to_string());
692
693    view! {
694        <div
695            id=info_id
696            class=combined_class
697            style=style.unwrap_or_default()
698            role="status"
699            aria-live="polite"
700        >
701            {if let Some(format_str) = format {
702                let start = start_item.get();
703                let end = end_item.get();
704                let total = total_items;
705                let current = context.current_page.get();
706                let total_pages = context.total_pages;
707
708                let info_text = format_str
709                    .replace("{start}", &start.to_string())
710                    .replace("{end}", &end.to_string())
711                    .replace("{total}", &total.to_string())
712                    .replace("{current}", &current.to_string())
713                    .replace("{total_pages}", &total_pages.to_string());
714
715                view! {
716                    <span class="radix-pagination-info-text">{info_text}</span>
717                }
718            } else {
719                view! { <span class="radix-pagination-info-text">{String::new()}</span> }
720            }}
721            {children()}
722        </div>
723    }
724}
725
726/// PaginationContent component for wrapping pagination content
727#[component]
728pub fn PaginationContent(
729    /// CSS classes
730    #[prop(optional)]
731    class: Option<String>,
732    /// CSS styles
733    #[prop(optional)]
734    style: Option<String>,
735    /// Child content
736    children: Children,
737) -> impl IntoView {
738    let content_id = generate_id("pagination-content");
739
740    // Build base classes
741    let base_classes = "radix-pagination-content";
742    let combined_class = merge_classes(Some(base_classes), class.as_deref())
743        .unwrap_or_else(|| base_classes.to_string());
744
745    view! {
746        <div
747            id=content_id
748            class=combined_class
749            style=style.unwrap_or_default()
750        >
751            {children()}
752        </div>
753    }
754}
755
756/// Helper function to generate page numbers for pagination
757pub fn generate_page_numbers(
758    current_page: usize,
759    total_pages: usize,
760    maxvisible: usize,
761) -> Vec<PaginationPage> {
762    if total_pages <= maxvisible {
763        return (1..=total_pages)
764            .map(|page| PaginationPage::new(page).withcurrent(page == current_page))
765            .collect();
766    }
767
768    let (start, end) = calculate_page_range(current_page, total_pages, maxvisible);
769    let mut pages = Vec::new();
770
771    // Add first page if not in range
772    if start > 1 {
773        pages.push(PaginationPage::new(1));
774        if start > 2 {
775            pages.push(PaginationPage::new(0).withdisabled(true)); // Placeholder for ellipsis
776        }
777    }
778
779    // Add visible pages
780    for page in start..=end {
781        pages.push(PaginationPage::new(page).withcurrent(page == current_page));
782    }
783
784    // Add last page if not in range
785    if end < total_pages {
786        if end < total_pages - 1 {
787            pages.push(PaginationPage::new(0).withdisabled(true)); // Placeholder for ellipsis
788        }
789        pages.push(PaginationPage::new(total_pages));
790    }
791
792    pages
793}
794
795/// Helper function to generate page numbers for pagination
796/// This function returns a vector of page numbers that should be displayed
797/// It handles ellipsis for large page counts
798pub fn getvisible_page_numbers(
799    current_page: usize,
800    total_pages: usize,
801    maxvisible: usize,
802) -> Vec<usize> {
803    if total_pages <= maxvisible {
804        return (1..=total_pages).collect();
805    }
806
807    let (start, end) = calculate_page_range(current_page, total_pages, maxvisible);
808    let mut pages = Vec::new();
809
810    // Add first page if not in range
811    if start > 1 {
812        pages.push(1);
813        if start > 2 {
814            pages.push(0); // Placeholder for ellipsis
815        }
816    }
817
818    // Add visible pages
819    for page in start..=end {
820        pages.push(page);
821    }
822
823    // Add last page if not in range
824    if end < total_pages {
825        if end < total_pages - 1 {
826            pages.push(0); // Placeholder for ellipsis
827        }
828        pages.push(total_pages);
829    }
830
831    pages
832}
833
834#[cfg(test)]
835mod tests {
836    use crate::{Pagination, PaginationProps, PaginationSize, PaginationVariant};
837    use leptos::callback::Callback;
838    use proptest::prelude::*;
839    use wasm_bindgen_test::*;
840
841    wasm_bindgen_test_configure!(run_in_browser);
842
843    // 1. Basic Rendering Tests
844    #[test]
845    fn test_pagination_sizes() {
846        run_test(|| {
847            let sizes = [
848                PaginationSize::Small,
849                PaginationSize::Medium,
850                PaginationSize::Large,
851            ];
852
853            for size in sizes {
854                // Each size should have a valid string representation
855                assert!(!size.as_str().is_empty());
856            }
857        });
858    }
859
860    // 2. Props Validation Tests
861    #[test]
862    fn test_pagination_variants() {
863        run_test(|| {
864            let variants = [
865                PaginationVariant::Default,
866                PaginationVariant::Compact,
867                PaginationVariant::Detailed,
868            ];
869
870            for variant in variants {
871                // Each variant should have a valid string representation
872                assert!(!variant.as_str().is_empty());
873            }
874        });
875    }
876
877    // 3. State Management Tests
878    #[test]
879    fn test_pagination_page_change() {
880        run_test(|| {
881            // Test pagination state logic
882            let mut current_page = 1;
883            let total_pages = 10;
884
885            // Initial page should be 1
886            assert_eq!(current_page, 1);
887
888            // Simulate page change
889            current_page = 2;
890            assert_eq!(current_page, 2);
891
892            // Should not exceed total pages
893            assert!(current_page <= total_pages);
894        });
895    }
896
897    // 4. Property-Based Tests
898    proptest! {
899        #[test]
900        fn test_pagination_properties(
901            current_page in 1..100usize,
902            total_pages in 1..100usize,
903            page_size in 1..50usize
904        ) {
905            // Property: current_page should never exceed total_pages
906            prop_assume!(current_page <= total_pages);
907
908            // Calculate total_items based on realistic pagination scenario
909            let total_items = total_pages * page_size;
910
911            // Property: Pagination should always render without panicking
912            // Property: Calculated values should be consistent
913            let max_possible_items = total_pages * page_size;
914            prop_assert!(total_items <= max_possible_items);
915
916            // Property: Current page should never exceed total pages
917            prop_assert!(current_page <= total_pages);
918
919            // Property: Page size should be positive
920            prop_assert!(page_size > 0);
921        }
922    }
923
924    // Helper function for running tests
925    fn run_test<F>(f: F)
926    where
927        F: FnOnce(),
928    {
929        // Simplified test runner for Leptos 0.8
930        f();
931    }
932}