diff --git a/pyproject.toml b/pyproject.toml
index 1915d9e..bc1b405 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -52,4 +52,4 @@ include = ["pkgmgr*"]
[tool.setuptools.package-data]
"pkgmgr.config" = ["*.yml", "*.yaml"]
-"pkgmgr.actions.review" = ["*.j2"]
+"pkgmgr.actions.review" = ["*.j2", "*.LICENSE"]
diff --git a/src/pkgmgr/actions/review/bootstrap-icons.LICENSE b/src/pkgmgr/actions/review/bootstrap-icons.LICENSE
new file mode 100644
index 0000000..f952439
--- /dev/null
+++ b/src/pkgmgr/actions/review/bootstrap-icons.LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2019-2024 The Bootstrap Authors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/src/pkgmgr/actions/review/review.html.j2 b/src/pkgmgr/actions/review/review.html.j2
index 7026ec2..f90343c 100644
--- a/src/pkgmgr/actions/review/review.html.j2
+++ b/src/pkgmgr/actions/review/review.html.j2
@@ -1,5 +1,9 @@
{% set status_letters = {"added": "A", "deleted": "D", "modified": "M", "renamed": "R", "copied": "C", "untracked": "U"} %}
{% set signs = {"add": "+", "del": "-", "ctx": "", "meta": ""} %}
+{% set icon_paths = {
+ "arrows-expand": "M1 8a.5.5 0 0 1 .5-.5h13a.5.5 0 0 1 0 1h-13A.5.5 0 0 1 1 8M7.646.146a.5.5 0 0 1 .708 0l2 2a.5.5 0 0 1-.708.708L8.5 1.707V5.5a.5.5 0 0 1-1 0V1.707L6.354 2.854a.5.5 0 1 1-.708-.708zM8 10a.5.5 0 0 1 .5.5v3.793l1.146-1.147a.5.5 0 0 1 .708.708l-2 2a.5.5 0 0 1-.708 0l-2-2a.5.5 0 0 1 .708-.708L7.5 14.293V10.5A.5.5 0 0 1 8 10",
+ "arrows-collapse": "M1 8a.5.5 0 0 1 .5-.5h13a.5.5 0 0 1 0 1h-13A.5.5 0 0 1 1 8m7-8a.5.5 0 0 1 .5.5v3.793l1.146-1.147a.5.5 0 0 1 .708.708l-2 2a.5.5 0 0 1-.708 0l-2-2a.5.5 0 1 1 .708-.708L7.5 4.293V.5A.5.5 0 0 1 8 0m-.5 11.707-1.146 1.147a.5.5 0 0 1-.708-.708l2-2a.5.5 0 0 1 .708 0l2 2a.5.5 0 0 1-.708.708L8.5 11.707V15.5a.5.5 0 0 1-1 0z",
+} %}
{% set style %}
:root {
color-scheme: light dark;
@@ -93,6 +97,7 @@ input[type="search"] { width: 260px; max-width: 100%; padding: 5px 10px; border:
input[type="search"]:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
button { padding: 5px 12px; border: 1px solid var(--border); border-radius: 6px; background: var(--subtle); color: var(--text); font: 500 13px var(--sans); cursor: pointer; }
button:hover { border-color: var(--muted); }
+button.icon { display: inline-flex; align-items: center; justify-content: center; padding: 6px 8px; }
main { padding-top: 24px; padding-bottom: 80px; }
.group { margin-bottom: 36px; }
h2 { display: flex; flex-wrap: wrap; align-items: baseline; gap: 12px; margin: 0 0 12px; font-size: 17px; font-weight: 600; }
@@ -187,6 +192,7 @@ details.commit > summary { padding: 10px 14px; }
})();
{% endset %}
{% macro stat(added, removed) %}+{{ added }}−{{ removed }}{% endmacro %}
+{% macro icon(name) %}{% endmacro %}
{% macro plural(count, word) %}{{ count }} {{ word }}{{ "" if count == 1 else "s" }}{% endmacro %}
{% macro file_block(file, open) %}
@@ -282,8 +288,8 @@ details.commit > summary { padding: 10px 14px; }
-
-
+
+
diff --git a/tests/integration/test_review.py b/tests/integration/test_review.py
index 2d436ff..21784b0 100644
--- a/tests/integration/test_review.py
+++ b/tests/integration/test_review.py
@@ -2,6 +2,7 @@ from __future__ import annotations
import io
import os
+import re
import subprocess
import tempfile
import unittest
@@ -160,6 +161,27 @@ class TestReview(unittest.TestCase):
self.assertNotIn("big 11", html)
self.assertIn("20 more lines not shown", html)
+ def test_toolbar_buttons_are_labelled_icons(self) -> None:
+ html = self._render("page.html", None, [])
+
+ for key, label in (("expand", "Expand all"), ("collapse", "Collapse all")):
+ with self.subTest(button=key):
+ button = re.search(
+ rf'',
+ html,
+ re.DOTALL,
+ )
+ self.assertIsNotNone(button)
+ attrs, inner = button.groups()
+ self.assertIn(f'title="{label}"', attrs)
+ self.assertIn(f'aria-label="{label}"', attrs)
+ self.assertRegex(
+ inner, rf'^