Coverage for src / lexigram / ui / molecules / pagination.py: 100%
34 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-10 04:11 +0800
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-10 04:11 +0800
1"""Simple general-purpose prev/next pagination component."""
3from __future__ import annotations
5from typing import Any
7from lexigram.ui.core.base import Component, el
10class Pagination(Component):
11 """Simple prev/next pagination for any URL-based page navigation.
13 Renders summary text and previous/next page links using plain anchor tags.
14 For HTMX-enhanced pagination with zone targeting, use the admin-specific
15 Pagination in lexigram.admin.ui.
16 """
18 def __init__(
19 self,
20 page: int = 1,
21 total: int = 0,
22 per_page: int = 20,
23 base_url: str = "",
24 show_summary: bool = True,
25 **props: Any,
26 ) -> None:
27 """Initialize the pagination component.
29 Args:
30 page: Current page number (1-based).
31 total: Total number of items.
32 per_page: Number of items per page.
33 base_url: Base URL for page links (page/per_page appended as query params).
34 show_summary: Whether to show the "Showing X to Y of Z" summary.
35 """
36 super().__init__()
37 self.page = max(1, int(page))
38 self.total = max(0, int(total))
39 self.per_page = max(1, int(per_page))
40 self.base_url = base_url
41 self.show_summary = show_summary
43 @property
44 def total_pages(self) -> int:
45 """Total number of pages."""
46 return (self.total + self.per_page - 1) // self.per_page
48 def _page_url(self, page: int) -> str:
49 separator = "&" if "?" in self.base_url else "?"
50 return f"{self.base_url}{separator}page={page}&per_page={self.per_page}"
52 def render(self) -> Any:
53 if self.total_pages <= 1:
54 return ""
56 start = (self.page - 1) * self.per_page + 1
57 end = min(self.total, self.page * self.per_page)
59 parts: list[Any] = []
61 if self.show_summary:
62 parts.append(
63 el(
64 "span",
65 f"Showing {start}-{end} of {self.total}",
66 class_="text-sm text-muted-foreground",
67 ),
68 )
70 if self.page > 1:
71 parts.append(
72 el(
73 "a",
74 "← Previous",
75 href=self._page_url(self.page - 1),
76 class_="px-3 py-1 text-sm text-primary hover:underline",
77 aria_label="Previous page",
78 ),
79 )
80 else:
81 parts.append(
82 el(
83 "span",
84 "← Previous",
85 class_="px-3 py-1 text-sm text-muted-foreground cursor-not-allowed",
86 ),
87 )
89 parts.append(
90 el(
91 "span",
92 f"Page {self.page} of {self.total_pages}",
93 class_="px-3 py-1 text-sm text-foreground",
94 aria_current="page",
95 ),
96 )
98 if self.page < self.total_pages:
99 parts.append(
100 el(
101 "a",
102 "Next →",
103 href=self._page_url(self.page + 1),
104 class_="px-3 py-1 text-sm text-primary hover:underline",
105 aria_label="Next page",
106 ),
107 )
108 else:
109 parts.append(
110 el(
111 "span",
112 "Next →",
113 class_="px-3 py-1 text-sm text-muted-foreground cursor-not-allowed",
114 ),
115 )
117 return el(
118 "nav",
119 *parts,
120 class_="flex flex-wrap items-center gap-2",
121 aria_label="Pagination",
122 )
125__all__ = ["Pagination"]