feat filter commits by conventional type via clickable pills

Clicking a commit-type pill (feat, fix, etc.) in commit lists navigates to /:repo/commits/?type=<type>, showing only commits matching that conventional commit prefix. When a filter is active, a pill with an x button appears above the list to clear the filter. The pill links use the same color scheme as the inline pills.

Commit
79f20758589625a8f2b3715ddbd9cdb4460c27b3
Author
GPT-5.6 Sol <kiro@amazon.com>
Author date
Committer
Marius Peter <dev@marius-peter.com>
Committer date
Changed files
lib/handlers.ml
index 39e6cf52..13c957cc 100644..100644
@@ -44,6 +44,10 @@
44 44
45 45 let handle config handler _request name = with_repository config name handler
46 46
47 Added: let handle_with_request config handler request name =
48 Added: with_repository config name (fun repository context ->
49 Added: handler request repository context)
50 Added:
47 51 let handle_id config handler _request name id =
48 52 with_repository config name (fun repository context ->
49 53 handler repository context id)
@@ -55,11 +59,12 @@
55 59 in
56 60 Views.Repo.summary context branches commits
57 61
58 Removed: let commits config repository context =
62 Added: let commits config request repository context =
59 63 let* commits =
60 64 Resolvers.Commit.recent repository config.Config.commits_max_displayed
61 65 in
62 Removed: Views.Repo.commits context commits
66 Added: let type_filter = Dream.query request "type" in
67 Added: Views.Repo.commits context ?type_filter commits
63 68
64 69 let commits_branch config repository context branch =
65 70 let* reference = Resolvers.Reference.of_id repository branch in
@@ -115,7 +120,8 @@
115 120 get Routes.root_path (root config);
116 121 get Routes.repo_root_path (Repo.handle config (Repo.summary config));
117 122 get Routes.repo_path (Repo.handle config (Repo.summary config));
118 Removed: get Routes.commits_path (Repo.handle config (Repo.commits config));
123 Added: get Routes.commits_path
124 Added: (Repo.handle_with_request config (Repo.commits config));
119 125 get Routes.commits_branch_path
120 126 (Repo.handle_id config (Repo.commits_branch config));
121 127 get Routes.commit_path (Repo.handle_id config Repo.commit_id);
lib/static/styles.css
index 15c2f2eb..6267a6e6 100644..100644
@@ -288,6 +288,36 @@
288 288 color: #f9a8d4;
289 289 }
290 290
291 Added: .filter-bar {
292 Added: display: flex;
293 Added: align-items: center;
294 Added: gap: 0.5em;
295 Added: padding: 0.5em 0.75em;
296 Added: margin-bottom: 0.5em;
297 Added: background: #222;
298 Added: border-radius: 0.25rem;
299 Added: border: 1px solid #3a3a3a;
300 Added: }
301 Added:
302 Added: .filter-clear {
303 Added: color: #999;
304 Added: text-decoration: none;
305 Added: font-size: 1.2em;
306 Added: line-height: 1;
307 Added: }
308 Added:
309 Added: .filter-clear:hover {
310 Added: color: white;
311 Added: }
312 Added:
313 Added: a.commit-pill {
314 Added: text-decoration: none;
315 Added: }
316 Added:
317 Added: a.commit-pill:hover {
318 Added: opacity: 0.8;
319 Added: }
320 Added:
291 321 .commit-title {
292 322 overflow: hidden;
293 323 text-overflow: ellipsis;
lib/views/repo.ml
index 1d81b996..16b782a8 100644..100644
@@ -118,11 +118,14 @@
118 118 let pill =
119 119 match commit_type with
120 120 | None -> HTML.null []
121 Removed: | Some commit_type ->
121 Added: | Some ct ->
122 122 HTML.(
123 Removed: span
124 Removed: [ class_ "commit-pill commit-pill-%s" commit_type ]
125 Removed: [ txt "%s" commit_type ])
123 Added: a
124 Added: [
125 Added: href "/%s/commits/?type=%s" repo ct;
126 Added: class_ "commit-pill commit-pill-%s" ct;
127 Added: ]
128 Added: [ txt "%s" ct ])
126 129 in
127 130 let title_span =
128 131 HTML.(span [ class_ "commit-title" ] [ txt "%s" commit_title ])
@@ -160,9 +163,39 @@
160 163 ul [] (List.map (li_of_commit context.repo) commits);
161 164 ]
162 165
163 Removed: let commits context commits =
166 Added: let commit_type_of (commit : Resolvers.Commit.t) =
167 Added: let message = parse_commit_message commit.message in
168 Added: fst (parse_conventional message.summary)
169 Added:
170 Added: let filter_bar context type_filter =
171 Added: HTML.(
172 Added: div
173 Added: [ class_ "filter-bar" ]
174 Added: [
175 Added: span
176 Added: [ class_ "commit-pill commit-pill-%s" type_filter ]
177 Added: [ txt "%s" type_filter ];
178 Added: a
179 Added: [
180 Added: href "/%s/commits/" context.repo;
181 Added: class_ "filter-clear";
182 Added: Aria.label "Clear filter";
183 Added: ]
184 Added: [ txt "\xc3\x97" ];
185 Added: ])
186 Added:
187 Added: let commits context ?type_filter all_commits =
188 Added: let commits =
189 Added: match type_filter with
190 Added: | None -> all_commits
191 Added: | Some ft ->
192 Added: List.filter (fun commit -> commit_type_of commit = Some ft) all_commits
193 Added: in
194 Added: let header =
195 Added: match type_filter with None -> [] | Some ft -> [ filter_bar context ft ]
196 Added: in
164 197 render_page context ~active:Commits
165 Removed: HTML.[ ul [] (List.map (li_of_commit context.repo) commits) ]
198 Added: (header @ HTML.[ ul [] (List.map (li_of_commit context.repo) commits) ])
166 199
167 200 let breadcrumbs repo (trail : (string * string) list) =
168 201 let root_link = HTML.li [] [ Routes.link_to (Files repo) (txt "Home") ] in