feat add commit pill filtering with toolbar component

Clicking a commit pill navigates to the commits list filtered by that conventional commit type via a ?type= query parameter. A persistent toolbar component appears in both the commits and files views. In commits, it displays the active filter pill with an adjacent small round dismiss button. In files, it wraps the breadcrumbs. The filter dismiss button is a small grey circle (×) close to the pill, with an expanded touch target via pseudo-element for accessibility.

Commit
ae6963f5d6853de9b8220d328464a4904f6b7ea8
Author
Claude Sonnet 4 <ai@anthropic.com>
Author date
Committer
Claude Sonnet 4 <ai@anthropic.com>
Committer date
Changed files
lib/handlers.ml
index 39e6cf52..180967d9 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_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,39 @@
55 59 in
56 60 Views.Repo.summary context branches commits
57 61
58 Removed: let commits config repository context =
62 Added: let filter_commits filter_type commits =
63 Added: match filter_type with
64 Added: | None -> commits
65 Added: | Some ct ->
66 Added: List.filter
67 Added: (fun (commit : Resolvers.Commit.t) ->
68 Added: match commit.message with
69 Added: | None -> false
70 Added: | Some message -> (
71 Added: let summary =
72 Added: match String.split_on_char '\n' message with
73 Added: | [] -> ""
74 Added: | s :: _ -> s
75 Added: in
76 Added: match String.index_opt summary ':' with
77 Added: | None -> false
78 Added: | Some colon_pos ->
79 Added: let prefix = String.sub summary 0 colon_pos in
80 Added: let type_name =
81 Added: match String.index_opt prefix '(' with
82 Added: | Some paren_pos -> String.sub prefix 0 paren_pos
83 Added: | None -> prefix
84 Added: in
85 Added: String.lowercase_ascii type_name = ct))
86 Added: commits
87 Added:
88 Added: let commits config request repository context =
89 Added: let filter_type = Dream.query request "type" in
59 90 let* commits =
60 91 Resolvers.Commit.recent repository config.Config.commits_max_displayed
61 92 in
62 Removed: Views.Repo.commits context commits
93 Added: let commits = filter_commits filter_type commits in
94 Added: Views.Repo.commits ?filter_type context commits
63 95
64 96 let commits_branch config repository context branch =
65 97 let* reference = Resolvers.Reference.of_id repository branch in
@@ -115,7 +147,7 @@
115 147 get Routes.root_path (root config);
116 148 get Routes.repo_root_path (Repo.handle config (Repo.summary config));
117 149 get Routes.repo_path (Repo.handle config (Repo.summary config));
118 Removed: get Routes.commits_path (Repo.handle config (Repo.commits config));
150 Added: get Routes.commits_path (Repo.handle_request config (Repo.commits config));
119 151 get Routes.commits_branch_path
120 152 (Repo.handle_id config (Repo.commits_branch config));
121 153 get Routes.commit_path (Repo.handle_id config Repo.commit_id);
lib/static/styles.css
index 7400865a..b0924116 100644..100644
@@ -337,6 +337,52 @@
337 337 color: #fbb6d0;
338 338 }
339 339
340 Added: /* Toolbar — persistent bar between navbar and content in commits/files views */
341 Added:
342 Added: .toolbar {
343 Added: display: flex;
344 Added: align-items: center;
345 Added: gap: 0.75em;
346 Added: min-height: 44px;
347 Added: box-sizing: border-box;
348 Added: }
349 Added:
350 Added: .toolbar-filter {
351 Added: display: inline-flex;
352 Added: align-items: center;
353 Added: gap: 0.35em;
354 Added: }
355 Added:
356 Added: .toolbar-dismiss {
357 Added: position: relative;
358 Added: display: inline-flex;
359 Added: align-items: center;
360 Added: justify-content: center;
361 Added: width: 1.2em;
362 Added: height: 1.2em;
363 Added: padding: 0;
364 Added: color: #b0b0b0;
365 Added: background-color: #3a3a3a;
366 Added: text-decoration: none;
367 Added: font-size: 0.8em;
368 Added: line-height: 1;
369 Added: border-radius: 50%;
370 Added: box-sizing: border-box;
371 Added: }
372 Added:
373 Added: .toolbar-dismiss::before {
374 Added: content: "";
375 Added: position: absolute;
376 Added: inset: -10px;
377 Added: min-width: 44px;
378 Added: min-height: 44px;
379 Added: }
380 Added:
381 Added: .toolbar-dismiss:hover {
382 Added: background-color: #555;
383 Added: color: white;
384 Added: }
385 Added:
340 386 h1 {
341 387 padding: 0;
342 388 }
lib/views/repo.ml
index be0f08fa..2f1519cd 100644..100644
@@ -123,7 +123,7 @@
123 123 HTML.(
124 124 span
125 125 [ class_ "commit-pill commit-pill-%s" ct ]
126 Removed: [ Routes.link_to commit_route (txt "%s" ct) ])
126 Added: [ a [ href "/%s/commits/?type=%s" repo ct ] [ txt "%s" ct ] ])
127 127 in
128 128 let title_span =
129 129 HTML.(
@@ -161,9 +161,40 @@
161 161 ul [] (List.map (li_of_commit context.repo) commits);
162 162 ]
163 163
164 Removed: let commits context commits =
164 Added: let toolbar ?(filter : (string * string) option) content =
165 Added: let filter_el =
166 Added: match filter with
167 Added: | None -> HTML.null []
168 Added: | Some (commit_type, dismiss_href) ->
169 Added: HTML.(
170 Added: span
171 Added: [ class_ "toolbar-filter" ]
172 Added: [
173 Added: span
174 Added: [ class_ "commit-pill commit-pill-%s" commit_type ]
175 Added: [ txt "%s" commit_type ];
176 Added: a
177 Added: [
178 Added: href "%s" dismiss_href;
179 Added: class_ "toolbar-dismiss";
180 Added: Aria.label "Remove filter";
181 Added: ]
182 Added: [ txt "\xc3\x97" ];
183 Added: ])
184 Added: in
185 Added: let children = content @ [ filter_el ] in
186 Added: HTML.(
187 Added: div [ class_ "toolbar"; role `toolbar; Aria.label "View toolbar" ] children)
188 Added:
189 Added: let commits ?filter_type context commits =
190 Added: let filter =
191 Added: match filter_type with
192 Added: | None -> None
193 Added: | Some ct -> Some (ct, Printf.sprintf "/%s/commits/" context.repo)
194 Added: in
195 Added: let tb = toolbar ?filter [] in
165 196 render_page context ~active:Commits
166 Removed: HTML.[ ul [] (List.map (li_of_commit context.repo) commits) ]
197 Added: HTML.[ tb; ul [] (List.map (li_of_commit context.repo) commits) ]
167 198
168 199 let breadcrumbs repo (trail : (string * string) list) =
169 200 let root_link = HTML.li [] [ Routes.link_to (Files repo) (txt "Home") ] in
@@ -179,12 +210,10 @@
179 210 [ ul [] (root_link :: crumbs) ])
180 211
181 212 let files context trail (tree : Resolvers.Tree.t) =
213 Added: let bc = breadcrumbs context.repo trail in
214 Added: let tb = toolbar [ bc ] in
182 215 render_page context ~active:Files
183 Removed: HTML.
184 Removed: [
185 Removed: breadcrumbs context.repo trail;
186 Removed: ul [] (List.map (li_of_entry context.repo) tree.entries);
187 Removed: ]
216 Added: HTML.[ tb; ul [] (List.map (li_of_entry context.repo) tree.entries) ]
188 217
189 218 let file ?(active = Layout.Files) context trail (blob : Resolvers.Blob.t) =
190 219 let to_numbered_line number line =
@@ -207,7 +236,11 @@
207 236 |> List.mapi to_numbered_line |> List.concat
208 237 in
209 238 render_page context ~active
210 Removed: HTML.[ breadcrumbs context.repo trail; div [ id "blob" ] formatted_blob ]
239 Added: HTML.
240 Added: [
241 Added: toolbar [ breadcrumbs context.repo trail ];
242 Added: div [ id "blob" ] formatted_blob;
243 Added: ]
211 244
212 245 let commit context (commit : Resolvers.Commit.t) diff =
213 246 let message = parse_commit_message commit.message in