@govtechsg/oobee 0.10.70 → 0.10.72

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/DETAILS.md +0 -1
  2. package/README.md +12 -0
  3. package/S3_UPLOAD_README.md +172 -0
  4. package/dev/runGenerateJustHtmlReport.ts +25 -0
  5. package/package.json +4 -2
  6. package/src/combine.ts +71 -14
  7. package/src/constants/common.ts +89 -91
  8. package/src/constants/constants.ts +534 -59
  9. package/src/crawlers/crawlDomain.ts +313 -305
  10. package/src/crawlers/crawlIntelligentSitemap.ts +24 -18
  11. package/src/crawlers/crawlLocalFile.ts +29 -27
  12. package/src/crawlers/crawlSitemap.ts +264 -253
  13. package/src/crawlers/custom/utils.ts +809 -119
  14. package/src/crawlers/runCustom.ts +29 -4
  15. package/src/generateHtmlReport.ts +224 -0
  16. package/src/mergeAxeResults.ts +94 -44
  17. package/src/runGenerateJustHtmlReport.ts +20 -0
  18. package/src/services/s3Uploader.ts +184 -0
  19. package/src/static/ejs/partials/components/allIssues/AllIssues.ejs +9 -0
  20. package/src/static/ejs/partials/components/allIssues/CategoryBadges.ejs +82 -0
  21. package/src/static/ejs/partials/components/allIssues/FilterBar.ejs +33 -0
  22. package/src/static/ejs/partials/components/allIssues/IssuesTable.ejs +41 -0
  23. package/src/static/ejs/partials/components/header/SiteInfo.ejs +119 -0
  24. package/src/static/ejs/partials/components/header/aboutScanModal/AboutScanModal.ejs +15 -0
  25. package/src/static/ejs/partials/components/header/aboutScanModal/ScanConfiguration.ejs +44 -0
  26. package/src/static/ejs/partials/components/header/aboutScanModal/ScanDetails.ejs +142 -0
  27. package/src/static/ejs/partials/components/prioritiseIssues/IssueDetailCard.ejs +36 -0
  28. package/src/static/ejs/partials/components/prioritiseIssues/PrioritiseIssues.ejs +47 -0
  29. package/src/static/ejs/partials/components/ruleModal/ruleOffcanvas.ejs +196 -0
  30. package/src/static/ejs/partials/components/scannedPagesSegmentedTabs.ejs +48 -0
  31. package/src/static/ejs/partials/components/shared/InfoAlert.ejs +3 -0
  32. package/src/static/ejs/partials/components/{topFive.ejs → topTen.ejs} +2 -2
  33. package/src/static/ejs/partials/components/wcagCompliance/FailedCriteria.ejs +47 -0
  34. package/src/static/ejs/partials/components/wcagCompliance/WcagCompliance.ejs +16 -0
  35. package/src/static/ejs/partials/components/wcagCompliance/WcagGaugeBar.ejs +16 -0
  36. package/src/static/ejs/partials/components/wcagCoverageDetails.ejs +18 -0
  37. package/src/static/ejs/partials/footer.ejs +1 -1
  38. package/src/static/ejs/partials/header.ejs +7 -223
  39. package/src/static/ejs/partials/main.ejs +12 -23
  40. package/src/static/ejs/partials/scripts/allIssues/AllIssues.ejs +376 -0
  41. package/src/static/ejs/partials/scripts/categorySummary.ejs +1 -1
  42. package/src/static/ejs/partials/scripts/header/SiteInfo.ejs +44 -0
  43. package/src/static/ejs/partials/scripts/header/aboutScanModal/AboutScanModal.ejs +51 -0
  44. package/src/static/ejs/partials/scripts/header/aboutScanModal/ScanConfiguration.ejs +127 -0
  45. package/src/static/ejs/partials/scripts/header/aboutScanModal/ScanDetails.ejs +60 -0
  46. package/src/static/ejs/partials/scripts/prioritiseIssues/IssueDetailCard.ejs +137 -0
  47. package/src/static/ejs/partials/scripts/prioritiseIssues/PrioritiseIssues.ejs +214 -0
  48. package/src/static/ejs/partials/scripts/prioritiseIssues/wcagSvgMap.ejs +861 -0
  49. package/src/static/ejs/partials/scripts/ruleModal/constants.ejs +949 -0
  50. package/src/static/ejs/partials/scripts/ruleModal/itemCardRenderer.ejs +352 -0
  51. package/src/static/ejs/partials/scripts/ruleModal/pageAccordionBuilder.ejs +468 -0
  52. package/src/static/ejs/partials/scripts/ruleModal/ruleOffcanvas.ejs +306 -0
  53. package/src/static/ejs/partials/scripts/ruleModal/utilities.ejs +483 -0
  54. package/src/static/ejs/partials/scripts/scannedPagesSegmentedTabs.ejs +35 -0
  55. package/src/static/ejs/partials/scripts/screenshotLightbox.ejs +61 -57
  56. package/src/static/ejs/partials/scripts/topTen.ejs +61 -0
  57. package/src/static/ejs/partials/scripts/utils.ejs +15 -0
  58. package/src/static/ejs/partials/scripts/wcagCompliance/FailedCriteria.ejs +103 -0
  59. package/src/static/ejs/partials/scripts/wcagCompliance/WcagGaugeBar.ejs +47 -0
  60. package/src/static/ejs/partials/scripts/wcagCompliance.ejs +15 -0
  61. package/src/static/ejs/partials/scripts/wcagCoverageDetails.ejs +75 -0
  62. package/src/static/ejs/partials/styles/allIssues/AllIssues.ejs +384 -0
  63. package/src/static/ejs/partials/styles/bootstrap.ejs +17 -1
  64. package/src/static/ejs/partials/styles/header/SiteInfo.ejs +121 -0
  65. package/src/static/ejs/partials/styles/header/aboutScanModal/AboutScanModal.ejs +82 -0
  66. package/src/static/ejs/partials/styles/header/aboutScanModal/ScanConfiguration.ejs +50 -0
  67. package/src/static/ejs/partials/styles/header/aboutScanModal/ScanDetails.ejs +149 -0
  68. package/src/static/ejs/partials/styles/header.ejs +7 -0
  69. package/src/static/ejs/partials/styles/prioritiseIssues/IssueDetailCard.ejs +141 -0
  70. package/src/static/ejs/partials/styles/prioritiseIssues/PrioritiseIssues.ejs +204 -0
  71. package/src/static/ejs/partials/styles/ruleModal/ruleOffcanvas.ejs +456 -0
  72. package/src/static/ejs/partials/styles/scannedPagesSegmentedTabs.ejs +46 -0
  73. package/src/static/ejs/partials/styles/shared/InfoAlert.ejs +12 -0
  74. package/src/static/ejs/partials/styles/styles.ejs +198 -470
  75. package/src/static/ejs/partials/styles/topTenCard.ejs +44 -0
  76. package/src/static/ejs/partials/styles/wcagCompliance/FailedCriteria.ejs +59 -0
  77. package/src/static/ejs/partials/styles/wcagCompliance/WcagGaugeBar.ejs +62 -0
  78. package/src/static/ejs/partials/styles/wcagCompliance.ejs +36 -0
  79. package/src/static/ejs/partials/styles/wcagCoverageDetails.ejs +33 -0
  80. package/src/static/ejs/report.ejs +42 -259
  81. package/src/static/ejs/summary.ejs +1 -1
  82. package/src/utils.ts +30 -0
  83. package/src/static/ejs/partials/components/categorySelector.ejs +0 -4
  84. package/src/static/ejs/partials/components/categorySelectorDropdown.ejs +0 -57
  85. package/src/static/ejs/partials/components/pagesScannedModal.ejs +0 -70
  86. package/src/static/ejs/partials/components/reportSearch.ejs +0 -47
  87. package/src/static/ejs/partials/components/ruleOffcanvas.ejs +0 -105
  88. package/src/static/ejs/partials/components/scanAbout.ejs +0 -328
  89. package/src/static/ejs/partials/components/wcagCompliance.ejs +0 -52
  90. package/src/static/ejs/partials/scripts/categorySelectorDropdownScript.ejs +0 -190
  91. package/src/static/ejs/partials/scripts/reportSearch.ejs +0 -287
  92. package/src/static/ejs/partials/scripts/ruleOffcanvas.ejs +0 -804
  93. package/src/static/ejs/partials/scripts/scanAboutScript.ejs +0 -38
@@ -0,0 +1,61 @@
1
+ <script>
2
+ function createExternalIcon() {
3
+ const ns = "http://www.w3.org/2000/svg";
4
+ const svg = document.createElementNS(ns, "svg");
5
+ svg.setAttribute("width", "16");
6
+ svg.setAttribute("height", "12");
7
+ svg.setAttribute("viewBox", "0 0 8 8");
8
+ svg.setAttribute("aria-hidden", "true");
9
+ svg.setAttribute("focusable", "false");
10
+
11
+ const path = document.createElementNS(ns, "path");
12
+ path.setAttribute(
13
+ "d",
14
+ "M7.11111 7.11111H0.888889V0.888889H4V0H0.888889C0.395556 0 0 0.4 0 0.888889V7.11111C0 7.6 0.395556 8 0.888889 8H7.11111C7.6 8 8 7.6 8 7.11111V4H7.11111V7.11111ZM4.88889 0V0.888889H6.48444L2.11556 5.25778L2.74222 5.88444L7.11111 1.51556V3.11111H8V0H4.88889Z"
15
+ );
16
+ path.setAttribute("fill", "#5735DF");
17
+ svg.appendChild(path);
18
+ return svg;
19
+ }
20
+
21
+ const scanDataTop10Card = () => {
22
+ const topIssuesList = document.getElementById('top-issues-list');
23
+
24
+ scanData.topTenPagesWithMostIssues.forEach(page => {
25
+ if (page.totalIssues !== 0) {
26
+ const listItem = document.createElement('li');
27
+ listItem.className = 'd-flex justify-content-between';
28
+
29
+ const link = document.createElement('a');
30
+ link.href = page.url;
31
+ link.target = '_blank';
32
+ link.rel = 'noopener';
33
+ link.className = 'page-links';
34
+
35
+ const text = page.pageTitle.length > 0 ? page.pageTitle : page.url;
36
+ const textSpan = document.createElement('span');
37
+ textSpan.className = 'link-text';
38
+ textSpan.textContent = text;
39
+
40
+ link.title = text;
41
+
42
+ link.appendChild(textSpan);
43
+ link.appendChild(createExternalIcon());
44
+
45
+ const issuesSpan = document.createElement('span');
46
+ issuesSpan.className = 'ms-4 issues-badge';
47
+ const issues = Number(page.totalIssues) || 0;
48
+ const occ = Number(page.totalOccurrences) || 0;
49
+ const issueWord = issues === 1 ? 'issue' : 'issues';
50
+ const occWord = occ === 1 ? 'occurrence' : 'occurrences';
51
+ issuesSpan.textContent = `${issues} ${issueWord} (${occ} occ.)`;
52
+
53
+ listItem.appendChild(link);
54
+ listItem.appendChild(issuesSpan);
55
+ topIssuesList.appendChild(listItem);
56
+ }
57
+ });
58
+ };
59
+
60
+ scanDataTop10Card();
61
+ </script>
@@ -421,6 +421,7 @@
421
421
  function escapeHtmlStringForArg(string) {
422
422
  return htmlEscapeString(string)
423
423
  .replaceAll('&#039;', "\\'")
424
+ .replace(/\r?\n/g, '\\n')
424
425
  .replace(/&gt;\s+(.*?)\s+&lt;\//g, '&gt;$1&lt;/');
425
426
  }
426
427
 
@@ -435,4 +436,18 @@
435
436
  .replace(/"/g, '&quot;')
436
437
  .replace(/'/g, '&#039;');
437
438
  }
439
+
440
+ // Format string from wcag111 to WCAG 1.1.1
441
+ function formatWcagId(wcag) {
442
+ if (!wcag) return '';
443
+ const numbers = wcag.replace('wcag', '').split('');
444
+ if (numbers.length === 3) {
445
+ return `WCAG ${numbers[0]}.${numbers[1]}.${numbers[2]}`;
446
+ } else if (numbers.length === 4) {
447
+ return `WCAG ${numbers[0]}.${numbers[1]}.${numbers[2]}${numbers[3]}`;
448
+ } else if (numbers.length === 5) {
449
+ return `WCAG ${numbers[0]}.${numbers[1]}.${numbers.slice(2).join('')}`;
450
+ }
451
+ return wcag;
452
+ }
438
453
  </script>
@@ -0,0 +1,103 @@
1
+ <script>
2
+ (function populateFailedCriteria() {
3
+ const wcagLinks = scanData?.wcagLinks || {};
4
+ const wcagViolations = scanData?.wcagViolations || [];
5
+ const wcagClauses = scanData?.wcagClauses || {};
6
+ const wcagCriteriaLabels = scanData?.wcagCriteriaLabels || {};
7
+
8
+ const failedCriteria = [];
9
+
10
+ wcagViolations.forEach(violation => {
11
+ const formattedId = formatWcagId(violation);
12
+
13
+ const level = wcagCriteriaLabels[formattedId];
14
+
15
+ if (level === 'AAA') {
16
+ return;
17
+ }
18
+
19
+ const wcagInfo = wcagLinks[formattedId];
20
+
21
+ if (wcagInfo) {
22
+ const clauseKey = formattedId.replace('WCAG ', '');
23
+
24
+ failedCriteria.push({
25
+ id: formattedId,
26
+ name: wcagClauses[clauseKey] || formattedId,
27
+ });
28
+ }
29
+ });
30
+
31
+ console.log('wcagViolations 111', wcagViolations)
32
+
33
+ failedCriteria.sort((a, b) => {
34
+ const aNum = a.id.replace('WCAG ', '').split('.').map(Number);
35
+ const bNum = b.id.replace('WCAG ', '').split('.').map(Number);
36
+
37
+ for (let i = 0; i < Math.max(aNum.length, bNum.length); i++) {
38
+ if ((aNum[i] || 0) !== (bNum[i] || 0)) {
39
+ return (aNum[i] || 0) - (bNum[i] || 0);
40
+ }
41
+ }
42
+ return 0;
43
+ });
44
+
45
+ const countElement = document.getElementById('failedCriteriaCount');
46
+ if (countElement) {
47
+ countElement.textContent = failedCriteria.length;
48
+ }
49
+
50
+ const modalCountElement = document.getElementById('failedCriteriaModalCount');
51
+ if (modalCountElement) {
52
+ modalCountElement.textContent = failedCriteria.length;
53
+ }
54
+
55
+ const listElement = document.getElementById('failedCriteriaList');
56
+ const cardElement = document.getElementById('failedCriteriaCard');
57
+
58
+ if (listElement && failedCriteria.length > 0) {
59
+ const maxDisplay = 5;
60
+ const displayCriteria = failedCriteria.slice(0, maxDisplay);
61
+
62
+ const listHTML = displayCriteria
63
+ .map(
64
+ criteria => `
65
+ <li class="d-flex justify-content-between">
66
+ <span>${criteria.id} – ${criteria.name}</span>
67
+ </li>
68
+ `,
69
+ )
70
+ .join('');
71
+
72
+ listElement.innerHTML = listHTML;
73
+
74
+ // Populate modal with all criteria
75
+ const modalListElement = document.getElementById('failedCriteriaModalList');
76
+ if (modalListElement) {
77
+ const allListHTML = failedCriteria
78
+ .map(
79
+ criteria => `
80
+ <li class="d-flex justify-content-between">
81
+ <span>${criteria.id} – ${criteria.name}</span>
82
+ </li>
83
+ `,
84
+ )
85
+ .join('');
86
+
87
+ modalListElement.innerHTML = allListHTML;
88
+ }
89
+
90
+ // Show "View all" button if there are more than maxDisplay criteria
91
+ if (failedCriteria.length > maxDisplay) {
92
+ const viewAllButton = document.getElementById('viewAllFailedCriteria');
93
+ if (viewAllButton) {
94
+ viewAllButton.hidden = false;
95
+ }
96
+ }
97
+ } else {
98
+ if (cardElement) {
99
+ cardElement.style.display = 'none';
100
+ }
101
+ }
102
+ })();
103
+ </script>
@@ -0,0 +1,47 @@
1
+ <script>
2
+ function renderWcagGaugeFromScanData() {
3
+ const wcagTotals = scanData?.wcagPassPercentage ?? {};
4
+ const totalChecks = Number(wcagTotals.totalWcagChecksAA ?? 0);
5
+ const violations = Number(wcagTotals.totalWcagViolationsAA ?? 0);
6
+ const checksPassed = Math.max(0, totalChecks - violations);
7
+
8
+ const numEl = document.getElementById('gaugeValueNumber');
9
+ const totalEl = document.getElementById('gaugeValueTotal');
10
+ const targetEl = document.getElementById('gaugeTarget');
11
+
12
+ if (numEl) numEl.textContent = String(checksPassed);
13
+ if (totalEl) totalEl.textContent = String(totalChecks);
14
+ if (targetEl) targetEl.textContent = String(totalChecks);
15
+
16
+ const gaugeFillPath = document.getElementById('gaugeFill');
17
+ const ratio = totalChecks > 0 ? checksPassed / totalChecks : 0;
18
+ const isPerfectWcagScore = totalChecks > 0 && violations === 0;
19
+
20
+ if (isPerfectWcagScore) {
21
+ numEl.classList.toggle('perfect-score', isPerfectWcagScore);
22
+ }
23
+
24
+ if (gaugeFillPath && typeof gaugeFillPath.getTotalLength === 'function') {
25
+ const len = gaugeFillPath.getTotalLength();
26
+ gaugeFillPath.style.strokeDasharray = String(len);
27
+ gaugeFillPath.style.strokeDashoffset = String(len);
28
+
29
+ gaugeFillPath.classList.remove('bad', 'good');
30
+ gaugeFillPath.classList.add(isPerfectWcagScore ? 'good' : 'bad');
31
+
32
+ requestAnimationFrame(() => {
33
+ gaugeFillPath.style.strokeDashoffset = String(len * (1 - ratio));
34
+ });
35
+ }
36
+
37
+ const gauge = document.getElementById('wcagGauge');
38
+ if (gauge) {
39
+ gauge.setAttribute(
40
+ 'aria-label',
41
+ `WCAG A & AA automated checks passed: ${checksPassed} out of ${totalChecks}`
42
+ );
43
+ }
44
+ }
45
+
46
+ renderWcagGaugeFromScanData();
47
+ </script>
@@ -0,0 +1,15 @@
1
+ <script>
2
+ function renderNotCompliantTextIfAny() {
3
+ const wcagTotals = scanData?.wcagPassPercentage ?? {};
4
+ const totalChecks = Number(wcagTotals.totalWcagChecksAA ?? 0);
5
+ const violations = Number(wcagTotals.totalWcagViolationsAA ?? 0);
6
+ const isPerfectWcagScore = totalChecks > 0 && violations === 0;
7
+ const statusEl = document.getElementById('wcagStatus');
8
+
9
+ if (statusEl) {
10
+ statusEl.hidden = isPerfectWcagScore;
11
+ }
12
+ }
13
+
14
+ renderNotCompliantTextIfAny();
15
+ </script>
@@ -0,0 +1,75 @@
1
+ <script>
2
+ const makeExternalIcon = () => {
3
+ const ns = "http://www.w3.org/2000/svg";
4
+ const svg = document.createElementNS(ns, "svg");
5
+ svg.setAttribute("width", "16");
6
+ svg.setAttribute("height", "12");
7
+ svg.setAttribute("viewBox", "0 0 8 8");
8
+ svg.setAttribute("aria-hidden", "true");
9
+ svg.setAttribute("focusable", "false");
10
+ svg.setAttribute("style", "margin-left: 0.3rem");
11
+ svg.setAttribute("class", "link-external-icon");
12
+
13
+ const path = document.createElementNS(ns, "path");
14
+ path.setAttribute(
15
+ "d",
16
+ "M7.11111 7.11111H0.888889V0.888889H4V0H0.888889C0.395556 0 0 0.4 0 0.888889V7.11111C0 7.6 0.395556 8 0.888889 8H7.11111C7.6 8 8 7.6 8 7.11111V4H7.11111V7.11111ZM4.88889 0V0.888889H6.48444L2.11556 5.25778L2.74222 5.88444L7.11111 1.51556V3.11111H8V0H4.88889Z"
17
+ );
18
+ path.setAttribute("fill", "#5735DF");
19
+ svg.appendChild(path);
20
+ return svg;
21
+ };
22
+
23
+ const scanDataWCAGList = () => {
24
+ const linksMap = scanData.wcagLinks || {};
25
+ const labelMap = scanData.wcagCriteriaLabels || {};
26
+ const norm = (s) => String(s||"").normalize("NFKC").replace(/\s+/g," ").trim().toUpperCase();
27
+
28
+ const labelLookup = new Map(
29
+ Object.entries(labelMap).map(([k,v]) => [norm(k), String(v).toUpperCase()])
30
+ );
31
+
32
+ const aaa = [];
33
+ const aaOrA = [];
34
+ Object.entries(linksMap).forEach(([code, url]) => {
35
+ const level = labelLookup.get(norm(code));
36
+ (level === "AAA" ? aaa : aaOrA).push([code, url]);
37
+ });
38
+
39
+ const wcagSort = ([a],[b]) => a.localeCompare(b, undefined, {numeric:true});
40
+ aaOrA.sort(wcagSort);
41
+ aaa.sort(wcagSort);
42
+
43
+ const listAA = document.getElementById("wcagLinksListAA");
44
+ const listAAA = document.getElementById("wcagLinksListAAA");
45
+ const countAA = document.getElementById("wcagAALabelCount");
46
+ const countAAA = document.getElementById("wcagAAALabelCount");
47
+
48
+ const render = (ul, items) => {
49
+ if (!ul) return;
50
+ ul.innerHTML = "";
51
+ items.forEach(([code, url]) => {
52
+ const li = document.createElement("li");
53
+
54
+ const a = document.createElement("a");
55
+ a.className = "wcag-link";
56
+ a.href = url;
57
+ a.target = "_blank";
58
+ a.rel = "noopener";
59
+
60
+ a.append(document.createTextNode(code), makeExternalIcon()); // text + icon both clickable
61
+
62
+ li.appendChild(a);
63
+ ul.appendChild(li);
64
+ });
65
+ };
66
+
67
+ render(listAA, aaOrA);
68
+ render(listAAA, aaa);
69
+
70
+ if (countAA) countAA.textContent = String(aaOrA.length);
71
+ if (countAAA) countAAA.textContent = String(aaa.length);
72
+ };
73
+
74
+ scanDataWCAGList();
75
+ </script>
@@ -0,0 +1,384 @@
1
+ <style>
2
+ #allIssuesSection {
3
+ margin-bottom: 1.5rem;
4
+ border-color: var(--soft-blue);
5
+ box-shadow: 0 2px 4px 0 rgb(77, 52, 191, 10%);
6
+ }
7
+
8
+ #allIssuesSection .card-body {
9
+ padding: 1.5rem;
10
+ }
11
+
12
+ /* Category Badges */
13
+ .category-badges-container {
14
+ display: flex;
15
+ gap: 1rem;
16
+ align-items: center;
17
+ }
18
+
19
+ .category-badge {
20
+ display: inline-flex;
21
+ align-items: center;
22
+ justify-content: center;
23
+ border-radius: 5px;
24
+ font-size: 12px;
25
+ font-weight: 600;
26
+ padding: 0 4px;
27
+ width: fit-content;
28
+ min-height: 20px;
29
+ height: 20px;
30
+ margin-right: 0.5rem;
31
+ }
32
+
33
+ .category-badge.must-fix {
34
+ background: var(--light-carmine-pink);
35
+ color: var(--true-white);
36
+ border: 1px solid var(--light-carmine-pink);
37
+ }
38
+
39
+ .category-badge.good-to-fix {
40
+ background: var(--strong-orange);
41
+ color: var(--true-white);
42
+ border: 1px solid var(--strong-orange);
43
+ }
44
+
45
+ .category-badge.needs-review {
46
+ background: var(--very-dark-gray);
47
+ color: var(--true-white);
48
+ border: 1px solid var(--very-dark-gray);
49
+ }
50
+
51
+ .category-item {
52
+ border-right: 1px solid var(--chinese-silver);
53
+ padding-right: 1rem;
54
+ line-height: 1.375rem;
55
+ }
56
+
57
+ .category-item:last-child {
58
+ border-right: none;
59
+ padding-right: 0;
60
+ }
61
+
62
+ .category-item-inner {
63
+ display: inline-flex;
64
+ align-items: center;
65
+ }
66
+
67
+ .category-tooltip-wrapper {
68
+ position: relative;
69
+ display: inline-flex;
70
+ align-items: center;
71
+ margin-right: 0.75rem;
72
+ }
73
+
74
+ .category-tooltip-icon {
75
+ border: none;
76
+ background: transparent;
77
+ padding: 0;
78
+ margin: 0;
79
+ display: inline-flex;
80
+ cursor: pointer;
81
+ }
82
+
83
+ .category-tooltip-icon svg {
84
+ display: block;
85
+ }
86
+
87
+ .category-tooltip-bubble {
88
+ position: absolute;
89
+ bottom: calc(100% + 6px);
90
+ left: 50%;
91
+ transform: translateX(-50%);
92
+ min-width: 220px;
93
+ max-width: 260px;
94
+ white-space: normal;
95
+ background: #222;
96
+ color: #fff;
97
+ padding: 0.4rem 0.6rem;
98
+ border-radius: 4px;
99
+ font-size: 0.75rem;
100
+ line-height: 1.3;
101
+ box-shadow: 0 2px 6px rgba(0,0,0,0.16);
102
+ opacity: 0;
103
+ visibility: hidden;
104
+ pointer-events: none;
105
+ transition: opacity 0.15s ease;
106
+ z-index: 20;
107
+ font-size: 0.875rem;
108
+ }
109
+
110
+ /* Little arrow */
111
+ .category-tooltip-bubble::after {
112
+ content: "";
113
+ position: absolute;
114
+ top: 100%;
115
+ left: 50%;
116
+ transform: translateX(-50%);
117
+ border-width: 5px;
118
+ border-style: solid;
119
+ border-color: #222 transparent transparent transparent;
120
+ }
121
+
122
+ .category-tooltip-wrapper:hover .category-tooltip-bubble,
123
+ .category-tooltip-wrapper:focus-within .category-tooltip-bubble {
124
+ opacity: 1;
125
+ visibility: visible;
126
+ }
127
+
128
+ /* Filter Bar */
129
+ .filter-bar {
130
+ display: flex;
131
+ gap: 1rem;
132
+ margin-bottom: 1.5rem;
133
+ align-items: center;
134
+ flex-wrap: wrap;
135
+ }
136
+
137
+ .filter-dropdowns {
138
+ display: flex;
139
+ gap: 0.75rem;
140
+ }
141
+
142
+ .filter-dropdown {
143
+ padding: 0.5rem 1rem;
144
+ border: 1px solid var(--chinese-silver);
145
+ border-radius: 5px;
146
+ cursor: pointer;
147
+ min-width: 150px;
148
+ }
149
+
150
+ .filter-dropdown:focus {
151
+ outline: 2px solid var(--ocean-blue);
152
+ outline-offset: 2px;
153
+ }
154
+
155
+ .search-container {
156
+ position: relative;
157
+ min-width: 250px;
158
+ flex: 1;
159
+ }
160
+
161
+ .search-icon {
162
+ position: absolute;
163
+ left: 12px;
164
+ top: 50%;
165
+ transform: translateY(-50%);
166
+ pointer-events: none;
167
+ }
168
+
169
+ .search-input {
170
+ width: 100%;
171
+ padding: 0.5rem 1rem 0.5rem 1rem;
172
+ border: 1px solid var(--chinese-silver);
173
+ border-radius: 5px;
174
+ line-height: 1.5;
175
+ }
176
+
177
+ .search-input:focus {
178
+ outline: 2px solid var(--ocean-blue);
179
+ outline-offset: 2px;
180
+ border-color: var(--ocean-blue);
181
+ }
182
+
183
+ /* Issues Table */
184
+ .issues-table-container {
185
+ position: relative;
186
+ max-height: 67vh;
187
+ overflow-y: auto;
188
+ border-radius: 5px;
189
+ width: 100%;
190
+ max-width: 100%;
191
+ }
192
+
193
+ .issues-table {
194
+ width: 100%;
195
+ border-collapse: separate;
196
+ border-spacing: 0;
197
+ table-layout: fixed;
198
+ }
199
+
200
+ .issues-table thead th {
201
+ padding: 0.75rem 1rem;
202
+ text-align: left;
203
+ font-size: 1rem;
204
+ font-weight: 600;
205
+ background: var(--true-white);
206
+ position: sticky;
207
+ top: 0;
208
+ z-index: 10;
209
+ overflow: hidden;
210
+ text-overflow: ellipsis;
211
+ }
212
+
213
+ .issues-table th.sortable {
214
+ cursor: pointer;
215
+ user-select: none;
216
+ }
217
+
218
+ .issues-table th.sortable:hover {
219
+ background: #f0f0f0;
220
+ }
221
+
222
+ .issues-table th span {
223
+ display: inline-flex;
224
+ align-items: center;
225
+ gap: 0.5rem;
226
+ }
227
+
228
+ .sort-icon {
229
+ display: inline-block;
230
+ vertical-align: middle;
231
+ }
232
+
233
+ .sort-icon.active {
234
+ opacity: 1;
235
+ }
236
+
237
+ .issues-table tbody tr {
238
+ cursor: pointer;
239
+ transition: background 0.2s;
240
+ }
241
+
242
+ .issues-table tbody tr:hover {
243
+ background: #f9f9f9;
244
+ }
245
+
246
+ .issues-table tbody td {
247
+ border-bottom: 1px solid var(--chinese-silver);
248
+ padding: 1rem;
249
+ font-size: 1rem;
250
+ }
251
+
252
+ .issues-table tbody td:last-child {
253
+ text-align: right;
254
+ padding-right: 1rem;
255
+ }
256
+
257
+ .issue-name-cell {
258
+ display: flex;
259
+ flex-direction: column;
260
+ gap: 0.5rem;
261
+ }
262
+
263
+ .issue-description {
264
+ color: #333;
265
+ }
266
+
267
+ .issue-conformance-badges {
268
+ display: flex;
269
+ gap: 0.5rem;
270
+ flex-wrap: wrap;
271
+ margin-top: 0.25rem;
272
+ }
273
+
274
+ .conformance-badge {
275
+ background-color: white;
276
+ border-radius: 5px;
277
+ font-size: 0.75rem;
278
+ white-space: nowrap;
279
+ border: 1px solid var(--a11y-black-100);
280
+ line-height: 1.5;
281
+ font-weight: 700;
282
+ }
283
+
284
+ .occurrence-count {
285
+ font-weight: 600;
286
+ color: var(--light-carmine-pink);
287
+ }
288
+
289
+ .empty-state {
290
+ text-align: center;
291
+ padding: 3rem 1rem;
292
+ color: var(--a11y-black-300);
293
+ }
294
+
295
+ .total-issues-count {
296
+ padding: 0.75rem 1rem;
297
+ background: var(--true-white);
298
+ border-radius: 5px;
299
+ font-size: 1rem;
300
+ position: sticky;
301
+ bottom: 0;
302
+ z-index: 10;
303
+ margin-top: 0;
304
+ }
305
+
306
+ @media (max-width: 768px) {
307
+ .category-badges-container {
308
+ flex-direction: column;
309
+ align-items: flex-start;
310
+ }
311
+
312
+ .category-badge {
313
+ width: 100%;
314
+ height: fit-content;
315
+ }
316
+
317
+ .filter-bar {
318
+ flex-direction: column;
319
+ align-items: stretch;
320
+ }
321
+
322
+ .filter-dropdowns {
323
+ flex-direction: column;
324
+ }
325
+
326
+ .filter-dropdown,
327
+ .search-container {
328
+ width: 100%;
329
+ }
330
+
331
+ .issues-table-container {
332
+ overflow-x: auto;
333
+ }
334
+
335
+ .issues-table {
336
+ font-size: 0.8rem;
337
+ min-width: 100%;
338
+ table-layout: auto;
339
+ }
340
+
341
+ .issues-table thead th {
342
+ padding: 0.5rem 0.25rem;
343
+ font-size: 0.75rem;
344
+ white-space: nowrap;
345
+ }
346
+
347
+ .issues-table thead th span {
348
+ gap: 0.25rem;
349
+ font-size: 0.75rem;
350
+ }
351
+
352
+ .issues-table thead th .sort-icon {
353
+ width: 16px;
354
+ height: 16px;
355
+ }
356
+
357
+ .issues-table tbody td {
358
+ padding: 0.5rem 0.25rem;
359
+ font-size: 0.8rem;
360
+ }
361
+
362
+ .issues-table tbody td:last-child {
363
+ padding-right: 0.5rem;
364
+ }
365
+
366
+ .issues-table thead th:last-child {
367
+ width: 10%;
368
+ min-width: 40px;
369
+ }
370
+
371
+ .category-item {
372
+ border-right: 0;
373
+ }
374
+
375
+ .issue-conformance-badges {
376
+ gap: 0.25rem;
377
+ }
378
+
379
+ .conformance-badge {
380
+ font-size: 0.65rem;
381
+ padding: 2px 4px;
382
+ }
383
+ }
384
+ </style>