feat add commit list pagination with 20 commits per page

The commits view now displays 20 commits per page with < and > navigation buttons at both top and bottom of the list, right-aligned. Page state is tracked via a ?page= query parameter that composes with the ?type= filter. Pagination arrows appear whenever there are more commits beyond the current page in either direction. They are hidden only when all commits fit on a single page. Orphan guard: each page fetches 11 beyond page_size to look ahead. If the next page would have fewer than 10 items, the current page absorbs them (displaying up to 30 items) and hides the next-page button, preventing navigation to a tiny last page. Disabled pagination buttons are visually dimmed and use aria-hidden. Active buttons meet the 44px minimum touch target.

Commit
9d11882d2c336c48d52d418827635d0cb4257b28
Author
Claude Sonnet 4 <ai@anthropic.com>
Author date
Committer
Claude Sonnet 4 <ai@anthropic.com>
Committer date
Changed files
lib/handlers.ml
index 180967d9..e11855e2 100644..100644
@@ -85,21 +85,54 @@
85 85 String.lowercase_ascii type_name = ct))
86 86 commits
87 87
88 Removed: let commits config request repository context =
88 Added: let page_size = 20
89 Added:
90 Added: let commits _config request repository context =
89 91 let filter_type = Dream.query request "type" in
90 Removed: let* commits =
91 Removed: Resolvers.Commit.recent repository config.Config.commits_max_displayed
92 Added: let page =
93 Added: match Dream.query request "page" with
94 Added: | None -> 1
95 Added: | Some s -> (
96 Added: match int_of_string_opt s with Some p when p > 0 -> p | _ -> 1)
92 97 in
93 Removed: let commits = filter_commits filter_type commits in
94 Removed: Views.Repo.commits ?filter_type context commits
98 Added: let offset = (page - 1) * page_size in
99 Added: (* Fetch one beyond orphan threshold to detect whether more exist *)
100 Added: let fetch_count = offset + page_size + 11 in
101 Added: let* all_commits = Resolvers.Commit.recent repository fetch_count in
102 Added: let all_commits = filter_commits filter_type all_commits in
103 Added: let total = List.length all_commits in
104 Added: let after_offset =
105 Added: if offset >= total then []
106 Added: else List.filteri (fun i _ -> i >= offset) all_commits
107 Added: in
108 Added: let remaining = List.length after_offset in
109 Added: (* If next page would have <10 items, absorb them; otherwise normal page *)
110 Added: let effective_size =
111 Added: if remaining > page_size && remaining <= page_size + 10 then remaining
112 Added: else page_size
113 Added: in
114 Added: let page_commits =
115 Added: List.filteri (fun i _ -> i < effective_size) after_offset
116 Added: in
117 Added: let has_next = remaining > effective_size in
118 Added: let has_prev = page > 1 in
119 Added: Views.Repo.commits ?filter_type ~page ~has_prev ~has_next context
120 Added: page_commits
95 121
96 Removed: let commits_branch config repository context branch =
122 Added: let commits_branch _config repository context branch =
97 123 let* reference = Resolvers.Reference.of_id repository branch in
124 Added: let fetch_count = page_size + 11 in
98 125 let* commits =
99 Removed: Resolvers.Commit.recent_from repository reference.hash
100 Removed: config.Config.commits_max_displayed
126 Added: Resolvers.Commit.recent_from repository reference.hash fetch_count
101 127 in
102 Removed: Views.Repo.commits context commits
128 Added: let remaining = List.length commits in
129 Added: let effective_size =
130 Added: if remaining > page_size && remaining <= page_size + 10 then remaining
131 Added: else page_size
132 Added: in
133 Added: let page_commits = List.filteri (fun i _ -> i < effective_size) commits in
134 Added: let has_next = remaining > effective_size in
135 Added: Views.Repo.commits ~page:1 ~has_prev:false ~has_next context page_commits
103 136
104 137 let commit_id repository context id =
105 138 let* commit = Resolvers.Commit.of_id repository id in
lib/static/styles.css
index 14294d2b..e8120766 100644..100644
@@ -396,6 +396,48 @@
396 396 color: white;
397 397 }
398 398
399 Added: /* Pagination controls */
400 Added:
401 Added: .pagination {
402 Added: display: flex;
403 Added: align-items: center;
404 Added: justify-content: flex-end;
405 Added: gap: 0.5em;
406 Added: padding: 0.5em 0;
407 Added: }
408 Added:
409 Added: .pagination-btn {
410 Added: display: inline-flex;
411 Added: align-items: center;
412 Added: justify-content: center;
413 Added: min-width: 44px;
414 Added: min-height: 44px;
415 Added: padding: 0.4em 0.75em;
416 Added: border-radius: 0.25rem;
417 Added: font-size: 1.1em;
418 Added: font-weight: 600;
419 Added: text-decoration: none;
420 Added: color: white;
421 Added: background-color: #2a2a2a;
422 Added: box-sizing: border-box;
423 Added: }
424 Added:
425 Added: .pagination-btn:hover {
426 Added: background-color: white;
427 Added: color: black;
428 Added: }
429 Added:
430 Added: .pagination-disabled {
431 Added: color: #555;
432 Added: background-color: #1e1e1e;
433 Added: cursor: default;
434 Added: }
435 Added:
436 Added: .pagination-disabled:hover {
437 Added: color: #555;
438 Added: background-color: #1e1e1e;
439 Added: }
440 Added:
399 441 h1 {
400 442 padding: 0;
401 443 }
lib/views/repo.ml
index 2ed1c2b8..7af65471 100644..100644
@@ -187,7 +187,7 @@
187 187 HTML.(
188 188 div [ class_ "toolbar"; role `toolbar; Aria.label "View toolbar" ] children)
189 189
190 Removed: let commits ?filter_type context commits =
190 Added: let commits ?filter_type ~page ~has_prev ~has_next context commits =
191 191 let filter =
192 192 match filter_type with
193 193 | None -> None
@@ -197,8 +197,63 @@
197 197 let tb =
198 198 match filter with None -> HTML.null [] | Some _ -> toolbar ?filter []
199 199 in
200 Added: let page_url p =
201 Added: let base = Printf.sprintf "/%s/commits/" context.repo in
202 Added: let params =
203 Added: (if p > 1 then [ Printf.sprintf "page=%d" p ] else [])
204 Added: @
205 Added: match filter_type with
206 Added: | Some ct -> [ Printf.sprintf "type=%s" ct ]
207 Added: | None -> []
208 Added: in
209 Added: match params with [] -> base | _ -> base ^ "?" ^ String.concat "&" params
210 Added: in
211 Added: let show_pagination = has_prev || has_next in
212 Added: let pagination =
213 Added: if not show_pagination then HTML.null []
214 Added: else
215 Added: HTML.(
216 Added: nav
217 Added: [ class_ "pagination"; Aria.label "Pagination" ]
218 Added: [
219 Added: (if has_prev then
220 Added: a
221 Added: [
222 Added: href "%s" (page_url (page - 1));
223 Added: class_ "pagination-btn";
224 Added: Aria.label "Previous page";
225 Added: ]
226 Added: [ txt "<" ]
227 Added: else
228 Added: span
229 Added: [
230 Added: class_ "pagination-btn pagination-disabled"; Aria.hidden true;
231 Added: ]
232 Added: [ txt "<" ]);
233 Added: (if has_next then
234 Added: a
235 Added: [
236 Added: href "%s" (page_url (page + 1));
237 Added: class_ "pagination-btn";
238 Added: Aria.label "Next page";
239 Added: ]
240 Added: [ txt ">" ]
241 Added: else
242 Added: span
243 Added: [
244 Added: class_ "pagination-btn pagination-disabled"; Aria.hidden true;
245 Added: ]
246 Added: [ txt ">" ]);
247 Added: ])
248 Added: in
200 249 render_page context ~active:Commits
201 Removed: HTML.[ tb; ul [] (List.map (li_of_commit ~hide_pill context.repo) commits) ]
250 Added: HTML.
251 Added: [
252 Added: tb;
253 Added: pagination;
254 Added: ul [] (List.map (li_of_commit ~hide_pill context.repo) commits);
255 Added: pagination;
256 Added: ]
202 257
203 258 let breadcrumbs repo (trail : (string * string) list) =
204 259 let root_link = HTML.li [] [ Routes.link_to (Files repo) (txt "Home") ] in