feat(review): icon buttons with labels for expand and collapse
The review page toolbar showed Expand all and Collapse all as text. They are now Bootstrap Icons (arrows-expand, arrows-collapse) inlined as SVG under the page's strict CSP, with title and aria-label carrying the words; the Bootstrap Icons MIT licence ships next to the template as package data. A test checks the labels, the icon classes, no visible text and no external resource. 🎯 Fix: 95 % 💥 Break: 2 % 💸 Cost: 100 % ⏱️ Time: 100 % 🛡️ Security: 100 % 🏆 Score: 93 🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 % Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9542f68f90
commit
b0f9395c68
4 files changed
+52
-3
No files matched your search
+1
-1
@@ -52,4 +52,4 @@ include = ["pkgmgr*"]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
"pkgmgr.config" = ["*.yml", "*.yaml"]
|
||||
"pkgmgr.actions.review" = ["*.j2"]
|
||||
"pkgmgr.actions.review" = ["*.j2", "*.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.
|
||||
@@ -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) %}<span class="stat"><span class="add">+{{ added }}</span><span class="del">−{{ removed }}</span></span>{% endmacro %}
|
||||
{% macro icon(name) %}<svg class="bi bi-{{ name }}" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" focusable="false"><path fill-rule="evenodd" d="{{ icon_paths[name] }}"/></svg>{% endmacro %}
|
||||
{% macro plural(count, word) %}{{ count }} {{ word }}{{ "" if count == 1 else "s" }}{% endmacro %}
|
||||
{% macro file_block(file, open) %}
|
||||
<details class="file" id="{{ file.anchor }}" data-path="{{ file.search }}"{% if open %} open{% endif %}>
|
||||
@@ -282,8 +288,8 @@ details.commit > summary { padding: 10px 14px; }
|
||||
<div class="spacer"></div>
|
||||
<input id="filter" type="search" placeholder="Filter files by path ( / )" autocomplete="off" spellcheck="false" aria-label="Filter files by path">
|
||||
<span id="filter-count" class="muted"></span>
|
||||
<button type="button" id="expand-all">Expand all</button>
|
||||
<button type="button" id="collapse-all">Collapse all</button>
|
||||
<button type="button" id="expand-all" class="icon" title="Expand all" aria-label="Expand all">{{ icon("arrows-expand") }}</button>
|
||||
<button type="button" id="collapse-all" class="icon" title="Collapse all" aria-label="Collapse all">{{ icon("arrows-collapse") }}</button>
|
||||
</div>
|
||||
</nav>
|
||||
<main>
|
||||
|
||||
@@ -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</td>", 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'<button type="button" id="{key}-all"([^>]*)>(.*?)</button>',
|
||||
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'^<svg class="bi bi-arrows-{key}"[^>]* aria-hidden="true"'
|
||||
)
|
||||
self.assertEqual(re.sub(r"<[^>]+>", "", inner).strip(), "")
|
||||
self.assertNotIn("<link", html)
|
||||
self.assertNotIn("<img", html)
|
||||
|
||||
def test_errors_exit_two(self) -> None:
|
||||
plain = os.path.join(self.root, "plain")
|
||||
os.makedirs(plain)
|
||||
|
||||
Reference in new issue
Block a user