dependency-radar 0.3.1 → 0.5.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/dist/report.js CHANGED
@@ -6,15 +6,50 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.renderReport = renderReport;
7
7
  const promises_1 = __importDefault(require("fs/promises"));
8
8
  const path_1 = __importDefault(require("path"));
9
+ const cta_1 = require("./cta");
9
10
  const report_assets_1 = require("./report-assets");
11
+ /**
12
+ * Escape occurrences of closing `</style` tags in a CSS payload to prevent premature termination when inlined into HTML.
13
+ *
14
+ * @param value - The CSS text to sanitize
15
+ * @returns The sanitized string with each `</style` sequence replaced by `<\/style` (case-insensitive)
16
+ */
17
+ function sanitizeInlineStyleTagPayload(value) {
18
+ return value.replace(/<\/style/gi, '<\\/style');
19
+ }
20
+ /**
21
+ * Escapes closing `</script` sequences so a string can be embedded safely inside an inline `<script>` tag.
22
+ *
23
+ * @param value - The script content to sanitize
24
+ * @returns The input with every `</script` (case-insensitive) replaced by `<\/script`
25
+ */
26
+ function sanitizeInlineScriptTagPayload(value) {
27
+ return value.replace(/<\/script/gi, '<\\/script');
28
+ }
29
+ /**
30
+ * Generate the HTML report from aggregated data and write it to the given file path.
31
+ *
32
+ * @param data - Aggregated radar data used to build the report
33
+ * @param outputPath - Filesystem path where the generated HTML report will be written; parent directories are created if missing
34
+ */
10
35
  async function renderReport(data, outputPath) {
11
36
  const html = buildHtml(data);
12
37
  await promises_1.default.mkdir(path_1.default.dirname(outputPath), { recursive: true });
13
38
  await promises_1.default.writeFile(outputPath, html, 'utf8');
14
39
  }
40
+ /**
41
+ * Build a complete HTML report string populated from the provided aggregated data.
42
+ *
43
+ * The returned document embeds sanitized CSS and JS assets, a JSON-serialized copy of `data` (with `<` characters escaped), a computed CTA URL derived from `data.dependencyRadarVersion`, and a human-friendly formatted `generatedAt` timestamp when parsable. Dynamic interpolations that appear in the HTML (e.g., project path, formatted date, CTA URL) are HTML-escaped.
44
+ *
45
+ * @param data - Aggregated data used to populate the report (includes project metadata, generatedAt timestamp, dependencyRadarVersion, and dependency list)
46
+ * @returns The full HTML document for the dependency radar report as a string
47
+ */
15
48
  function buildHtml(data) {
16
- var _a, _b, _c;
17
49
  const json = JSON.stringify(data).replace(/</g, '\\u003c');
50
+ const ctaUrl = (0, cta_1.buildCtaUrl)(data.dependencyRadarVersion);
51
+ const safeCssContent = sanitizeInlineStyleTagPayload(report_assets_1.CSS_CONTENT);
52
+ const safeJsContent = sanitizeInlineScriptTagPayload(report_assets_1.JS_CONTENT);
18
53
  // Format the generated date
19
54
  let formattedDate = data.generatedAt;
20
55
  try {
@@ -35,25 +70,57 @@ function buildHtml(data) {
35
70
  catch {
36
71
  // Keep the original if parsing fails
37
72
  }
38
- // Build conditional meta items
39
- const runtimeVersion = ((_a = data.environment) === null || _a === void 0 ? void 0 : _a.runtimeVersion)
40
- ? data.environment.runtimeVersion.replace(/^v/, '')
41
- : null;
42
- const minRequiredMajor = (_b = data.environment) === null || _b === void 0 ? void 0 : _b.minRequiredMajor;
43
- const nodeVersionText = runtimeVersion
44
- ? `${runtimeVersion}${minRequiredMajor && minRequiredMajor > 0 ? ` (requires ≥${minRequiredMajor})` : ''}`
45
- : null;
46
- const nodeDisclaimer = minRequiredMajor && minRequiredMajor > 0
47
- ? 'Node requirement derived from dependency engine ranges.'
48
- : null;
49
73
  return `<!doctype html>
50
74
  <html lang="en">
51
75
  <head>
52
76
  <meta charset="UTF-8" />
53
77
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
54
78
  <title>Dependency Radar</title>
79
+ <link
80
+ rel="icon"
81
+ type="image/svg+xml"
82
+ href="data:image/svg+xml;utf8,
83
+ <svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' version='1.1' viewBox='0 0 1024 1024'>
84
+ <defs>
85
+ <style>
86
+ .st0, .st1 {fill: %23ff8000;}
87
+ .st2, .st3 {fill: %2340ff40;}
88
+ .st3, .st4, .st1 {opacity: .4;}
89
+ .st5 {fill: url(%23linear-gradient);}
90
+ .st4, .st6 {fill: red;}
91
+ .st7 {stroke-width: 16px;}
92
+ .st7, .st8 {fill: %23171772;}
93
+ .st7, .st8, .st9 {stroke: %2355fffa; stroke-miterlimit: 10;}
94
+ .st8 {stroke-width: 10px;}
95
+ .st10 {fill: %2314145e;}
96
+ .st9 {fill: none; opacity: .3; stroke-width: 6px;}
97
+ .st11 {fill: %2355fffa;}
98
+ </style>
99
+ <linearGradient id='linear-gradient' x1='239.3' y1='298.2' x2='815.3' y2='298.2' gradientTransform='translate(150 -115.1) rotate(15)' gradientUnits='userSpaceOnUse'>
100
+ <stop offset='.4' stop-color='%2355fffa' stop-opacity='0'/>
101
+ <stop offset='1' stop-color='%2355fffa' stop-opacity='.5'/>
102
+ </linearGradient>
103
+ </defs>
104
+ <circle class='st10' cx='512' cy='512' r='512'/>
105
+ <circle class='st7' cx='512' cy='512' r='430'/>
106
+ <circle class='st8' cx='512' cy='512' r='256'/>
107
+ <circle class='st9' cx='512' cy='512' r='160'/>
108
+ <circle class='st9' cx='512' cy='512' r='379.5'/>
109
+ <circle class='st9' cx='512' cy='512' r='339'/>
110
+ <circle class='st9' cx='512' cy='512' r='210'/>
111
+ <rect class='st11' x='690.2' y='193.1' width='15.8' height='427.6' transform='translate(701.4 -401.1) rotate(60)'/>
112
+ <circle class='st11' cx='512' cy='514.4' r='64'/>
113
+ <path class='st5' d='M517.2,513.4l365.8-213.9c-54.6-95.4-145.8-169.8-260.3-200.5-100.1-26.8-201.5-15.8-288.8,24.3l183.4,390Z'/>
114
+ <circle class='st4' cx='512' cy='256' r='96'/>
115
+ <circle class='st6' cx='512' cy='256' r='56'/>
116
+ <circle class='st3' cx='733.7' cy='640' r='96' transform='translate(-237.7 706.3) rotate(-45)'/>
117
+ <circle class='st2' cx='733.7' cy='640' r='56' transform='translate(-237.7 706.3) rotate(-45)'/>
118
+ <ellipse class='st1' cx='290.3' cy='640' rx='96' ry='96' transform='translate(-367.5 392.7) rotate(-45)'/>
119
+ <circle class='st0' cx='290.3' cy='640' r='56'/>
120
+ </svg>"
121
+ >
55
122
  <style>
56
- ${report_assets_1.CSS_CONTENT}
123
+ ${safeCssContent}
57
124
  </style>
58
125
  </head>
59
126
  <body>
@@ -61,79 +128,62 @@ ${report_assets_1.CSS_CONTENT}
61
128
  <header class="top-header">
62
129
  <div class="header-row">
63
130
  <div class="header-content">
64
- <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"
65
- viewBox="0 0 1024 1024" class="logo">
131
+ <svg class="logo" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 1024 1024">
66
132
  <defs>
67
133
  <style>
68
- .st0, .st1 { fill: #ff8000; }
69
- .st2 { fill: #191772; }
70
- .st2, .st3, .st4, .st5 { stroke: #55fffa; stroke-miterlimit: 10; stroke-width: 6px; }
71
- .st6 { fill: #0a0a33; }
72
- .st7 { opacity: .3; }
73
- .st7, .st8 { fill: #55fffa; }
74
- .st3 { fill: #161466; }
75
- .st9 { fill: url(#linear-gradient); }
76
- .st10, .st1, .st11 { opacity: .4; }
77
- .st10, .st12 { fill: red; }
78
- .st4 { fill: #1c197f; }
79
- .st13, .st11 { fill: #00be00; }
80
- .st5 { fill: #141259; }
134
+ .st0, .st1 {fill: #ff8000;}
135
+ .st2, .st3 {fill: #40ff40;}
136
+ .st3, .st4, .st1 {opacity: .4;}
137
+ .st5 {fill: url(#linear-gradient);}
138
+ .st4, .st6 {fill: red;}
139
+ .st7 {stroke-width: 16px;}
140
+ .st7, .st8 {fill: #171772;}
141
+ .st7, .st8, .st9 {stroke: #55fffa; stroke-miterlimit: 10;}
142
+ .st8 {stroke-width: 10px;}
143
+ .st10 {fill: #14145e;}
144
+ .st9 {fill: none; opacity: .3; stroke-width: 6px;}
145
+ .st11 {fill: #55fffa;}
81
146
  </style>
82
- <linearGradient id="linear-gradient" x1="225" y1="287" x2="831.3" y2="287" gradientUnits="userSpaceOnUse">
83
- <stop offset=".4" stop-color="#55fffa" stop-opacity="0" />
84
- <stop offset="1" stop-color="#55fffa" stop-opacity=".5" />
147
+ <linearGradient id="linear-gradient" x1="239.3" y1="298.2" x2="815.3" y2="298.2" gradientTransform="translate(150 -115.1) rotate(15)" gradientUnits="userSpaceOnUse">
148
+ <stop offset=".4" stop-color="#55fffa" stop-opacity="0"/>
149
+ <stop offset="1" stop-color="#55fffa" stop-opacity=".5"/>
85
150
  </linearGradient>
86
151
  </defs>
87
- <circle class="st6" cx="512" cy="512" r="512" />
88
- <circle class="st5" cx="512" cy="512" r="450" />
89
- <circle class="st3" cx="512" cy="512" r="325" />
90
- <circle class="st2" cx="512" cy="512" r="200" />
91
- <circle class="st4" cx="512" cy="512" r="80" />
92
- <path class="st9" d="M517.7,512l313.6-317.1c-81.5-82.1-194.5-132.9-319.3-132.9s-209.1,38.8-287,103.4l292.7,346.6Z" />
93
- <path class="st7" d="M891.9,618.4c-64.1,245.1-337.5,365.9-562.6,250.5,0,0-14.3-7.7-14.3-7.7-5.8-3.4-11.7-7.1-17.4-10.5-5.3-3.5-11.7-7.9-16.9-11.4-15-10.9-30.5-23.6-43.9-36.5-5.7-5.3-11.8-11.8-17.3-17.4-37-40.1-66.2-88-84.1-139.6-38.9-110.4-27.2-234.3,31.1-335.8,37.6-65.3,93.5-119.6,159.6-155.7,0,0,15-7.7,15-7.7,4.5-2.4,10.7-4.9,15.3-7.1,2.1-.9,5.6-2.6,7.7-3.4,3.9-1.5,11.8-4.7,15.7-6.2,4.7-1.8,11.1-3.8,15.9-5.4,0,0,4-1.3,4-1.3,6.7-1.8,13.5-4,20.3-5.7,116.4-29.7,241.1-7.4,339.7,61.6,18.6,13.1,36.2,27.6,52.6,43.4l-42.9,42.9c-17.1-17.4-35.9-33.2-55.9-47.1-4.7-2.9-13.9-9.3-18.6-11.7-3.1-1.8-8.1-4.7-11.1-6.4-4-2-10.7-5.5-14.7-7.6-5.1-2.4-11.6-5.3-16.7-7.6-5.7-2.3-11.4-4.5-17.1-6.8-60.2-21.9-126.3-27.5-189.3-16.1,0,0-14.6,2.9-14.6,2.9-6,1.4-12,3.1-18,4.5-5.5,1.7-12.3,3.7-17.8,5.5-4.4,1.5-11.4,4.1-15.8,5.7-3,1.2-9,3.7-12,5-43.3,18.6-83.4,46-116.3,79.8-105.7,106.7-134.5,269.3-74.7,406.7,49.8,114.5,155.4,198.1,278.4,219.7,94.7,17.4,195.6-3.1,276-56.1,76.9-50.4,135.7-128.5,160.8-217.2h0Z" />
94
- <path class="st7" d="M770.9,586c-20.5,84.7-85.9,156.4-167.8,185.8-103.6,37.8-221.1,7.9-294.5-74.2-73.1-80.1-91.2-199-47.6-298,28-63.9,80.3-116.6,144.1-144.9,91-41.1,199.6-30.9,281.6,26,13.1,9.1,25.5,19.2,37,30.2,0,0-42.9,42.9-42.9,42.9-21.5-22.4-47.3-40.6-75.7-53.1-18.3-7.8-37.9-13.6-57.6-16.7-38.4-6-78.3-2.5-115,10.4-34,11.7-65.3,31.6-90.7,57.1-89.3,88.8-94.6,233.1-14.3,329.6,37,45.2,90.4,76.8,147.9,87.3,130.1,24.7,258-55.6,295.4-182.4h0Z" />
95
- <path class="st7" d="M649.9,553.6c-13.4,61.2-70.2,107.5-133.1,109.4-48.9,2.2-96.8-21.6-125.4-61.3-75.1-106.5,4.4-248.5,133.3-247.2,40.8.5,80.9,16.7,110.7,44.7,0,0-42.9,42.9-42.9,42.9-11.9-13.1-27-23.5-43.8-29.5-28.9-10.5-62-8.3-89.4,5.9-61.5,31.6-81.6,109.8-45.9,168.4,17.8,29.7,48.4,51.3,82.3,58.2,66.3,14.4,134-26.8,154.1-91.6h0Z" />
96
- <rect class="st8" x="664.2" y="129.5" width="16.7" height="450" transform="translate(447.6 -371.7) rotate(45)" />
97
- <circle class="st8" cx="512" cy="512" r="32" />
98
- <circle class="st10" cx="800" cy="662" r="50" />
99
- <circle class="st12" cx="800" cy="662" r="25" />
100
- <circle class="st10" cx="256.9" cy="315.2" r="50" />
101
- <circle class="st12" cx="256.9" cy="315.2" r="25" />
102
- <circle class="st1" cx="400.1" cy="673" r="50" />
103
- <circle class="st0" cx="400.1" cy="673" r="25" />
104
- <circle class="st1" cx="578.1" cy="135" r="50" />
105
- <circle class="st0" cx="578.1" cy="135" r="25" />
106
- <circle class="st11" cx="187" cy="569.5" r="50" />
107
- <circle class="st13" cx="187" cy="569.5" r="25" />
108
- <circle class="st11" cx="592" cy="894.1" r="50" />
109
- <circle class="st13" cx="592" cy="894.1" r="25" />
110
- <circle class="st11" cx="512" cy="314" r="50" />
111
- <circle class="st13" cx="512" cy="314" r="25" />
112
- <circle class="st10" cx="329" cy="854.6" r="50" />
113
- <circle class="st12" cx="329" cy="854.6" r="25" />
152
+ <circle class="st10" cx="512" cy="512" r="512"/>
153
+ <circle class="st7" cx="512" cy="512" r="430"/>
154
+ <circle class="st8" cx="512" cy="512" r="256"/>
155
+ <circle class="st9" cx="512" cy="512" r="160"/>
156
+ <circle class="st9" cx="512" cy="512" r="379.5"/>
157
+ <circle class="st9" cx="512" cy="512" r="339"/>
158
+ <circle class="st9" cx="512" cy="512" r="210"/>
159
+ <rect class="st11" x="690.2" y="193.1" width="15.8" height="427.6" transform="translate(701.4 -401.1) rotate(60)"/>
160
+ <circle class="st11" cx="512" cy="514.4" r="64"/>
161
+ <path class="st5" d="M517.2,513.4l365.8-213.9c-54.6-95.4-145.8-169.8-260.3-200.5-100.1-26.8-201.5-15.8-288.8,24.3l183.4,390Z"/>
162
+ <circle class="st4" cx="512" cy="256" r="96"/>
163
+ <circle class="st6" cx="512" cy="256" r="56"/>
164
+ <circle class="st3" cx="733.7" cy="640" r="96" transform="translate(-237.7 706.3) rotate(-45)"/>
165
+ <circle class="st2" cx="733.7" cy="640" r="56" transform="translate(-237.7 706.3) rotate(-45)"/>
166
+ <ellipse class="st1" cx="290.3" cy="640" rx="96" ry="96" transform="translate(-367.5 392.7) rotate(-45)"/>
167
+ <circle class="st0" cx="290.3" cy="640" r="56"/>
114
168
  </svg>
115
169
  <div class="header-text">
116
170
  <h1>Dependency Radar</h1>
117
171
  <div class="header-meta">
118
172
  <span class="meta-item"><span class="meta-label">Project</span> <strong id="project-path">${escapeHtml(data.project.projectDir)}</strong></span>
119
- ${((_c = data.git) === null || _c === void 0 ? void 0 : _c.branch) ? `<span class="meta-item"><span class="meta-label">Branch</span> <strong>${escapeHtml(data.git.branch)}</strong></span>` : ''}
120
- ${nodeVersionText ? `<span class="meta-item"><span class="meta-label">Node</span> <strong>${escapeHtml(nodeVersionText)}</strong></span>` : ''}
173
+ <span class="meta-item" id="git-branch-item" style="display: none;"><span class="meta-label">Branch</span> <strong id="git-branch"></strong></span>
174
+ <span class="meta-item" id="node-item" style="display: none;"><span class="meta-label">Node</span> <strong id="node-version"></strong></span>
121
175
  <span class="meta-item"><span class="meta-label">Generated</span> <strong id="formatted-date">${escapeHtml(formattedDate)}</strong></span>
122
- ${nodeDisclaimer ? `<span class="header-disclaimer">${escapeHtml(nodeDisclaimer)}</span>` : ''}
176
+ <span class="header-disclaimer" id="node-disclaimer" style="display: none;"></span>
123
177
  </div>
124
178
  </div>
125
179
  </div>
126
180
  <div class="cta-section">
127
- <a href="https://dependency-radar.com" class="cta-link" target="_blank" rel="noopener">
128
- Get full analysis report
181
+ <a href="${escapeHtml(ctaUrl)}" class="cta-link" target="_blank" rel="noopener" id="cta-primary-link">
182
+ Enrich this scan
129
183
  <span class="cta-arrow">→</span>
130
184
  </a>
131
- <div class="cta-benefits">
132
- <span>AI-powered risk summary for stakeholders</span>
133
- <span>Charts & assets for presentations</span>
134
- <span>Actionable upgrade recommendations</span>
135
- </div>
136
- <div class="cta-text">dependency-radar.com</div>
185
+ <p class="cta-text">Beyond the standalone report</p>
186
+ <a href="${escapeHtml(ctaUrl)}" target="_blank" rel="noopener" class="cta-url" id="cta-secondary-link">dependency-radar.com</a>
137
187
  </div>
138
188
  </div>
139
189
  </header>
@@ -141,90 +191,127 @@ ${report_assets_1.CSS_CONTENT}
141
191
  <!-- Sticky Filter Bar -->
142
192
  <div class="filter-bar">
143
193
  <div class="filter-bar-inner">
144
- <div class="search-wrapper">
145
- <svg class="search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
146
- <circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
147
- </svg>
148
- <input type="search" id="search" placeholder="Search packages..." />
149
- </div>
150
-
151
- <div class="filter-group">
152
- <span class="filter-label">Type</span>
153
- <select id="direct-filter">
154
- <option value="all">All</option>
155
- <option value="direct">Dependency</option>
156
- <option value="transitive">Sub-Dependency</option>
157
- </select>
158
- </div>
159
-
160
- <div class="filter-group">
161
- <span class="filter-label">Scope</span>
162
- <select id="runtime-filter">
163
- <option value="all">All</option>
164
- <option value="runtime">Runtime</option>
165
- <option value="dev">Dev</option>
166
- <option value="optional">Optional</option>
167
- <option value="peer">Peer</option>
168
- </select>
169
- </div>
170
-
171
- <div class="filter-group sort-wrapper">
172
- <span class="filter-label">Sort</span>
173
- <select id="sort-by">
174
- <option value="name">Name</option>
175
- <option value="severity">Severity</option>
176
- <option value="depth">Depth</option>
177
- </select>
178
- <button type="button" class="sort-direction-btn" id="sort-direction" title="Toggle sort direction">↑</button>
179
- </div>
180
-
181
- <button type="button" class="license-filter-toggle" id="license-toggle">
182
- License Categories
183
- <span class="chevron">▼</span>
184
- </button>
185
-
186
- <label class="checkbox-filter">
187
- <input type="checkbox" id="has-vulns" />
188
- Has vulnerabilities
189
- </label>
190
-
191
- <div class="theme-toggle">
192
- <span class="theme-toggle-label">Theme</span>
193
- <div class="theme-switch" id="theme-switch" title="Toggle dark/light mode"></div>
194
- </div>
195
- </div>
196
-
197
- <!-- Collapsible License Filter Panel (inside sticky bar) -->
198
- <div class="license-filter-panel" id="license-panel">
199
- <div class="license-filter-inner">
200
- <div class="license-filter-header">
201
- <span class="license-filter-title">Filter by License Type</span>
202
- <div class="license-quick-actions">
203
- <button type="button" class="quick-action-btn" id="license-all">Show All</button>
204
- <button type="button" class="quick-action-btn" id="license-friendly">Business-Friendly Only</button>
194
+ <div class="filter-row">
195
+ <div class="filter-top-row">
196
+ <div class="search-wrapper">
197
+ <svg class="search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
198
+ <circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
199
+ </svg>
200
+ <input type="search" id="search" placeholder="Search packages..." />
201
+ </div>
202
+ <button
203
+ type="button"
204
+ class="filters-toggle"
205
+ id="filters-toggle"
206
+ aria-expanded="false"
207
+ >
208
+ Filters
209
+ <span class="chevron">▼</span>
210
+ </button>
211
+ </div>
212
+
213
+ <div class="filter-controls" id="filter-controls">
214
+ <div class="filter-controls-row">
215
+ <div class="filter-group">
216
+ <span class="filter-label">Type</span>
217
+ <select id="direct-filter">
218
+ <option value="all">All</option>
219
+ <option value="direct">Dependency</option>
220
+ <option value="transitive">Sub-Dependency</option>
221
+ </select>
222
+ </div>
223
+
224
+ <div class="filter-group">
225
+ <span class="filter-label">Scope</span>
226
+ <select id="runtime-filter">
227
+ <option value="all">All</option>
228
+ <option value="runtime">Runtime</option>
229
+ <option value="dev">Dev</option>
230
+ <option value="optional">Optional</option>
231
+ <option value="peer">Peer</option>
232
+ </select>
233
+ </div>
234
+
235
+ <button type="button" class="license-filter-toggle" id="license-toggle">
236
+ License Categories
237
+ <span class="chevron">▼</span>
238
+ </button>
239
+
240
+ <label class="checkbox-filter">
241
+ <input type="checkbox" id="has-vulns" />
242
+ Has vulnerabilities
243
+ </label>
244
+
245
+ <!-- Sort dropdown - visible on mobile, hidden on desktop (replaced by column headers) -->
246
+ <div class="filter-group sort-wrapper mobile-only" id="mobile-sort">
247
+ <span class="filter-label">SORT</span>
248
+ <select id="sort-by">
249
+ <option value="name">Name</option>
250
+ <option value="type">Type</option>
251
+ <option value="scope">Scope</option>
252
+ <option value="license">License</option>
253
+ <option value="severity">Severity</option>
254
+ <option value="install">Install</option>
255
+ <option value="depth">Depth</option>
256
+ </select>
257
+ <button type="button" class="sort-direction-btn" id="sort-direction" title="Toggle sort direction">↑</button>
258
+ </div>
259
+
260
+ <div class="theme-toggle">
261
+ <span class="theme-toggle-label">Theme</span>
262
+ <div class="theme-switch" id="theme-switch" title="Toggle dark/light mode"></div>
263
+ </div>
205
264
  </div>
206
265
  </div>
207
- <div class="license-groups">
208
- <label class="license-group-checkbox">
209
- <input type="checkbox" id="license-permissive" checked />
210
- <span class="license-dot permissive"></span>
211
- Permissive (MIT, BSD, Apache, ISC)
212
- </label>
213
- <label class="license-group-checkbox">
214
- <input type="checkbox" id="license-weak-copyleft" checked />
215
- <span class="license-dot weak-copyleft"></span>
216
- Weak Copyleft (LGPL, MPL, EPL)
217
- </label>
218
- <label class="license-group-checkbox">
219
- <input type="checkbox" id="license-strong-copyleft" checked />
220
- <span class="license-dot strong-copyleft"></span>
221
- Strong Copyleft (GPL, AGPL)
222
- </label>
223
- <label class="license-group-checkbox">
224
- <input type="checkbox" id="license-unknown" checked />
225
- <span class="license-dot unknown"></span>
226
- Other / Unknown
227
- </label>
266
+
267
+ <!-- Collapsible License Filter Panel -->
268
+ <div class="license-filter-panel-row">
269
+ <div class="license-filter-panel" id="license-panel">
270
+ <div class="license-filter-inner">
271
+ <div class="license-filter-header">
272
+ <span class="license-filter-title">Filter by License Type</span>
273
+ <div class="license-quick-actions">
274
+ <button type="button" class="quick-action-btn" id="license-all">Show All</button>
275
+ <button type="button" class="quick-action-btn" id="license-friendly">Business-Friendly Only</button>
276
+ </div>
277
+ </div>
278
+ <div class="license-groups">
279
+ <label class="license-group-checkbox">
280
+ <input type="checkbox" id="license-permissive" checked />
281
+ <span class="license-dot permissive"></span>
282
+ Permissive (MIT, BSD, Apache, ISC)
283
+ </label>
284
+ <label class="license-group-checkbox">
285
+ <input type="checkbox" id="license-weak-copyleft" checked />
286
+ <span class="license-dot weak-copyleft"></span>
287
+ Weak Copyleft (LGPL, MPL, EPL)
288
+ </label>
289
+ <label class="license-group-checkbox">
290
+ <input type="checkbox" id="license-strong-copyleft" checked />
291
+ <span class="license-dot strong-copyleft"></span>
292
+ Strong Copyleft (GPL, AGPL)
293
+ </label>
294
+ <label class="license-group-checkbox">
295
+ <input type="checkbox" id="license-unknown" checked />
296
+ <span class="license-dot unknown"></span>
297
+ Other / Unknown
298
+ </label>
299
+ </div>
300
+ </div>
301
+ </div>
302
+ </div>
303
+
304
+ <!-- Results summary and column headers row -->
305
+ <div class="column-headers-section" id="column-headers-section">
306
+ <div class="package-header-wrapper">
307
+ <div class="results-summary" id="results-summary"></div>
308
+ <button type="button" class="column-header package-header column-header-no-border" data-sort="name" id="package-header">
309
+ PACKAGE
310
+ <span class="sort-indicator"></span>
311
+ </button>
312
+ </div>
313
+ <!-- Column headers are dynamically generated by JavaScript from COLUMN_CONFIG -->
314
+ <div id="column-headers-container"></div>
228
315
  </div>
229
316
  </div>
230
317
  </div>
@@ -232,7 +319,6 @@ ${report_assets_1.CSS_CONTENT}
232
319
 
233
320
  <!-- Main Content -->
234
321
  <main class="main-content">
235
- <div class="results-summary" id="results-summary"></div>
236
322
  <div id="dependency-list" class="dependency-grid"></div>
237
323
  </main>
238
324
 
@@ -242,9 +328,9 @@ ${report_assets_1.CSS_CONTENT}
242
328
  </footer>
243
329
 
244
330
  <script type="application/json" id="radar-data">${json}</script>
245
- <script>
246
- ${report_assets_1.JS_CONTENT}
247
- </script>
331
+ <script>
332
+ ${safeJsContent}
333
+ </script>
248
334
  </body>
249
335
  </html>`;
250
336
  }