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

1"""Simple general-purpose prev/next pagination component.""" 

2 

3from __future__ import annotations 

4 

5from typing import Any 

6 

7from lexigram.ui.core.base import Component, el 

8 

9 

10class Pagination(Component): 

11 """Simple prev/next pagination for any URL-based page navigation. 

12 

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 """ 

17 

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. 

28 

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 

42 

43 @property 

44 def total_pages(self) -> int: 

45 """Total number of pages.""" 

46 return (self.total + self.per_page - 1) // self.per_page 

47 

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}" 

51 

52 def render(self) -> Any: 

53 if self.total_pages <= 1: 

54 return "" 

55 

56 start = (self.page - 1) * self.per_page + 1 

57 end = min(self.total, self.page * self.per_page) 

58 

59 parts: list[Any] = [] 

60 

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 ) 

69 

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 ) 

88 

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 ) 

97 

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 ) 

116 

117 return el( 

118 "nav", 

119 *parts, 

120 class_="flex flex-wrap items-center gap-2", 

121 aria_label="Pagination", 

122 ) 

123 

124 

125__all__ = ["Pagination"]