Coverage for src/lexigram/admin/ui/organisms/pagination.py: 21%
28 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-24 23:39 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-24 23:39 +0800
1from __future__ import annotations
3from typing import Any
5from lexigram.ui import (
6 Component,
7 JumpToPage,
8 Link,
9 PageSizeSelector,
10 PaginationLinks,
11 Zones,
12 el,
13)
16class Pagination(Component):
17 """
18 Standalone pagination component with page size selector and jump to page.
19 Compiles molecules into a functional organism.
20 Uses Zone-based targeting for consistent HTMX behavior.
21 """
23 def __init__(
24 self,
25 page: int = 1,
26 total: int = 0,
27 per_page: int = 20,
28 base_url: str = "",
29 show_size_selector: bool = True,
30 show_jump_to_page: bool = True,
31 extra_query: str = "",
32 hx_target: str | None = None,
33 hx_swap: str = "innerHTML",
34 hx_push_url: str = "true",
35 next_cursor: str | None = None,
36 state: Any | None = None,
37 **props,
38 ) -> None:
39 super().__init__(
40 page=page,
41 total=total,
42 per_page=per_page,
43 base_url=base_url,
44 **props,
45 )
46 self.state = state
47 self.page = page
48 self.total = total
49 self.per_page = per_page
50 self.base_url = base_url
51 self.next_cursor = next_cursor
52 self.show_size_selector = show_size_selector
53 self.show_jump_to_page = show_jump_to_page
54 self.extra_query = extra_query
55 # Use Zones.DATA by default
56 self.hx_target = hx_target or Zones.DATA.selector
57 self.hx_swap = hx_swap
58 self.hx_push_url = hx_push_url
60 def render(self) -> Any:
61 if self.total <= 0:
62 return ""
64 total_pages = (self.total + self.per_page - 1) // self.per_page
66 start_item = (self.page - 1) * self.per_page + 1
67 end_item = min(self.page * self.per_page, self.total)
69 # Render Modular Components
70 size_selector = (
71 PageSizeSelector(
72 per_page=self.per_page,
73 base_url=self.base_url,
74 extra_query=self.extra_query,
75 hx_target=self.hx_target,
76 hx_swap=self.hx_swap,
77 hx_push_url=self.hx_push_url,
78 state=self.state,
79 )
80 if self.show_size_selector
81 else ""
82 )
84 jump_to_page = (
85 JumpToPage(
86 page=self.page,
87 total_pages=total_pages,
88 per_page=self.per_page,
89 base_url=self.base_url,
90 extra_query=self.extra_query,
91 hx_target=self.hx_target,
92 hx_swap=self.hx_swap,
93 hx_push_url=self.hx_push_url,
94 state=self.state,
95 )
96 if self.show_jump_to_page
97 else ""
98 )
100 page_links = PaginationLinks(
101 page=self.page,
102 total_pages=total_pages,
103 per_page=self.per_page,
104 base_url=self.base_url,
105 extra_query=self.extra_query,
106 hx_target=self.hx_target,
107 hx_swap=self.hx_swap,
108 hx_push_url=self.hx_push_url,
109 state=self.state,
110 )
112 return el(
113 "div",
114 {
115 "id": "table-pagination",
116 "class": "flex items-center justify-between border-t border-border bg-background px-4 py-3 mt-4",
117 },
118 # Mobile View
119 el(
120 "div",
121 {"class": "flex flex-1 justify-between sm:hidden"},
122 # Previous (mobile). Render as span when disabled to avoid HTMX attributes on inactive controls.
123 (
124 el(
125 "span",
126 "Previous",
127 class_=f"relative inline-flex items-center rounded-md border border-border bg-card px-4 py-2 text-sm font-medium text-foreground {'invisible' if self.page <= 1 else ''}",
128 )
129 if self.page <= 1
130 else Link(
131 "Previous",
132 f"{self.base_url}?page={max(1, self.page - 1)}&per_page={self.per_page}{self.extra_query}",
133 color="muted",
134 class_=f"relative inline-flex items-center rounded-md border border-border bg-card px-4 py-2 text-sm font-medium text-foreground hover:bg-muted {'invisible' if self.page <= 1 else ''}",
135 hx_get=f"{self.base_url}?page={max(1, self.page - 1)}&per_page={self.per_page}{self.extra_query}",
136 hx_trigger="click",
137 hx_target=self.hx_target,
138 hx_swap=self.hx_swap,
139 hx_push_url=self.hx_push_url,
140 hx_include="this",
141 onclick="return false",
142 preload="mouseover",
143 )
144 ),
145 # Next (mobile). Render as span when disabled to avoid HTMX attributes on inactive controls.
146 (
147 el(
148 "span",
149 "Next",
150 class_=f"relative ml-3 inline-flex items-center rounded-md border border-border bg-card px-4 py-2 text-sm font-medium text-foreground {'invisible' if not self.next_cursor and self.page >= total_pages else ''}",
151 )
152 if (not self.next_cursor and self.page >= total_pages)
153 else Link(
154 "Next",
155 f"{self.base_url}?cursor={self.next_cursor}&per_page={self.per_page}{self.extra_query}"
156 if self.next_cursor
157 else f"{self.base_url}?page={min(total_pages, self.page + 1)}&per_page={self.per_page}{self.extra_query}",
158 class_=f"relative ml-3 inline-flex items-center rounded-md border border-border bg-card px-4 py-2 text-sm font-medium text-foreground hover:bg-muted {'invisible' if not self.next_cursor and self.page >= total_pages else ''}",
159 hx_get=f"{self.base_url}?cursor={self.next_cursor}&per_page={self.per_page}{self.extra_query}"
160 if self.next_cursor
161 else f"{self.base_url}?page={min(total_pages, self.page + 1)}&per_page={self.per_page}{self.extra_query}",
162 hx_trigger="click",
163 hx_target=self.hx_target,
164 hx_swap=self.hx_swap,
165 hx_push_url=self.hx_push_url,
166 hx_include="this",
167 onclick="return false",
168 preload="mouseover",
169 )
170 ),
171 ),
172 # Desktop View
173 el(
174 "div",
175 {
176 "class": "hidden sm:flex sm:flex-1 sm:items-center sm:justify-between px-4",
177 },
178 # Results text
179 el(
180 "div",
181 el(
182 "p",
183 "Showing ",
184 el("span", str(start_item), class_="font-bold"),
185 " to ",
186 el("span", str(end_item), class_="font-bold"),
187 " of ",
188 el("span", str(self.total), class_="font-bold"),
189 " results",
190 class_="text-[11px] uppercase tracking-wider text-muted-foreground font-semibold",
191 ),
192 ),
193 # Pagination Controls
194 el(
195 "div",
196 page_links,
197 size_selector,
198 jump_to_page,
199 class_="flex items-center space-x-4",
200 ),
201 ),
202 )