@vibecodeqa/cli 0.16.0 → 0.18.0
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.
- package/README.md +73 -63
- package/dist/check-meta.d.ts +1 -0
- package/dist/check-meta.js +58 -6
- package/dist/cli.js +48 -10
- package/dist/detect.js +24 -2
- package/dist/fs-utils.d.ts +4 -0
- package/dist/fs-utils.js +12 -6
- package/dist/report/html.d.ts +18 -9
- package/dist/report/html.js +108 -68
- package/dist/report/pages.d.ts +4 -4
- package/dist/report/pages.js +165 -115
- package/dist/report/sarif.d.ts +3 -0
- package/dist/report/sarif.js +67 -0
- package/dist/report/styles.d.ts +1 -1
- package/dist/report/styles.js +105 -33
- package/dist/report/svg.d.ts +17 -0
- package/dist/report/svg.js +99 -0
- package/dist/runners/accessibility.d.ts +3 -0
- package/dist/runners/accessibility.js +85 -0
- package/dist/runners/architecture.d.ts +2 -0
- package/dist/runners/architecture.js +232 -20
- package/dist/runners/code-coherence.d.ts +17 -0
- package/dist/runners/code-coherence.js +39 -0
- package/dist/runners/complexity.js +7 -37
- package/dist/runners/confusion.js +3 -31
- package/dist/runners/context.js +9 -40
- package/dist/runners/dependencies.js +28 -0
- package/dist/runners/doc-coherence.d.ts +14 -0
- package/dist/runners/doc-coherence.js +48 -0
- package/dist/runners/docs.js +7 -32
- package/dist/runners/duplication.js +9 -37
- package/dist/runners/lint.js +17 -0
- package/dist/runners/performance.d.ts +10 -0
- package/dist/runners/performance.js +174 -0
- package/dist/runners/react.d.ts +3 -0
- package/dist/runners/react.js +86 -0
- package/dist/runners/secrets.js +8 -29
- package/dist/runners/security.js +15 -38
- package/dist/runners/standards.js +3 -36
- package/dist/runners/structure.js +35 -55
- package/dist/runners/testing.js +2 -36
- package/dist/runners/type-safety.d.ts +1 -1
- package/dist/runners/type-safety.js +19 -37
- package/dist/runners/types-check.d.ts +1 -1
- package/dist/runners/types-check.js +38 -20
- package/dist/types.d.ts +5 -5
- package/package.json +11 -10
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/** SARIF 2.1.0 output for GitHub Code Scanning integration. */
|
|
2
|
+
import { getCheckMeta } from "../check-meta.js";
|
|
3
|
+
export function generateSARIF(report) {
|
|
4
|
+
const rules = [];
|
|
5
|
+
const ruleIndex = new Map();
|
|
6
|
+
const results = [];
|
|
7
|
+
for (const check of report.checks) {
|
|
8
|
+
const meta = getCheckMeta(check.name);
|
|
9
|
+
for (const issue of check.issues) {
|
|
10
|
+
const ruleId = issue.rule || check.name;
|
|
11
|
+
// Register rule if not seen
|
|
12
|
+
if (!ruleIndex.has(ruleId)) {
|
|
13
|
+
ruleIndex.set(ruleId, rules.length);
|
|
14
|
+
rules.push({
|
|
15
|
+
id: ruleId,
|
|
16
|
+
name: ruleId,
|
|
17
|
+
shortDescription: { text: meta.label },
|
|
18
|
+
fullDescription: meta.description ? { text: meta.description } : undefined,
|
|
19
|
+
defaultConfiguration: { level: severityToLevel(issue.severity) },
|
|
20
|
+
helpUri: "https://vibecodeqa.online",
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
const result = {
|
|
24
|
+
ruleId,
|
|
25
|
+
level: severityToLevel(issue.severity),
|
|
26
|
+
message: { text: `[${check.name}] ${issue.message}` },
|
|
27
|
+
};
|
|
28
|
+
if (issue.file) {
|
|
29
|
+
const filePath = issue.file.split(":")[0];
|
|
30
|
+
result.locations = [
|
|
31
|
+
{
|
|
32
|
+
physicalLocation: {
|
|
33
|
+
artifactLocation: { uri: filePath },
|
|
34
|
+
...(issue.line ? { region: { startLine: issue.line } } : {}),
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
results.push(result);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const sarif = {
|
|
43
|
+
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
|
|
44
|
+
version: "2.1.0",
|
|
45
|
+
runs: [
|
|
46
|
+
{
|
|
47
|
+
tool: {
|
|
48
|
+
driver: {
|
|
49
|
+
name: "VibeCode QA",
|
|
50
|
+
version: report.version,
|
|
51
|
+
informationUri: "https://vibecodeqa.online",
|
|
52
|
+
rules,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
results,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
return JSON.stringify(sarif, null, 2);
|
|
60
|
+
}
|
|
61
|
+
function severityToLevel(severity) {
|
|
62
|
+
if (severity === "error")
|
|
63
|
+
return "error";
|
|
64
|
+
if (severity === "warning")
|
|
65
|
+
return "warning";
|
|
66
|
+
return "note";
|
|
67
|
+
}
|
package/dist/report/styles.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** All CSS for the HTML report, extracted for maintainability. */
|
|
2
|
-
export declare const CSS = "\n:root{--bg:#09090b;--card:#111115;--border:#1e1e24;--text:#e5e5e5;--muted:#6b7280;--pass:#22c55e;--fail:#ef4444;--warn:#eab308;--info:#6366f1;--accent:#818cf8}\n*{margin:0;padding:0;box-sizing:border-box}\nbody{font-family:\"Inter\",system-ui,sans-serif;background:var(--bg);color:var(--text);line-height:1.5}\ncode{font-family:\"SF Mono\",Menlo,monospace;font-size:0.85em}\n\n/* Top nav */\n.top{position:sticky;top:0;z-index:20;background:#0c0c0fcc;backdrop-filter:blur(12px);border-bottom:1px solid var(--border);padding:0 2rem;display:flex;align-items:center;gap:0}\n.logo{font-weight:800;font-size:1rem;margin-right:1.5rem;padding:0.7rem 0}\n.logo span{color:var(--accent)}\n.tn{padding:0.7rem 0.8rem;font-size:0.78rem;color:var(--muted);text-decoration:none;cursor:pointer;border-bottom:2px solid transparent;transition:all 0.15s}\n.tn:hover{color:var(--text)}\n.tn.active{color:var(--text);border-bottom-color:var(--accent)}\n\n/* Sidebar */\n.side{position:fixed;top:42px;left:0;bottom:0;width:200px;background:#0c0c0f;border-right:1px solid var(--border);overflow-y:auto;padding:0.8rem 0;font-size:0.7rem;z-index:10}\n.side-section{padding:0.3rem 0;border-bottom:1px solid var(--border)}\n.side-section:last-child{border-bottom:none}\n.side-score{font-size:1.4rem;font-weight:900;padding:0.3rem 0.8rem}\n.side-cat{display:block;padding:0.3rem 0.8rem;color:var(--text);font-weight:700;cursor:pointer;text-decoration:none;font-size:0.72rem}\n.side-cat:hover{background:#14141a}\n.side-check{display:block;padding:0.2rem 0.8rem 0.2rem 1.2rem;color:var(--muted);cursor:pointer;text-decoration:none;font-size:0.65rem}\n.side-check:hover{color:var(--text);background:#14141a}\n.side-check span{display:inline-block;width:1rem;font-weight:800;text-align:center}\n\n/* Content */\n.content{max-width:900px;margin-left:200px;padding:2rem}\n.page{display:none;animation:fadeIn 0.15s}\n.page.active{display:block}\n@keyframes fadeIn{from{opacity:0}to{opacity:1}}\n\n/* Overview */\n.dash{display:flex;gap:2rem;margin-bottom:2rem;align-items:center;flex-wrap:wrap}\n.hero{display:flex;align-items:center;gap:1rem}\n.hero svg{width:100px;height:100px}\n.hc{display:flex;flex-direction:column}\n.hg{font-size:2.5rem;font-weight:900;line-height:1}\n.hs{font-size:1rem;font-weight:600}\n.hd{font-size:0.68rem;color:var(--muted)}\n.radar{flex:1;display:flex;justify-content:center}\n.radar svg{max-width:240px;width:100%}\n.cats{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:0.6rem;margin-bottom:2rem}\n.cc{background:var(--card);border:1px solid var(--border);border-radius:0.6rem;padding:0.8rem;cursor:pointer;transition:border-color 0.15s}\n.cc:hover{border-color:var(--accent)}\n.cc-s{font-size:1.8rem;font-weight:900}\n.cc-l{font-size:0.75rem;color:var(--muted)}\n.cc-m{margin-top:0.3rem;display:flex;gap:0.25rem}\n.mc{font-size:0.65rem;font-weight:800}\nh3{font-size:0.85rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.04em;margin-bottom:0.5rem}\n.bars{margin-bottom:1.5rem}\n.brow{display:flex;align-items:center;gap:0.4rem;margin-bottom:0.25rem;font-size:0.72rem}\n.bl{width:80px;text-align:right;color:var(--muted);flex-shrink:0}\n.bb{flex:1;height:14px;background:var(--card);border-radius:3px;overflow:hidden;border:1px solid var(--border)}\n.bf{height:100%;border-radius:2px}\n.bv{width:36px;font-weight:700;font-size:0.68rem}\n.stack{display:flex;gap:0.35rem;flex-wrap:wrap}\n.stack span{background:var(--card);border:1px solid var(--border);padding:0.1rem 0.45rem;border-radius:9999px;font-size:0.62rem;color:var(--muted)}\n\n/* Category pages */\n.cat-head{margin-bottom:0.3rem}\n.bar2{height:4px;background:var(--card);border-radius:2px;margin-bottom:1rem;overflow:hidden}\n.bf2{height:100%;border-radius:2px}\n.sub-nav{display:flex;gap:0;border-bottom:1px solid var(--border);margin-bottom:1rem}\n.sn{padding:0.5rem 0.8rem;font-size:0.75rem;color:var(--muted);cursor:pointer;border-bottom:2px solid transparent}\n.sn:hover{color:var(--text)}\n.sn.active{color:var(--text);border-bottom-color:var(--accent)}\n.sp{display:none}.sp.active{display:block}\n\n/* Check detail */\n.ch-head{display:flex;align-items:center;gap:0.7rem;margin-bottom:0.8rem}\n.ch-g{font-size:2rem;font-weight:900}\n.ch-s{display:block;font-size:0.7rem;color:var(--muted)}\n.pri{font-size:0.62rem;font-weight:700;text-transform:uppercase;letter-spacing:0.04em;padding:0.15rem 0.5rem;border-radius:9999px;border:1px solid currentColor;flex-shrink:0}\n.info-panel{background:#0d0d12;border:1px solid var(--border);border-radius:0.5rem;padding:0.7rem 0.9rem;margin-bottom:1rem;font-size:0.72rem;line-height:1.6}\n.ip-row{margin-bottom:0.4rem;display:flex;gap:0.5rem}\n.ip-row:last-child{margin-bottom:0}\n.ip-label{color:var(--accent);font-weight:700;min-width:2.5rem;flex-shrink:0}\n.skip-r{color:var(--muted);font-style:italic;font-size:0.78rem}\n.kvs{display:flex;gap:0.6rem;flex-wrap:wrap;margin-bottom:1rem}\n.kv{background:var(--card);border:1px solid var(--border);border-radius:0.4rem;padding:0.3rem 0.6rem;font-size:0.7rem}\n.k{color:var(--muted);margin-right:0.3rem}\n.v{font-weight:600}\n\n/* Issue list grouped by file */\n.iss-list{margin-top:1rem}\n.fg{margin-bottom:0.8rem}\n.fn{font-size:0.72rem;font-weight:600;font-family:\"SF Mono\",monospace;padding:0.3rem 0;border-bottom:1px solid var(--border);margin-bottom:0.2rem;display:flex;align-items:center;gap:0.5rem}\n.fc{background:var(--border);border-radius:9999px;padding:0 0.4rem;font-size:0.6rem;color:var(--muted)}\n.ir{font-size:0.65rem;font-family:\"SF Mono\",monospace;padding:0.12rem 0 0.12rem 0.5rem;display:flex;gap:0.4rem;align-items:baseline}\n.is{font-weight:800;font-size:0.55rem;width:0.9rem;text-align:center;border-radius:2px;flex-shrink:0}\n.ir.error .is{color:var(--fail);background:#ef444418}\n.ir.warning .is{color:var(--warn);background:#eab30818}\n.il{color:var(--accent);min-width:2rem;flex-shrink:0}\n.im{flex:1;word-break:break-word}\n.iru{color:#555;font-size:0.55rem}\n\n/* All issues table */\n.isf{color:var(--muted);font-size:0.75rem;margin-bottom:0.8rem}\n.it{width:100%;border-collapse:collapse;font-size:0.68rem}\n.it th{text-align:left;padding:0.35rem 0.4rem;color:var(--muted);font-size:0.62rem;text-transform:uppercase;border-bottom:1px solid var(--border)}\n.it td{padding:0.25rem 0.4rem;border-bottom:1px solid var(--border);font-family:\"SF Mono\",monospace;font-size:0.62rem}\n.it tr.error .is2{color:var(--fail)}\n.it tr.warning .is2{color:var(--warn)}\n.is2{font-weight:800;width:1rem}\n.ic2{color:var(--muted);width:70px}\n.il2{color:var(--muted)}\n.iru2{color:#555;font-size:0.58rem}\n\n/* File heatmap */\n.fr{display:flex;align-items:center;gap:0.5rem;margin-bottom:0.3rem;font-size:0.7rem}\n.ff{width:200px;font-family:\"SF Mono\",monospace;font-size:0.65rem;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.fb{flex:1;height:12px;background:var(--card);border-radius:3px;overflow:hidden;border:1px solid var(--border)}\n.fbf{height:100%;border-radius:2px}\n.fv{width:50px;font-size:0.65rem;color:var(--muted);flex-shrink:0}\n.fcs{font-size:0.6rem;color:#555}\n\n.footer{text-align:center;color:var(--muted);font-size:0.58rem;margin-top:2rem;padding:0.8rem 0;border-top:1px solid var(--border)}\n.footer a{color:var(--muted)}\n.flink{color:var(--accent);text-decoration:none;font-family:\"SF Mono\",monospace}.flink:hover{text-decoration:underline}\n.arch-svg{margin:1rem 0;overflow-x:auto}\n.hm-row{display:flex;align-items:center;gap:0.5rem;margin-bottom:0.2rem;font-size:0.7rem}\n.hm-name{width:200px;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:\"SF Mono\",monospace;font-size:0.65rem}\n.hm-bar{height:14px;border-radius:3px;min-width:4px}\n.hm-count{color:var(--muted);font-size:0.65rem;flex-shrink:0}\n.arch-svg svg{border-radius:8px}\n.cp-btn{background:none;border:none;cursor:pointer;font-size:0.6rem;opacity:0.3;padding:0 0.2rem;flex-shrink:0}.cp-btn:hover{opacity:1}\n.ir:hover .cp-btn{opacity:0.6}\n@media(max-width:768px){.side{display:none}.content{margin-left:0;padding:1rem}.cats{grid-template-columns:1fr 1fr}.dash{flex-direction:column}}\n";
|
|
2
|
+
export declare const CSS = "\n:root{--bg:#09090b;--card:#111115;--border:#1e1e24;--text:#e5e5e5;--muted:#6b7280;--pass:#22c55e;--fail:#ef4444;--warn:#eab308;--info:#6366f1;--accent:#818cf8;--side-w:200px;--top-h:42px}\n*{margin:0;padding:0;box-sizing:border-box}\nbody{font-family:\"Inter\",system-ui,sans-serif;background:var(--bg);color:var(--text);line-height:1.5}\ncode{font-family:\"SF Mono\",Menlo,monospace;font-size:0.85em}\n\n/* \u2500\u2500 Top nav \u2500\u2500 */\n.top{position:sticky;top:0;z-index:30;background:#0c0c0fdd;backdrop-filter:blur(12px);border-bottom:1px solid var(--border);padding:0 1.5rem;display:flex;align-items:center;height:var(--top-h)}\n.logo{font-weight:800;font-size:1rem;margin-right:1rem;flex-shrink:0;text-decoration:none;color:var(--text)}\n.logo span{color:var(--accent)}\n.nav-scroll{display:flex;align-items:center;gap:0;overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none;flex:1}\n.nav-scroll::-webkit-scrollbar{display:none}\n.tn{padding:0 0.7rem;font-size:0.78rem;color:var(--muted);text-decoration:none;border-bottom:2px solid transparent;transition:all 0.15s;white-space:nowrap;line-height:var(--top-h)}\n.tn:hover{color:var(--text)}\n.tn.active{color:var(--text);border-bottom-color:var(--accent)}\n.hamburger{display:none;background:none;border:none;color:var(--muted);font-size:1.3rem;cursor:pointer;padding:0 0.4rem;line-height:var(--top-h)}\n\n/* \u2500\u2500 Sidebar \u2500\u2500 */\n.side{position:fixed;top:var(--top-h);left:0;bottom:0;width:var(--side-w);background:#0c0c0f;border-right:1px solid var(--border);overflow-y:auto;padding:0.6rem 0;font-size:0.7rem;z-index:20}\n.side-section{padding:0.3rem 0;border-bottom:1px solid var(--border)}\n.side-section:last-child{border-bottom:none}\n.side-label{padding:0.2rem 0.8rem;font-size:0.6rem;text-transform:uppercase;letter-spacing:0.05em;color:#444;font-weight:600}\n.side-score{font-size:1.4rem;font-weight:900;padding:0.2rem 0.8rem}\n.side-cat{display:block;padding:0.3rem 0.8rem;color:var(--text);font-weight:700;cursor:pointer;text-decoration:none;font-size:0.72rem}\n.side-cat:hover{background:#14141a}\n.side-cat-title{padding:0.3rem 0.8rem;font-size:0.65rem;text-transform:uppercase;letter-spacing:0.04em;color:var(--accent);font-weight:700}\n.side-check{display:block;padding:0.15rem 0.8rem 0.15rem 0.8rem;color:var(--muted);cursor:pointer;text-decoration:none;font-size:0.65rem}\n.side-check:hover{color:var(--text);background:#14141a}\n.side-check span{display:inline-block;min-width:2.5rem;font-weight:700;font-size:0.6rem}\n.side-stat{padding:0.15rem 0.8rem;font-size:0.7rem;color:var(--muted)}\n.side-stat span{font-weight:800;font-size:0.8rem}\n.side-views{padding-top:0.3rem}\n.side-views .side-check{padding-left:0.8rem}\n\n/* \u2500\u2500 Content \u2500\u2500 */\n.content{margin-left:var(--side-w);padding:1.5rem 2rem;max-width:960px}\n\n/* \u2500\u2500 Overview \u2500\u2500 */\n.dash{display:flex;gap:2rem;margin-bottom:2rem;align-items:center;flex-wrap:wrap}\n.hero{display:flex;align-items:center;gap:1rem}\n.hero svg{width:100px;height:100px}\n.hc{display:flex;flex-direction:column}\n.hg{font-size:2.5rem;font-weight:900;line-height:1}\n.hs{font-size:1rem;font-weight:600}\n.hd{font-size:0.68rem;color:var(--muted)}\n.radar{flex:1;display:flex;justify-content:center}\n.radar svg{max-width:240px;width:100%}\n.cats{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:0.6rem;margin-bottom:2rem}\n.cc{background:var(--card);border:1px solid var(--border);border-radius:0.6rem;padding:0.8rem;transition:border-color 0.15s;text-decoration:none;color:var(--text);display:block}\n.cc:hover{border-color:var(--accent)}\n.cc-s{font-size:1.8rem;font-weight:900}\n.cc-l{font-size:0.75rem;color:var(--muted)}\n.cc-m{margin-top:0.3rem;display:flex;gap:0.25rem}\n.mc{font-size:0.65rem;font-weight:800}\nh3{font-size:0.85rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.04em;margin-bottom:0.5rem}\n\n/* \u2500\u2500 Overview sections \u2500\u2500 */\n.ov-section{margin-bottom:1.5rem}\n.ov-issue{font-size:0.68rem;font-family:\"SF Mono\",monospace;padding:0.2rem 0;display:flex;gap:0.4rem;align-items:baseline;border-bottom:1px solid var(--border)}\n.ov-issue .is{flex-shrink:0}\n.ov-issue.error .is{color:var(--fail)}\n.ov-issue.warning .is{color:var(--warn)}\n.ov-check{color:var(--muted);width:70px;flex-shrink:0;font-size:0.62rem}\n.ov-loc{color:var(--accent);flex-shrink:0;font-size:0.62rem;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.ov-msg{flex:1;word-break:break-word}\n.ov-link{display:block;margin-top:0.5rem;font-size:0.72rem;color:var(--accent);text-decoration:none}\n.ov-link:hover{text-decoration:underline}\n\n/* \u2500\u2500 Timeline \u2500\u2500 */\n.timeline{margin:0.5rem 0;overflow-x:auto}\n.timeline svg{max-width:100%}\n\n/* \u2500\u2500 Bar chart \u2500\u2500 */\n.bars{margin-bottom:1.5rem}\n.brow{display:flex;align-items:center;gap:0.4rem;margin-bottom:0.25rem;font-size:0.72rem}\n.bl{width:90px;text-align:right;color:var(--muted);flex-shrink:0}\n.bb{flex:1;height:14px;background:var(--card);border-radius:3px;overflow:hidden;border:1px solid var(--border)}\n.bf{height:100%;border-radius:2px}\n.bv{width:36px;font-weight:700;font-size:0.68rem}\n.stack{display:flex;gap:0.35rem;flex-wrap:wrap;margin-top:1rem}\n.stack span{background:var(--card);border:1px solid var(--border);padding:0.1rem 0.45rem;border-radius:9999px;font-size:0.62rem;color:var(--muted)}\n\n/* \u2500\u2500 Category pages \u2500\u2500 */\n.cat-head{margin-bottom:0.3rem}\n.bar2{height:4px;background:var(--card);border-radius:2px;margin-bottom:1rem;overflow:hidden}\n.bf2{height:100%;border-radius:2px}\n.sub-nav{display:flex;gap:0;border-bottom:1px solid var(--border);margin-bottom:1rem;flex-wrap:wrap}\n.sn{padding:0.5rem 0.8rem;font-size:0.75rem;color:var(--muted);cursor:pointer;border-bottom:2px solid transparent}\n.sn:hover{color:var(--text)}\n.sn.active{color:var(--text);border-bottom-color:var(--accent)}\n.sp{display:none}.sp.active{display:block}\n\n/* \u2500\u2500 Check detail \u2500\u2500 */\n.ch-head{display:flex;align-items:center;gap:0.7rem;margin-bottom:0.8rem}\n.ch-g{font-size:2rem;font-weight:900}\n.ch-s{display:block;font-size:0.7rem;color:var(--muted)}\n.pri{font-size:0.62rem;font-weight:700;text-transform:uppercase;letter-spacing:0.04em;padding:0.15rem 0.5rem;border-radius:9999px;border:1px solid currentColor;flex-shrink:0}\n.info-panel{background:#0d0d12;border:1px solid var(--border);border-radius:0.5rem;padding:0.7rem 0.9rem;margin-bottom:1rem;font-size:0.72rem;line-height:1.6}\n.ip-row{margin-bottom:0.4rem;display:flex;gap:0.5rem}\n.ip-row:last-child{margin-bottom:0}\n.ip-label{color:var(--accent);font-weight:700;min-width:2.5rem;flex-shrink:0}\n.skip-r{color:var(--muted);font-style:italic;font-size:0.78rem}\n.kvs{display:flex;gap:0.6rem;flex-wrap:wrap;margin-bottom:1rem}\n.kv{background:var(--card);border:1px solid var(--border);border-radius:0.4rem;padding:0.3rem 0.6rem;font-size:0.7rem}\n.k{color:var(--muted);margin-right:0.3rem}\n.v{font-weight:600}\n\n/* \u2500\u2500 Issue list grouped by file \u2500\u2500 */\n.iss-list{margin-top:1rem}\n.fg{margin-bottom:0.8rem}\n.fn{font-size:0.72rem;font-weight:600;font-family:\"SF Mono\",monospace;padding:0.3rem 0;border-bottom:1px solid var(--border);margin-bottom:0.2rem;display:flex;align-items:center;gap:0.5rem}\n.fc{background:var(--border);border-radius:9999px;padding:0 0.4rem;font-size:0.6rem;color:var(--muted)}\n.ir{font-size:0.65rem;font-family:\"SF Mono\",monospace;padding:0.12rem 0 0.12rem 0.5rem;display:flex;gap:0.4rem;align-items:baseline}\n.is{font-weight:800;font-size:0.55rem;width:0.9rem;text-align:center;border-radius:2px;flex-shrink:0}\n.ir.error .is{color:var(--fail);background:#ef444418}\n.ir.warning .is{color:var(--warn);background:#eab30818}\n.ir.info .is{color:var(--info);background:#6366f118}\n.il{color:var(--accent);min-width:2rem;flex-shrink:0}\n.im{flex:1;word-break:break-word}\n.iru{color:#555;font-size:0.55rem}\n\n/* \u2500\u2500 All issues table \u2500\u2500 */\n.isf{color:var(--muted);font-size:0.75rem;margin-bottom:0.8rem}\n.it{width:100%;border-collapse:collapse;font-size:0.68rem}\n.it th{text-align:left;padding:0.35rem 0.4rem;color:var(--muted);font-size:0.62rem;text-transform:uppercase;border-bottom:1px solid var(--border)}\n.it td{padding:0.25rem 0.4rem;border-bottom:1px solid var(--border);font-family:\"SF Mono\",monospace;font-size:0.62rem}\n.it tr.error .is2{color:var(--fail)}\n.it tr.warning .is2{color:var(--warn)}\n.is2{font-weight:800;width:1rem}\n.ic2{color:var(--muted);width:70px}\n.il2{color:var(--muted)}\n.iru2{color:#555;font-size:0.58rem}\n\n/* \u2500\u2500 File health \u2500\u2500 */\n.fr{display:flex;align-items:center;gap:0.5rem;margin-bottom:0.3rem;font-size:0.7rem}\n.ff{width:200px;font-family:\"SF Mono\",monospace;font-size:0.65rem;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.fb{flex:1;height:12px;background:var(--card);border-radius:3px;overflow:hidden;border:1px solid var(--border)}\n.fbf{height:100%;border-radius:2px}\n.fv{width:50px;font-size:0.65rem;color:var(--muted);flex-shrink:0}\n.hm-row{display:flex;align-items:center;gap:0.5rem;margin-bottom:0.2rem;font-size:0.7rem}\n.hm-name{width:200px;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:\"SF Mono\",monospace;font-size:0.65rem}\n.hm-bar{height:14px;border-radius:3px;min-width:4px}\n.hm-count{color:var(--muted);font-size:0.65rem;flex-shrink:0;min-width:50px}\n.hm-checks{font-size:0.58rem;color:#555;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n\n/* \u2500\u2500 Premium cards \u2500\u2500 */\n.pro-card{background:linear-gradient(135deg,#0f0f1a 0%,#13131f 100%);border:1px solid #2a2a3d;border-radius:0.75rem;padding:1.5rem;position:relative;overflow:hidden}\n.pro-card::before{content:\"\";position:absolute;top:-50%;right:-50%;width:200%;height:200%;background:radial-gradient(circle,#6366f108 0%,transparent 70%);pointer-events:none}\n.pro-badge{display:inline-block;background:linear-gradient(135deg,#6366f1,#818cf8);color:#fff;font-size:0.6rem;font-weight:800;padding:0.15rem 0.5rem;border-radius:9999px;letter-spacing:0.06em;margin-bottom:0.6rem}\n.pro-desc{color:var(--muted);font-size:0.78rem;line-height:1.6;margin-bottom:0.8rem}\n.pro-cta{color:#6366f1;font-size:0.72rem;font-weight:600;margin-top:1rem}\n.sn-pro{opacity:0.7}\n\n.footer{text-align:center;color:var(--muted);font-size:0.58rem;margin-top:2rem;padding:0.8rem 0;border-top:1px solid var(--border)}\n.footer a{color:var(--muted)}\n.flink{color:var(--accent);text-decoration:none;font-family:\"SF Mono\",monospace}.flink:hover{text-decoration:underline}\n.arch-svg{margin:1rem 0;overflow-x:auto;-webkit-overflow-scrolling:touch}\n.arch-svg svg{border-radius:8px}\n.cp-btn{background:none;border:none;cursor:pointer;font-size:0.6rem;opacity:0.3;padding:0 0.2rem;flex-shrink:0}.cp-btn:hover{opacity:1}\n.ir:hover .cp-btn{opacity:0.6}\n\n/* \u2500\u2500 Mobile: hamburger collapses both navs \u2500\u2500 */\n@media(max-width:768px){\n.hamburger{display:block}\n.nav-scroll{display:none}\n.nav-scroll.open{display:flex;position:absolute;top:var(--top-h);left:0;right:0;background:var(--bg);border-bottom:1px solid var(--border);flex-wrap:wrap;padding:0.3rem 0.5rem;z-index:25}\n.side{display:none}\n.side.open{display:block;z-index:25}\n.top{padding:0 0.8rem}\n.logo{font-size:0.85rem;margin-right:0.5rem}\n.content{margin-left:0;padding:0.8rem}\n.cats{grid-template-columns:1fr 1fr}\n.dash{flex-direction:column;gap:1rem}\n.hero svg{width:80px;height:80px}\n.hg{font-size:2rem}\n.radar svg{max-width:180px}\n.bl{width:60px;font-size:0.62rem}\n.bv{width:30px;font-size:0.6rem}\n.it{display:block;overflow-x:auto;-webkit-overflow-scrolling:touch}\n.ff{width:120px;font-size:0.58rem}\n.hm-name{width:120px;font-size:0.58rem}\n.hm-checks{display:none}\n.ov-check{width:50px}\n.ov-loc{max-width:120px}\n.ir{font-size:0.6rem}\n.ch-head{flex-wrap:wrap}\n.ch-g{font-size:1.5rem}\n.info-panel{font-size:0.68rem;padding:0.5rem 0.6rem}\n.ip-row{flex-direction:column;gap:0.1rem}\n.kvs{gap:0.4rem}\n.kv{font-size:0.62rem;padding:0.2rem 0.4rem}\n.arch-svg svg{min-width:400px}\n}\n@media(max-width:480px){\n.cats{grid-template-columns:1fr}\n.tn{padding:0 0.4rem;font-size:0.65rem}\n.ff{width:90px}\n.hm-name{width:90px}\n.ov-check{display:none}\n}\n";
|
package/dist/report/styles.js
CHANGED
|
@@ -1,36 +1,42 @@
|
|
|
1
1
|
/** All CSS for the HTML report, extracted for maintainability. */
|
|
2
2
|
export const CSS = `
|
|
3
|
-
:root{--bg:#09090b;--card:#111115;--border:#1e1e24;--text:#e5e5e5;--muted:#6b7280;--pass:#22c55e;--fail:#ef4444;--warn:#eab308;--info:#6366f1;--accent:#818cf8}
|
|
3
|
+
:root{--bg:#09090b;--card:#111115;--border:#1e1e24;--text:#e5e5e5;--muted:#6b7280;--pass:#22c55e;--fail:#ef4444;--warn:#eab308;--info:#6366f1;--accent:#818cf8;--side-w:200px;--top-h:42px}
|
|
4
4
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
5
5
|
body{font-family:"Inter",system-ui,sans-serif;background:var(--bg);color:var(--text);line-height:1.5}
|
|
6
6
|
code{font-family:"SF Mono",Menlo,monospace;font-size:0.85em}
|
|
7
7
|
|
|
8
|
-
/* Top nav */
|
|
9
|
-
.top{position:sticky;top:0;z-index:
|
|
10
|
-
.logo{font-weight:800;font-size:1rem;margin-right:
|
|
8
|
+
/* ── Top nav ── */
|
|
9
|
+
.top{position:sticky;top:0;z-index:30;background:#0c0c0fdd;backdrop-filter:blur(12px);border-bottom:1px solid var(--border);padding:0 1.5rem;display:flex;align-items:center;height:var(--top-h)}
|
|
10
|
+
.logo{font-weight:800;font-size:1rem;margin-right:1rem;flex-shrink:0;text-decoration:none;color:var(--text)}
|
|
11
11
|
.logo span{color:var(--accent)}
|
|
12
|
-
.
|
|
12
|
+
.nav-scroll{display:flex;align-items:center;gap:0;overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none;flex:1}
|
|
13
|
+
.nav-scroll::-webkit-scrollbar{display:none}
|
|
14
|
+
.tn{padding:0 0.7rem;font-size:0.78rem;color:var(--muted);text-decoration:none;border-bottom:2px solid transparent;transition:all 0.15s;white-space:nowrap;line-height:var(--top-h)}
|
|
13
15
|
.tn:hover{color:var(--text)}
|
|
14
16
|
.tn.active{color:var(--text);border-bottom-color:var(--accent)}
|
|
17
|
+
.hamburger{display:none;background:none;border:none;color:var(--muted);font-size:1.3rem;cursor:pointer;padding:0 0.4rem;line-height:var(--top-h)}
|
|
15
18
|
|
|
16
|
-
/* Sidebar */
|
|
17
|
-
.side{position:fixed;top:
|
|
19
|
+
/* ── Sidebar ── */
|
|
20
|
+
.side{position:fixed;top:var(--top-h);left:0;bottom:0;width:var(--side-w);background:#0c0c0f;border-right:1px solid var(--border);overflow-y:auto;padding:0.6rem 0;font-size:0.7rem;z-index:20}
|
|
18
21
|
.side-section{padding:0.3rem 0;border-bottom:1px solid var(--border)}
|
|
19
22
|
.side-section:last-child{border-bottom:none}
|
|
20
|
-
.side-
|
|
23
|
+
.side-label{padding:0.2rem 0.8rem;font-size:0.6rem;text-transform:uppercase;letter-spacing:0.05em;color:#444;font-weight:600}
|
|
24
|
+
.side-score{font-size:1.4rem;font-weight:900;padding:0.2rem 0.8rem}
|
|
21
25
|
.side-cat{display:block;padding:0.3rem 0.8rem;color:var(--text);font-weight:700;cursor:pointer;text-decoration:none;font-size:0.72rem}
|
|
22
26
|
.side-cat:hover{background:#14141a}
|
|
23
|
-
.side-
|
|
27
|
+
.side-cat-title{padding:0.3rem 0.8rem;font-size:0.65rem;text-transform:uppercase;letter-spacing:0.04em;color:var(--accent);font-weight:700}
|
|
28
|
+
.side-check{display:block;padding:0.15rem 0.8rem 0.15rem 0.8rem;color:var(--muted);cursor:pointer;text-decoration:none;font-size:0.65rem}
|
|
24
29
|
.side-check:hover{color:var(--text);background:#14141a}
|
|
25
|
-
.side-check span{display:inline-block;width:
|
|
30
|
+
.side-check span{display:inline-block;min-width:2.5rem;font-weight:700;font-size:0.6rem}
|
|
31
|
+
.side-stat{padding:0.15rem 0.8rem;font-size:0.7rem;color:var(--muted)}
|
|
32
|
+
.side-stat span{font-weight:800;font-size:0.8rem}
|
|
33
|
+
.side-views{padding-top:0.3rem}
|
|
34
|
+
.side-views .side-check{padding-left:0.8rem}
|
|
26
35
|
|
|
27
|
-
/* Content */
|
|
28
|
-
.content{
|
|
29
|
-
.page{display:none;animation:fadeIn 0.15s}
|
|
30
|
-
.page.active{display:block}
|
|
31
|
-
@keyframes fadeIn{from{opacity:0}to{opacity:1}}
|
|
36
|
+
/* ── Content ── */
|
|
37
|
+
.content{margin-left:var(--side-w);padding:1.5rem 2rem;max-width:960px}
|
|
32
38
|
|
|
33
|
-
/* Overview */
|
|
39
|
+
/* ── Overview ── */
|
|
34
40
|
.dash{display:flex;gap:2rem;margin-bottom:2rem;align-items:center;flex-wrap:wrap}
|
|
35
41
|
.hero{display:flex;align-items:center;gap:1rem}
|
|
36
42
|
.hero svg{width:100px;height:100px}
|
|
@@ -40,34 +46,52 @@ code{font-family:"SF Mono",Menlo,monospace;font-size:0.85em}
|
|
|
40
46
|
.hd{font-size:0.68rem;color:var(--muted)}
|
|
41
47
|
.radar{flex:1;display:flex;justify-content:center}
|
|
42
48
|
.radar svg{max-width:240px;width:100%}
|
|
43
|
-
.cats{display:grid;grid-template-columns:repeat(auto-fit,minmax(
|
|
44
|
-
.cc{background:var(--card);border:1px solid var(--border);border-radius:0.6rem;padding:0.8rem;
|
|
49
|
+
.cats{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:0.6rem;margin-bottom:2rem}
|
|
50
|
+
.cc{background:var(--card);border:1px solid var(--border);border-radius:0.6rem;padding:0.8rem;transition:border-color 0.15s;text-decoration:none;color:var(--text);display:block}
|
|
45
51
|
.cc:hover{border-color:var(--accent)}
|
|
46
52
|
.cc-s{font-size:1.8rem;font-weight:900}
|
|
47
53
|
.cc-l{font-size:0.75rem;color:var(--muted)}
|
|
48
54
|
.cc-m{margin-top:0.3rem;display:flex;gap:0.25rem}
|
|
49
55
|
.mc{font-size:0.65rem;font-weight:800}
|
|
50
56
|
h3{font-size:0.85rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.04em;margin-bottom:0.5rem}
|
|
57
|
+
|
|
58
|
+
/* ── Overview sections ── */
|
|
59
|
+
.ov-section{margin-bottom:1.5rem}
|
|
60
|
+
.ov-issue{font-size:0.68rem;font-family:"SF Mono",monospace;padding:0.2rem 0;display:flex;gap:0.4rem;align-items:baseline;border-bottom:1px solid var(--border)}
|
|
61
|
+
.ov-issue .is{flex-shrink:0}
|
|
62
|
+
.ov-issue.error .is{color:var(--fail)}
|
|
63
|
+
.ov-issue.warning .is{color:var(--warn)}
|
|
64
|
+
.ov-check{color:var(--muted);width:70px;flex-shrink:0;font-size:0.62rem}
|
|
65
|
+
.ov-loc{color:var(--accent);flex-shrink:0;font-size:0.62rem;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
66
|
+
.ov-msg{flex:1;word-break:break-word}
|
|
67
|
+
.ov-link{display:block;margin-top:0.5rem;font-size:0.72rem;color:var(--accent);text-decoration:none}
|
|
68
|
+
.ov-link:hover{text-decoration:underline}
|
|
69
|
+
|
|
70
|
+
/* ── Timeline ── */
|
|
71
|
+
.timeline{margin:0.5rem 0;overflow-x:auto}
|
|
72
|
+
.timeline svg{max-width:100%}
|
|
73
|
+
|
|
74
|
+
/* ── Bar chart ── */
|
|
51
75
|
.bars{margin-bottom:1.5rem}
|
|
52
76
|
.brow{display:flex;align-items:center;gap:0.4rem;margin-bottom:0.25rem;font-size:0.72rem}
|
|
53
|
-
.bl{width:
|
|
77
|
+
.bl{width:90px;text-align:right;color:var(--muted);flex-shrink:0}
|
|
54
78
|
.bb{flex:1;height:14px;background:var(--card);border-radius:3px;overflow:hidden;border:1px solid var(--border)}
|
|
55
79
|
.bf{height:100%;border-radius:2px}
|
|
56
80
|
.bv{width:36px;font-weight:700;font-size:0.68rem}
|
|
57
|
-
.stack{display:flex;gap:0.35rem;flex-wrap:wrap}
|
|
81
|
+
.stack{display:flex;gap:0.35rem;flex-wrap:wrap;margin-top:1rem}
|
|
58
82
|
.stack span{background:var(--card);border:1px solid var(--border);padding:0.1rem 0.45rem;border-radius:9999px;font-size:0.62rem;color:var(--muted)}
|
|
59
83
|
|
|
60
|
-
/* Category pages */
|
|
84
|
+
/* ── Category pages ── */
|
|
61
85
|
.cat-head{margin-bottom:0.3rem}
|
|
62
86
|
.bar2{height:4px;background:var(--card);border-radius:2px;margin-bottom:1rem;overflow:hidden}
|
|
63
87
|
.bf2{height:100%;border-radius:2px}
|
|
64
|
-
.sub-nav{display:flex;gap:0;border-bottom:1px solid var(--border);margin-bottom:1rem}
|
|
88
|
+
.sub-nav{display:flex;gap:0;border-bottom:1px solid var(--border);margin-bottom:1rem;flex-wrap:wrap}
|
|
65
89
|
.sn{padding:0.5rem 0.8rem;font-size:0.75rem;color:var(--muted);cursor:pointer;border-bottom:2px solid transparent}
|
|
66
90
|
.sn:hover{color:var(--text)}
|
|
67
91
|
.sn.active{color:var(--text);border-bottom-color:var(--accent)}
|
|
68
92
|
.sp{display:none}.sp.active{display:block}
|
|
69
93
|
|
|
70
|
-
/* Check detail */
|
|
94
|
+
/* ── Check detail ── */
|
|
71
95
|
.ch-head{display:flex;align-items:center;gap:0.7rem;margin-bottom:0.8rem}
|
|
72
96
|
.ch-g{font-size:2rem;font-weight:900}
|
|
73
97
|
.ch-s{display:block;font-size:0.7rem;color:var(--muted)}
|
|
@@ -82,7 +106,7 @@ h3{font-size:0.85rem;color:var(--muted);text-transform:uppercase;letter-spacing:
|
|
|
82
106
|
.k{color:var(--muted);margin-right:0.3rem}
|
|
83
107
|
.v{font-weight:600}
|
|
84
108
|
|
|
85
|
-
/* Issue list grouped by file */
|
|
109
|
+
/* ── Issue list grouped by file ── */
|
|
86
110
|
.iss-list{margin-top:1rem}
|
|
87
111
|
.fg{margin-bottom:0.8rem}
|
|
88
112
|
.fn{font-size:0.72rem;font-weight:600;font-family:"SF Mono",monospace;padding:0.3rem 0;border-bottom:1px solid var(--border);margin-bottom:0.2rem;display:flex;align-items:center;gap:0.5rem}
|
|
@@ -91,11 +115,12 @@ h3{font-size:0.85rem;color:var(--muted);text-transform:uppercase;letter-spacing:
|
|
|
91
115
|
.is{font-weight:800;font-size:0.55rem;width:0.9rem;text-align:center;border-radius:2px;flex-shrink:0}
|
|
92
116
|
.ir.error .is{color:var(--fail);background:#ef444418}
|
|
93
117
|
.ir.warning .is{color:var(--warn);background:#eab30818}
|
|
118
|
+
.ir.info .is{color:var(--info);background:#6366f118}
|
|
94
119
|
.il{color:var(--accent);min-width:2rem;flex-shrink:0}
|
|
95
120
|
.im{flex:1;word-break:break-word}
|
|
96
121
|
.iru{color:#555;font-size:0.55rem}
|
|
97
122
|
|
|
98
|
-
/* All issues table */
|
|
123
|
+
/* ── All issues table ── */
|
|
99
124
|
.isf{color:var(--muted);font-size:0.75rem;margin-bottom:0.8rem}
|
|
100
125
|
.it{width:100%;border-collapse:collapse;font-size:0.68rem}
|
|
101
126
|
.it th{text-align:left;padding:0.35rem 0.4rem;color:var(--muted);font-size:0.62rem;text-transform:uppercase;border-bottom:1px solid var(--border)}
|
|
@@ -107,24 +132,71 @@ h3{font-size:0.85rem;color:var(--muted);text-transform:uppercase;letter-spacing:
|
|
|
107
132
|
.il2{color:var(--muted)}
|
|
108
133
|
.iru2{color:#555;font-size:0.58rem}
|
|
109
134
|
|
|
110
|
-
/* File
|
|
135
|
+
/* ── File health ── */
|
|
111
136
|
.fr{display:flex;align-items:center;gap:0.5rem;margin-bottom:0.3rem;font-size:0.7rem}
|
|
112
137
|
.ff{width:200px;font-family:"SF Mono",monospace;font-size:0.65rem;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
113
138
|
.fb{flex:1;height:12px;background:var(--card);border-radius:3px;overflow:hidden;border:1px solid var(--border)}
|
|
114
139
|
.fbf{height:100%;border-radius:2px}
|
|
115
140
|
.fv{width:50px;font-size:0.65rem;color:var(--muted);flex-shrink:0}
|
|
116
|
-
.
|
|
141
|
+
.hm-row{display:flex;align-items:center;gap:0.5rem;margin-bottom:0.2rem;font-size:0.7rem}
|
|
142
|
+
.hm-name{width:200px;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:"SF Mono",monospace;font-size:0.65rem}
|
|
143
|
+
.hm-bar{height:14px;border-radius:3px;min-width:4px}
|
|
144
|
+
.hm-count{color:var(--muted);font-size:0.65rem;flex-shrink:0;min-width:50px}
|
|
145
|
+
.hm-checks{font-size:0.58rem;color:#555;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
146
|
+
|
|
147
|
+
/* ── Premium cards ── */
|
|
148
|
+
.pro-card{background:linear-gradient(135deg,#0f0f1a 0%,#13131f 100%);border:1px solid #2a2a3d;border-radius:0.75rem;padding:1.5rem;position:relative;overflow:hidden}
|
|
149
|
+
.pro-card::before{content:"";position:absolute;top:-50%;right:-50%;width:200%;height:200%;background:radial-gradient(circle,#6366f108 0%,transparent 70%);pointer-events:none}
|
|
150
|
+
.pro-badge{display:inline-block;background:linear-gradient(135deg,#6366f1,#818cf8);color:#fff;font-size:0.6rem;font-weight:800;padding:0.15rem 0.5rem;border-radius:9999px;letter-spacing:0.06em;margin-bottom:0.6rem}
|
|
151
|
+
.pro-desc{color:var(--muted);font-size:0.78rem;line-height:1.6;margin-bottom:0.8rem}
|
|
152
|
+
.pro-cta{color:#6366f1;font-size:0.72rem;font-weight:600;margin-top:1rem}
|
|
153
|
+
.sn-pro{opacity:0.7}
|
|
117
154
|
|
|
118
155
|
.footer{text-align:center;color:var(--muted);font-size:0.58rem;margin-top:2rem;padding:0.8rem 0;border-top:1px solid var(--border)}
|
|
119
156
|
.footer a{color:var(--muted)}
|
|
120
157
|
.flink{color:var(--accent);text-decoration:none;font-family:"SF Mono",monospace}.flink:hover{text-decoration:underline}
|
|
121
|
-
.arch-svg{margin:1rem 0;overflow-x:auto}
|
|
122
|
-
.hm-row{display:flex;align-items:center;gap:0.5rem;margin-bottom:0.2rem;font-size:0.7rem}
|
|
123
|
-
.hm-name{width:200px;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:"SF Mono",monospace;font-size:0.65rem}
|
|
124
|
-
.hm-bar{height:14px;border-radius:3px;min-width:4px}
|
|
125
|
-
.hm-count{color:var(--muted);font-size:0.65rem;flex-shrink:0}
|
|
158
|
+
.arch-svg{margin:1rem 0;overflow-x:auto;-webkit-overflow-scrolling:touch}
|
|
126
159
|
.arch-svg svg{border-radius:8px}
|
|
127
160
|
.cp-btn{background:none;border:none;cursor:pointer;font-size:0.6rem;opacity:0.3;padding:0 0.2rem;flex-shrink:0}.cp-btn:hover{opacity:1}
|
|
128
161
|
.ir:hover .cp-btn{opacity:0.6}
|
|
129
|
-
|
|
162
|
+
|
|
163
|
+
/* ── Mobile: hamburger collapses both navs ── */
|
|
164
|
+
@media(max-width:768px){
|
|
165
|
+
.hamburger{display:block}
|
|
166
|
+
.nav-scroll{display:none}
|
|
167
|
+
.nav-scroll.open{display:flex;position:absolute;top:var(--top-h);left:0;right:0;background:var(--bg);border-bottom:1px solid var(--border);flex-wrap:wrap;padding:0.3rem 0.5rem;z-index:25}
|
|
168
|
+
.side{display:none}
|
|
169
|
+
.side.open{display:block;z-index:25}
|
|
170
|
+
.top{padding:0 0.8rem}
|
|
171
|
+
.logo{font-size:0.85rem;margin-right:0.5rem}
|
|
172
|
+
.content{margin-left:0;padding:0.8rem}
|
|
173
|
+
.cats{grid-template-columns:1fr 1fr}
|
|
174
|
+
.dash{flex-direction:column;gap:1rem}
|
|
175
|
+
.hero svg{width:80px;height:80px}
|
|
176
|
+
.hg{font-size:2rem}
|
|
177
|
+
.radar svg{max-width:180px}
|
|
178
|
+
.bl{width:60px;font-size:0.62rem}
|
|
179
|
+
.bv{width:30px;font-size:0.6rem}
|
|
180
|
+
.it{display:block;overflow-x:auto;-webkit-overflow-scrolling:touch}
|
|
181
|
+
.ff{width:120px;font-size:0.58rem}
|
|
182
|
+
.hm-name{width:120px;font-size:0.58rem}
|
|
183
|
+
.hm-checks{display:none}
|
|
184
|
+
.ov-check{width:50px}
|
|
185
|
+
.ov-loc{max-width:120px}
|
|
186
|
+
.ir{font-size:0.6rem}
|
|
187
|
+
.ch-head{flex-wrap:wrap}
|
|
188
|
+
.ch-g{font-size:1.5rem}
|
|
189
|
+
.info-panel{font-size:0.68rem;padding:0.5rem 0.6rem}
|
|
190
|
+
.ip-row{flex-direction:column;gap:0.1rem}
|
|
191
|
+
.kvs{gap:0.4rem}
|
|
192
|
+
.kv{font-size:0.62rem;padding:0.2rem 0.4rem}
|
|
193
|
+
.arch-svg svg{min-width:400px}
|
|
194
|
+
}
|
|
195
|
+
@media(max-width:480px){
|
|
196
|
+
.cats{grid-template-columns:1fr}
|
|
197
|
+
.tn{padding:0 0.4rem;font-size:0.65rem}
|
|
198
|
+
.ff{width:90px}
|
|
199
|
+
.hm-name{width:90px}
|
|
200
|
+
.ov-check{display:none}
|
|
201
|
+
}
|
|
130
202
|
`;
|
package/dist/report/svg.d.ts
CHANGED
|
@@ -4,6 +4,23 @@ export declare function buildRadar(items: {
|
|
|
4
4
|
label: string;
|
|
5
5
|
score: number;
|
|
6
6
|
}[]): string;
|
|
7
|
+
/** Score timeline — larger chart showing score history over last N runs. */
|
|
8
|
+
export declare function buildTimeline(entries: {
|
|
9
|
+
score: number;
|
|
10
|
+
timestamp: string;
|
|
11
|
+
}[], opts?: {
|
|
12
|
+
width?: number;
|
|
13
|
+
height?: number;
|
|
14
|
+
}): string;
|
|
15
|
+
/** Testing pyramid — proportional triangle showing test layer distribution. */
|
|
16
|
+
export declare function buildPyramid(layers: {
|
|
17
|
+
unit: number;
|
|
18
|
+
integration: number;
|
|
19
|
+
component: number;
|
|
20
|
+
e2e: number;
|
|
21
|
+
}): string;
|
|
22
|
+
/** Badge SVG — shields.io-style badge for README embedding. */
|
|
23
|
+
export declare function buildBadge(score: number, grade: string): string;
|
|
7
24
|
/** Sparkline — mini line chart for trend display. */
|
|
8
25
|
export declare function buildSparkline(values: number[], opts?: {
|
|
9
26
|
width?: number;
|
package/dist/report/svg.js
CHANGED
|
@@ -42,6 +42,105 @@ export function buildRadar(items) {
|
|
|
42
42
|
}
|
|
43
43
|
return `<svg viewBox="0 0 240 240">${grid}${axes}<polygon points="${dataPts}" fill="#818cf825" stroke="#818cf8" stroke-width="1.5"/>${dots}</svg>`;
|
|
44
44
|
}
|
|
45
|
+
/** Score timeline — larger chart showing score history over last N runs. */
|
|
46
|
+
export function buildTimeline(entries, opts) {
|
|
47
|
+
const width = opts?.width ?? 600;
|
|
48
|
+
const height = opts?.height ?? 120;
|
|
49
|
+
const pad = { top: 20, right: 20, bottom: 25, left: 35 };
|
|
50
|
+
const w = width - pad.left - pad.right;
|
|
51
|
+
const h = height - pad.top - pad.bottom;
|
|
52
|
+
if (entries.length === 0)
|
|
53
|
+
return "";
|
|
54
|
+
// Y axis: 0-100 always
|
|
55
|
+
const yScale = (v) => pad.top + h - (v / 100) * h;
|
|
56
|
+
const xScale = (i) => pad.left + (entries.length === 1 ? w / 2 : (i / (entries.length - 1)) * w);
|
|
57
|
+
// Grid lines at 25, 50, 75
|
|
58
|
+
let grid = "";
|
|
59
|
+
for (const v of [25, 50, 75]) {
|
|
60
|
+
const y = yScale(v).toFixed(1);
|
|
61
|
+
grid += `<line x1="${pad.left}" y1="${y}" x2="${pad.left + w}" y2="${y}" stroke="#1e1e24" stroke-width="0.7"/>`;
|
|
62
|
+
grid += `<text x="${pad.left - 6}" y="${y}" text-anchor="end" dominant-baseline="middle" fill="#555" font-size="8">${v}</text>`;
|
|
63
|
+
}
|
|
64
|
+
// Score line + dots
|
|
65
|
+
const points = entries.map((e, i) => `${xScale(i).toFixed(1)},${yScale(e.score).toFixed(1)}`).join(" ");
|
|
66
|
+
// Grade colors per dot
|
|
67
|
+
const dots = entries
|
|
68
|
+
.map((e, i) => {
|
|
69
|
+
const color = e.score >= 90 ? "#22c55e" : e.score >= 75 ? "#84cc16" : e.score >= 60 ? "#eab308" : e.score >= 40 ? "#f97316" : "#ef4444";
|
|
70
|
+
return `<circle cx="${xScale(i).toFixed(1)}" cy="${yScale(e.score).toFixed(1)}" r="3" fill="${color}"><title>${e.timestamp.split("T")[0]} — ${e.score}</title></circle>`;
|
|
71
|
+
})
|
|
72
|
+
.join("");
|
|
73
|
+
// X-axis labels (first, middle, last)
|
|
74
|
+
let xLabels = "";
|
|
75
|
+
const labelIndices = entries.length <= 3 ? entries.map((_, i) => i) : [0, Math.floor(entries.length / 2), entries.length - 1];
|
|
76
|
+
for (const i of labelIndices) {
|
|
77
|
+
const label = entries[i].timestamp.split("T")[0].slice(5); // MM-DD
|
|
78
|
+
xLabels += `<text x="${xScale(i).toFixed(1)}" y="${height - 4}" text-anchor="middle" fill="#555" font-size="7">${label}</text>`;
|
|
79
|
+
}
|
|
80
|
+
// Gradient fill under the line
|
|
81
|
+
const areaPoints = `${xScale(0).toFixed(1)},${yScale(0).toFixed(1)} ${points} ${xScale(entries.length - 1).toFixed(1)},${yScale(0).toFixed(1)}`;
|
|
82
|
+
return `<svg viewBox="0 0 ${width} ${height}" width="${width}" height="${height}">
|
|
83
|
+
<defs><linearGradient id="tlg" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#818cf8" stop-opacity="0.3"/><stop offset="100%" stop-color="#818cf8" stop-opacity="0.02"/></linearGradient></defs>
|
|
84
|
+
${grid}
|
|
85
|
+
<polygon points="${areaPoints}" fill="url(#tlg)"/>
|
|
86
|
+
<polyline points="${points}" fill="none" stroke="#818cf8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
87
|
+
${dots}${xLabels}
|
|
88
|
+
</svg>`;
|
|
89
|
+
}
|
|
90
|
+
/** Testing pyramid — proportional triangle showing test layer distribution. */
|
|
91
|
+
export function buildPyramid(layers) {
|
|
92
|
+
const total = layers.unit + layers.integration + layers.component + layers.e2e;
|
|
93
|
+
if (total === 0)
|
|
94
|
+
return "";
|
|
95
|
+
const w = 200, h = 160;
|
|
96
|
+
const cx = w / 2;
|
|
97
|
+
// Pyramid: e2e at top (smallest), unit at bottom (largest)
|
|
98
|
+
// Each layer gets proportional height
|
|
99
|
+
const items = [
|
|
100
|
+
{ label: "E2E", count: layers.e2e, color: "#ef4444" },
|
|
101
|
+
{ label: "Component", count: layers.component, color: "#f97316" },
|
|
102
|
+
{ label: "Integration", count: layers.integration, color: "#eab308" },
|
|
103
|
+
{ label: "Unit", count: layers.unit, color: "#22c55e" },
|
|
104
|
+
];
|
|
105
|
+
const layerH = (h - 20) / 4;
|
|
106
|
+
let svg = "";
|
|
107
|
+
for (let i = 0; i < 4; i++) {
|
|
108
|
+
const item = items[i];
|
|
109
|
+
const y = 10 + i * layerH;
|
|
110
|
+
// Trapezoid: wider at bottom
|
|
111
|
+
const topW = ((i + 0.5) / 4) * (w - 40);
|
|
112
|
+
const botW = ((i + 1.5) / 4) * (w - 40);
|
|
113
|
+
const opacity = item.count > 0 ? 1 : 0.2;
|
|
114
|
+
const x1t = cx - topW / 2, x2t = cx + topW / 2;
|
|
115
|
+
const x1b = cx - botW / 2, x2b = cx + botW / 2;
|
|
116
|
+
svg += `<polygon points="${x1t},${y} ${x2t},${y} ${x2b},${y + layerH} ${x1b},${y + layerH}" fill="${item.color}" opacity="${opacity * 0.25}" stroke="${item.color}" stroke-opacity="${opacity * 0.6}" stroke-width="1"/>`;
|
|
117
|
+
svg += `<text x="${cx}" y="${y + layerH / 2 + 3}" text-anchor="middle" fill="${item.count > 0 ? "#e5e5e5" : "#555"}" font-size="9" font-weight="600">${item.label} (${item.count})</text>`;
|
|
118
|
+
}
|
|
119
|
+
return `<svg viewBox="0 0 ${w} ${h}" width="${w}" height="${h}">${svg}</svg>`;
|
|
120
|
+
}
|
|
121
|
+
/** Badge SVG — shields.io-style badge for README embedding. */
|
|
122
|
+
export function buildBadge(score, grade) {
|
|
123
|
+
const color = score >= 90 ? "#22c55e" : score >= 75 ? "#84cc16" : score >= 60 ? "#eab308" : score >= 40 ? "#f97316" : "#ef4444";
|
|
124
|
+
const label = "vcqa";
|
|
125
|
+
const value = `${grade} ${score}`;
|
|
126
|
+
const labelW = 36, valueW = 44, totalW = labelW + valueW;
|
|
127
|
+
const h = 20, r = 3;
|
|
128
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${totalW}" height="${h}">
|
|
129
|
+
<linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient>
|
|
130
|
+
<clipPath id="r"><rect width="${totalW}" height="${h}" rx="${r}" fill="#fff"/></clipPath>
|
|
131
|
+
<g clip-path="url(#r)">
|
|
132
|
+
<rect width="${labelW}" height="${h}" fill="#555"/>
|
|
133
|
+
<rect x="${labelW}" width="${valueW}" height="${h}" fill="${color}"/>
|
|
134
|
+
<rect width="${totalW}" height="${h}" fill="url(#s)"/>
|
|
135
|
+
</g>
|
|
136
|
+
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11">
|
|
137
|
+
<text x="${labelW / 2}" y="14" fill="#010101" fill-opacity=".3">${label}</text>
|
|
138
|
+
<text x="${labelW / 2}" y="13">${label}</text>
|
|
139
|
+
<text x="${labelW + valueW / 2}" y="14" fill="#010101" fill-opacity=".3">${value}</text>
|
|
140
|
+
<text x="${labelW + valueW / 2}" y="13">${value}</text>
|
|
141
|
+
</g>
|
|
142
|
+
</svg>`;
|
|
143
|
+
}
|
|
45
144
|
/** Sparkline — mini line chart for trend display. */
|
|
46
145
|
export function buildSparkline(values, opts) {
|
|
47
146
|
const width = opts?.width ?? 120;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/** Accessibility check — detects common a11y violations in JSX/TSX code. */
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { gradeFromScore } from "../types.js";
|
|
5
|
+
import { getProductionFiles } from "../fs-utils.js";
|
|
6
|
+
export function runAccessibility(cwd) {
|
|
7
|
+
const start = Date.now();
|
|
8
|
+
const files = getProductionFiles(cwd).filter((f) => f.ext === ".tsx" || f.ext === ".jsx");
|
|
9
|
+
if (files.length === 0) {
|
|
10
|
+
return { name: "accessibility", score: 100, grade: "A", details: { skipped: true, reason: "no JSX/TSX files" }, issues: [], duration: Date.now() - start };
|
|
11
|
+
}
|
|
12
|
+
const issues = [];
|
|
13
|
+
let missingAlt = 0;
|
|
14
|
+
let clickDiv = 0;
|
|
15
|
+
let missingLabel = 0;
|
|
16
|
+
let missingLang = 0;
|
|
17
|
+
let autofocus = 0;
|
|
18
|
+
let positiveTabindex = 0;
|
|
19
|
+
for (const f of files) {
|
|
20
|
+
const lines = f.content.split("\n");
|
|
21
|
+
for (let i = 0; i < lines.length; i++) {
|
|
22
|
+
const line = lines[i];
|
|
23
|
+
const trimmed = line.trim();
|
|
24
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*"))
|
|
25
|
+
continue;
|
|
26
|
+
// 1. <img> without alt
|
|
27
|
+
if (/<img\b/.test(trimmed) && !/alt=/.test(trimmed)) {
|
|
28
|
+
const block = lines.slice(i, Math.min(i + 5, lines.length)).join(" ");
|
|
29
|
+
if (/<img\b/.test(block) && !/alt=/.test(block)) {
|
|
30
|
+
missingAlt++;
|
|
31
|
+
issues.push({ severity: "error", message: "<img> missing alt attribute", file: f.path, line: i + 1, rule: "img-alt" });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// 2. Click handler on non-interactive element without role/keyboard
|
|
35
|
+
if (/onClick=/.test(trimmed) && /<(?:div|span|p|li|section|article|header|footer)\b/.test(trimmed)) {
|
|
36
|
+
const block = lines.slice(i, Math.min(i + 3, lines.length)).join(" ");
|
|
37
|
+
if (!(/role=/.test(block) && /(?:onKeyDown|onKeyUp|onKeyPress|tabIndex)/.test(block))) {
|
|
38
|
+
clickDiv++;
|
|
39
|
+
issues.push({ severity: "warning", message: "Click handler on non-interactive element without role + keyboard handler", file: f.path, line: i + 1, rule: "click-events" });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// 3. <input>/<select>/<textarea> without associated label
|
|
43
|
+
if (/<(?:input|select|textarea)\b/.test(trimmed) && !/type=["'](?:hidden|submit|button|reset)["']/.test(trimmed)) {
|
|
44
|
+
const block = lines.slice(Math.max(0, i - 3), Math.min(i + 3, lines.length)).join(" ");
|
|
45
|
+
if (!/aria-label=/.test(block) && !/aria-labelledby=/.test(block) && !/<label/.test(block) && !/id=/.test(trimmed)) {
|
|
46
|
+
missingLabel++;
|
|
47
|
+
issues.push({ severity: "warning", message: "Form control without label, aria-label, or aria-labelledby", file: f.path, line: i + 1, rule: "form-label" });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// 4. autoFocus
|
|
51
|
+
if (/\bautoFocus\b/.test(trimmed) || /\bautofocus\b/.test(trimmed)) {
|
|
52
|
+
autofocus++;
|
|
53
|
+
issues.push({ severity: "warning", message: "autoFocus can disorient screen reader users", file: f.path, line: i + 1, rule: "no-autofocus" });
|
|
54
|
+
}
|
|
55
|
+
// 5. Positive tabIndex
|
|
56
|
+
if (/tabIndex=\{[1-9]/.test(trimmed) || /tabindex=["'][1-9]/.test(trimmed)) {
|
|
57
|
+
positiveTabindex++;
|
|
58
|
+
issues.push({ severity: "warning", message: "Positive tabIndex disrupts natural tab order — use 0 or -1", file: f.path, line: i + 1, rule: "tabindex" });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// 6. Check for html lang attribute in index.html
|
|
63
|
+
const htmlPaths = ["index.html", "web/index.html", "public/index.html"];
|
|
64
|
+
for (const h of htmlPaths) {
|
|
65
|
+
const full = join(cwd, h);
|
|
66
|
+
if (!existsSync(full))
|
|
67
|
+
continue;
|
|
68
|
+
const content = readFileSync(full, "utf-8");
|
|
69
|
+
if (/<html\b/.test(content) && !/<html[^>]*lang=/.test(content)) {
|
|
70
|
+
missingLang++;
|
|
71
|
+
issues.push({ severity: "warning", message: "<html> missing lang attribute", file: h, rule: "html-lang" });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const errors = issues.filter((i) => i.severity === "error").length;
|
|
75
|
+
const warnings = issues.filter((i) => i.severity === "warning").length;
|
|
76
|
+
const score = Math.max(0, Math.min(100, 100 - errors * 10 - warnings * 4));
|
|
77
|
+
return {
|
|
78
|
+
name: "accessibility",
|
|
79
|
+
score,
|
|
80
|
+
grade: gradeFromScore(score),
|
|
81
|
+
details: { jsxFiles: files.length, missingAlt, clickDiv, missingLabel, missingLang, autofocus, positiveTabindex },
|
|
82
|
+
issues,
|
|
83
|
+
duration: Date.now() - start,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -25,4 +25,6 @@ export interface ArchGraph {
|
|
|
25
25
|
}
|
|
26
26
|
export declare function runArchitecture(cwd: string): CheckResult;
|
|
27
27
|
export declare function generateArchSVG(details: Record<string, unknown>): string;
|
|
28
|
+
export declare function generateDSM(details: Record<string, unknown>): string;
|
|
29
|
+
export declare function generatePackageDiagram(details: Record<string, unknown>): string;
|
|
28
30
|
export {};
|