mcp-react-toolkit 1.3.0 → 1.4.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.
Files changed (268) hide show
  1. package/README.md +28 -28
  2. package/node_modules/@mcp-showcase/shared/build/McpServerBase.d.ts +13 -0
  3. package/node_modules/@mcp-showcase/shared/build/McpServerBase.d.ts.map +1 -1
  4. package/node_modules/@mcp-showcase/shared/build/McpServerBase.js +40 -0
  5. package/node_modules/@mcp-showcase/shared/build/McpServerBase.js.map +1 -1
  6. package/node_modules/@mcp-showcase/shared/build/types.d.ts +11 -0
  7. package/node_modules/@mcp-showcase/shared/build/types.d.ts.map +1 -1
  8. package/node_modules/@mcp-showcase/shared/src/McpServerBase.ts +45 -0
  9. package/node_modules/@mcp-showcase/shared/src/types.ts +12 -0
  10. package/node_modules/@mcp-showcase/ui-kit/README.md +30 -0
  11. package/node_modules/@mcp-showcase/ui-kit/build/components.d.ts +10 -0
  12. package/node_modules/@mcp-showcase/ui-kit/build/components.d.ts.map +1 -0
  13. package/node_modules/@mcp-showcase/ui-kit/build/components.js +177 -0
  14. package/node_modules/@mcp-showcase/ui-kit/build/components.js.map +1 -0
  15. package/node_modules/@mcp-showcase/ui-kit/build/escape.d.ts +3 -0
  16. package/node_modules/@mcp-showcase/ui-kit/build/escape.d.ts.map +1 -0
  17. package/node_modules/@mcp-showcase/ui-kit/build/escape.js +10 -0
  18. package/node_modules/@mcp-showcase/ui-kit/build/escape.js.map +1 -0
  19. package/node_modules/@mcp-showcase/ui-kit/build/fixture.d.ts +4 -0
  20. package/node_modules/@mcp-showcase/ui-kit/build/fixture.d.ts.map +1 -0
  21. package/node_modules/@mcp-showcase/ui-kit/build/fixture.js +52 -0
  22. package/node_modules/@mcp-showcase/ui-kit/build/fixture.js.map +1 -0
  23. package/node_modules/@mcp-showcase/ui-kit/build/generate-template.d.ts +2 -0
  24. package/node_modules/@mcp-showcase/ui-kit/build/generate-template.d.ts.map +1 -0
  25. package/node_modules/@mcp-showcase/ui-kit/build/generate-template.js +19 -0
  26. package/node_modules/@mcp-showcase/ui-kit/build/generate-template.js.map +1 -0
  27. package/node_modules/@mcp-showcase/ui-kit/build/index.d.ts +6 -0
  28. package/node_modules/@mcp-showcase/ui-kit/build/index.d.ts.map +1 -0
  29. package/node_modules/@mcp-showcase/ui-kit/build/index.js +7 -0
  30. package/node_modules/@mcp-showcase/ui-kit/build/index.js.map +1 -0
  31. package/node_modules/@mcp-showcase/ui-kit/build/render.d.ts +3 -0
  32. package/node_modules/@mcp-showcase/ui-kit/build/render.d.ts.map +1 -0
  33. package/node_modules/@mcp-showcase/ui-kit/build/render.js +38 -0
  34. package/node_modules/@mcp-showcase/ui-kit/build/render.js.map +1 -0
  35. package/node_modules/@mcp-showcase/ui-kit/build/result-components.d.ts +9 -0
  36. package/node_modules/@mcp-showcase/ui-kit/build/result-components.d.ts.map +1 -0
  37. package/node_modules/@mcp-showcase/ui-kit/build/result-components.js +105 -0
  38. package/node_modules/@mcp-showcase/ui-kit/build/result-components.js.map +1 -0
  39. package/node_modules/@mcp-showcase/ui-kit/build/result-fixture.d.ts +4 -0
  40. package/node_modules/@mcp-showcase/ui-kit/build/result-fixture.d.ts.map +1 -0
  41. package/node_modules/@mcp-showcase/ui-kit/build/result-fixture.js +39 -0
  42. package/node_modules/@mcp-showcase/ui-kit/build/result-fixture.js.map +1 -0
  43. package/node_modules/@mcp-showcase/ui-kit/build/result-render.d.ts +3 -0
  44. package/node_modules/@mcp-showcase/ui-kit/build/result-render.d.ts.map +1 -0
  45. package/node_modules/@mcp-showcase/ui-kit/build/result-render.js +37 -0
  46. package/node_modules/@mcp-showcase/ui-kit/build/result-render.js.map +1 -0
  47. package/node_modules/@mcp-showcase/ui-kit/build/result-runtime.d.ts +2 -0
  48. package/node_modules/@mcp-showcase/ui-kit/build/result-runtime.d.ts.map +1 -0
  49. package/node_modules/@mcp-showcase/ui-kit/build/result-runtime.js +72 -0
  50. package/node_modules/@mcp-showcase/ui-kit/build/result-runtime.js.map +1 -0
  51. package/node_modules/@mcp-showcase/ui-kit/build/runtime.d.ts +2 -0
  52. package/node_modules/@mcp-showcase/ui-kit/build/runtime.d.ts.map +1 -0
  53. package/node_modules/@mcp-showcase/ui-kit/build/runtime.js +221 -0
  54. package/node_modules/@mcp-showcase/ui-kit/build/runtime.js.map +1 -0
  55. package/node_modules/@mcp-showcase/ui-kit/build/theme.d.ts +2 -0
  56. package/node_modules/@mcp-showcase/ui-kit/build/theme.d.ts.map +1 -0
  57. package/node_modules/@mcp-showcase/ui-kit/build/theme.js +278 -0
  58. package/node_modules/@mcp-showcase/ui-kit/build/theme.js.map +1 -0
  59. package/node_modules/@mcp-showcase/ui-kit/build/types.d.ts +113 -0
  60. package/node_modules/@mcp-showcase/ui-kit/build/types.d.ts.map +1 -0
  61. package/node_modules/@mcp-showcase/ui-kit/build/types.js +35 -0
  62. package/node_modules/@mcp-showcase/ui-kit/build/types.js.map +1 -0
  63. package/node_modules/@mcp-showcase/ui-kit/demo/index.html +653 -0
  64. package/node_modules/@mcp-showcase/ui-kit/demo/result.html +445 -0
  65. package/node_modules/@mcp-showcase/ui-kit/package.json +19 -0
  66. package/node_modules/@mcp-showcase/ui-kit/src/components.ts +191 -0
  67. package/node_modules/@mcp-showcase/ui-kit/src/escape.ts +9 -0
  68. package/node_modules/@mcp-showcase/ui-kit/src/fixture.ts +53 -0
  69. package/node_modules/@mcp-showcase/ui-kit/src/generate-template.ts +21 -0
  70. package/node_modules/@mcp-showcase/ui-kit/src/index.test.ts +72 -0
  71. package/node_modules/@mcp-showcase/ui-kit/src/index.ts +6 -0
  72. package/node_modules/@mcp-showcase/ui-kit/src/render.ts +48 -0
  73. package/node_modules/@mcp-showcase/ui-kit/src/result-components.ts +112 -0
  74. package/node_modules/@mcp-showcase/ui-kit/src/result-fixture.ts +40 -0
  75. package/node_modules/@mcp-showcase/ui-kit/src/result-render.test.ts +47 -0
  76. package/node_modules/@mcp-showcase/ui-kit/src/result-render.ts +47 -0
  77. package/node_modules/@mcp-showcase/ui-kit/src/result-runtime.ts +72 -0
  78. package/node_modules/@mcp-showcase/ui-kit/src/runtime.smoke.test.ts +103 -0
  79. package/node_modules/@mcp-showcase/ui-kit/src/runtime.ts +221 -0
  80. package/node_modules/@mcp-showcase/ui-kit/src/theme.ts +278 -0
  81. package/node_modules/@mcp-showcase/ui-kit/src/types.ts +140 -0
  82. package/node_modules/@mcp-showcase/ui-kit/tsconfig.json +9 -0
  83. package/package.json +16 -5
  84. package/tools/accessibility-checker/build/health-report.d.ts +27 -0
  85. package/tools/accessibility-checker/build/health-report.d.ts.map +1 -0
  86. package/tools/accessibility-checker/build/health-report.js +140 -0
  87. package/tools/accessibility-checker/build/health-report.js.map +1 -0
  88. package/tools/accessibility-checker/build/index.js +7 -1
  89. package/tools/accessibility-checker/build/index.js.map +1 -1
  90. package/tools/accessibility-checker/package.json +1 -0
  91. package/tools/code-modernizer/build/index.js +60 -44
  92. package/tools/code-modernizer/build/index.js.map +1 -1
  93. package/tools/code-modernizer/build/result-report.d.ts +24 -0
  94. package/tools/code-modernizer/build/result-report.d.ts.map +1 -0
  95. package/tools/code-modernizer/build/result-report.js +101 -0
  96. package/tools/code-modernizer/build/result-report.js.map +1 -0
  97. package/tools/code-modernizer/package.json +2 -0
  98. package/tools/component-factory/build/index.js +7 -1
  99. package/tools/component-factory/build/index.js.map +1 -1
  100. package/tools/component-factory/build/result-report.d.ts +11 -0
  101. package/tools/component-factory/build/result-report.d.ts.map +1 -0
  102. package/tools/component-factory/build/result-report.js +104 -0
  103. package/tools/component-factory/build/result-report.js.map +1 -0
  104. package/tools/component-factory/package.json +1 -0
  105. package/tools/component-fixer/build/index.js +6 -6
  106. package/tools/component-fixer/build/index.js.map +1 -1
  107. package/tools/component-fixer/build/result-report.d.ts +37 -0
  108. package/tools/component-fixer/build/result-report.d.ts.map +1 -0
  109. package/tools/component-fixer/build/result-report.js +106 -0
  110. package/tools/component-fixer/build/result-report.js.map +1 -0
  111. package/tools/component-fixer/package.json +1 -0
  112. package/tools/component-reviewer/build/health-report.d.ts +37 -0
  113. package/tools/component-reviewer/build/health-report.d.ts.map +1 -0
  114. package/tools/component-reviewer/build/health-report.js +116 -0
  115. package/tools/component-reviewer/build/health-report.js.map +1 -0
  116. package/tools/component-reviewer/build/index.d.ts.map +1 -1
  117. package/tools/component-reviewer/build/index.js +7 -6
  118. package/tools/component-reviewer/build/index.js.map +1 -1
  119. package/tools/component-reviewer/package.json +1 -0
  120. package/tools/dep-auditor/build/health-report.d.ts +16 -0
  121. package/tools/dep-auditor/build/health-report.d.ts.map +1 -0
  122. package/tools/dep-auditor/build/health-report.js +187 -0
  123. package/tools/dep-auditor/build/health-report.js.map +1 -0
  124. package/tools/dep-auditor/build/index.d.ts.map +1 -1
  125. package/tools/dep-auditor/build/index.js +7 -1
  126. package/tools/dep-auditor/build/index.js.map +1 -1
  127. package/tools/dep-auditor/package.json +1 -0
  128. package/tools/generate-tests/build/index.js +8 -1
  129. package/tools/generate-tests/build/index.js.map +1 -1
  130. package/tools/generate-tests/build/result-report.d.ts +14 -0
  131. package/tools/generate-tests/build/result-report.d.ts.map +1 -0
  132. package/tools/generate-tests/build/result-report.js +124 -0
  133. package/tools/generate-tests/build/result-report.js.map +1 -0
  134. package/tools/generate-tests/package.json +1 -0
  135. package/tools/legacy-analyzer/build/health-report.d.ts +4 -0
  136. package/tools/legacy-analyzer/build/health-report.d.ts.map +1 -0
  137. package/tools/legacy-analyzer/build/health-report.js +164 -0
  138. package/tools/legacy-analyzer/build/health-report.js.map +1 -0
  139. package/tools/legacy-analyzer/build/index.js +15 -1
  140. package/tools/legacy-analyzer/build/index.js.map +1 -1
  141. package/tools/legacy-analyzer/build/tools/01-detect-project-tech.d.ts.map +1 -1
  142. package/tools/legacy-analyzer/build/tools/01-detect-project-tech.js +3 -8
  143. package/tools/legacy-analyzer/build/tools/01-detect-project-tech.js.map +1 -1
  144. package/tools/legacy-analyzer/build/tools/05-analyze-api-layer.d.ts.map +1 -1
  145. package/tools/legacy-analyzer/build/tools/05-analyze-api-layer.js +25 -3
  146. package/tools/legacy-analyzer/build/tools/05-analyze-api-layer.js.map +1 -1
  147. package/tools/legacy-analyzer/package.json +1 -0
  148. package/tools/lighthouse-runner/build/health-report.d.ts +14 -0
  149. package/tools/lighthouse-runner/build/health-report.d.ts.map +1 -0
  150. package/tools/lighthouse-runner/build/health-report.js +138 -0
  151. package/tools/lighthouse-runner/build/health-report.js.map +1 -0
  152. package/tools/lighthouse-runner/build/index.d.ts.map +1 -1
  153. package/tools/lighthouse-runner/build/index.js +7 -1
  154. package/tools/lighthouse-runner/build/index.js.map +1 -1
  155. package/tools/lighthouse-runner/package.json +1 -0
  156. package/tools/monorepo-manager/build/index.js +9 -1
  157. package/tools/monorepo-manager/build/index.js.map +1 -1
  158. package/tools/monorepo-manager/build/result-report.d.ts +20 -0
  159. package/tools/monorepo-manager/build/result-report.d.ts.map +1 -0
  160. package/tools/monorepo-manager/build/result-report.js +84 -0
  161. package/tools/monorepo-manager/build/result-report.js.map +1 -0
  162. package/tools/monorepo-manager/package.json +1 -0
  163. package/tools/performance-audit/build/health-report.d.ts +30 -0
  164. package/tools/performance-audit/build/health-report.d.ts.map +1 -0
  165. package/tools/performance-audit/build/health-report.js +152 -0
  166. package/tools/performance-audit/build/health-report.js.map +1 -0
  167. package/tools/performance-audit/build/index.d.ts.map +1 -1
  168. package/tools/performance-audit/build/index.js +7 -1
  169. package/tools/performance-audit/build/index.js.map +1 -1
  170. package/tools/performance-audit/package.json +1 -0
  171. package/tools/quality-pipeline/build/health-report.d.ts +11 -0
  172. package/tools/quality-pipeline/build/health-report.d.ts.map +1 -0
  173. package/tools/quality-pipeline/build/health-report.js +137 -0
  174. package/tools/quality-pipeline/build/health-report.js.map +1 -0
  175. package/tools/quality-pipeline/build/index.js +7 -1
  176. package/tools/quality-pipeline/build/index.js.map +1 -1
  177. package/tools/quality-pipeline/package.json +1 -0
  178. package/tools/render-analyzer/build/health-report.d.ts +33 -0
  179. package/tools/render-analyzer/build/health-report.d.ts.map +1 -0
  180. package/tools/render-analyzer/build/health-report.js +142 -0
  181. package/tools/render-analyzer/build/health-report.js.map +1 -0
  182. package/tools/render-analyzer/build/index.d.ts.map +1 -1
  183. package/tools/render-analyzer/build/index.js +7 -1
  184. package/tools/render-analyzer/build/index.js.map +1 -1
  185. package/tools/render-analyzer/package.json +1 -0
  186. package/tools/shared/build/McpServerBase.d.ts +13 -0
  187. package/tools/shared/build/McpServerBase.d.ts.map +1 -1
  188. package/tools/shared/build/McpServerBase.js +40 -0
  189. package/tools/shared/build/McpServerBase.js.map +1 -1
  190. package/tools/shared/build/types.d.ts +11 -0
  191. package/tools/shared/build/types.d.ts.map +1 -1
  192. package/tools/storybook-generator/build/index.d.ts.map +1 -1
  193. package/tools/storybook-generator/build/index.js +9 -1
  194. package/tools/storybook-generator/build/index.js.map +1 -1
  195. package/tools/storybook-generator/build/result-report.d.ts +22 -0
  196. package/tools/storybook-generator/build/result-report.d.ts.map +1 -0
  197. package/tools/storybook-generator/build/result-report.js +77 -0
  198. package/tools/storybook-generator/build/result-report.js.map +1 -0
  199. package/tools/storybook-generator/package.json +1 -0
  200. package/tools/test-gap-analyzer/build/health-report.d.ts +34 -0
  201. package/tools/test-gap-analyzer/build/health-report.d.ts.map +1 -0
  202. package/tools/test-gap-analyzer/build/health-report.js +190 -0
  203. package/tools/test-gap-analyzer/build/health-report.js.map +1 -0
  204. package/tools/test-gap-analyzer/build/index.d.ts.map +1 -1
  205. package/tools/test-gap-analyzer/build/index.js +7 -1
  206. package/tools/test-gap-analyzer/build/index.js.map +1 -1
  207. package/tools/test-gap-analyzer/package.json +1 -0
  208. package/tools/typescript-enforcer/build/health-report.d.ts +33 -0
  209. package/tools/typescript-enforcer/build/health-report.d.ts.map +1 -0
  210. package/tools/typescript-enforcer/build/health-report.js +143 -0
  211. package/tools/typescript-enforcer/build/health-report.js.map +1 -0
  212. package/tools/typescript-enforcer/build/index.js +6 -1
  213. package/tools/typescript-enforcer/build/index.js.map +1 -1
  214. package/tools/typescript-enforcer/package.json +1 -0
  215. package/tools/ui-kit/README.md +30 -0
  216. package/tools/ui-kit/build/components.d.ts +10 -0
  217. package/tools/ui-kit/build/components.d.ts.map +1 -0
  218. package/tools/ui-kit/build/components.js +177 -0
  219. package/tools/ui-kit/build/components.js.map +1 -0
  220. package/tools/ui-kit/build/escape.d.ts +3 -0
  221. package/tools/ui-kit/build/escape.d.ts.map +1 -0
  222. package/tools/ui-kit/build/escape.js +10 -0
  223. package/tools/ui-kit/build/escape.js.map +1 -0
  224. package/tools/ui-kit/build/fixture.d.ts +4 -0
  225. package/tools/ui-kit/build/fixture.d.ts.map +1 -0
  226. package/tools/ui-kit/build/fixture.js +52 -0
  227. package/tools/ui-kit/build/fixture.js.map +1 -0
  228. package/tools/ui-kit/build/generate-template.d.ts +2 -0
  229. package/tools/ui-kit/build/generate-template.d.ts.map +1 -0
  230. package/tools/ui-kit/build/generate-template.js +19 -0
  231. package/tools/ui-kit/build/generate-template.js.map +1 -0
  232. package/tools/ui-kit/build/index.d.ts +6 -0
  233. package/tools/ui-kit/build/index.d.ts.map +1 -0
  234. package/tools/ui-kit/build/index.js +7 -0
  235. package/tools/ui-kit/build/index.js.map +1 -0
  236. package/tools/ui-kit/build/render.d.ts +3 -0
  237. package/tools/ui-kit/build/render.d.ts.map +1 -0
  238. package/tools/ui-kit/build/render.js +38 -0
  239. package/tools/ui-kit/build/render.js.map +1 -0
  240. package/tools/ui-kit/build/result-components.d.ts +9 -0
  241. package/tools/ui-kit/build/result-components.d.ts.map +1 -0
  242. package/tools/ui-kit/build/result-components.js +105 -0
  243. package/tools/ui-kit/build/result-components.js.map +1 -0
  244. package/tools/ui-kit/build/result-fixture.d.ts +4 -0
  245. package/tools/ui-kit/build/result-fixture.d.ts.map +1 -0
  246. package/tools/ui-kit/build/result-fixture.js +39 -0
  247. package/tools/ui-kit/build/result-fixture.js.map +1 -0
  248. package/tools/ui-kit/build/result-render.d.ts +3 -0
  249. package/tools/ui-kit/build/result-render.d.ts.map +1 -0
  250. package/tools/ui-kit/build/result-render.js +37 -0
  251. package/tools/ui-kit/build/result-render.js.map +1 -0
  252. package/tools/ui-kit/build/result-runtime.d.ts +2 -0
  253. package/tools/ui-kit/build/result-runtime.d.ts.map +1 -0
  254. package/tools/ui-kit/build/result-runtime.js +72 -0
  255. package/tools/ui-kit/build/result-runtime.js.map +1 -0
  256. package/tools/ui-kit/build/runtime.d.ts +2 -0
  257. package/tools/ui-kit/build/runtime.d.ts.map +1 -0
  258. package/tools/ui-kit/build/runtime.js +221 -0
  259. package/tools/ui-kit/build/runtime.js.map +1 -0
  260. package/tools/ui-kit/build/theme.d.ts +2 -0
  261. package/tools/ui-kit/build/theme.d.ts.map +1 -0
  262. package/tools/ui-kit/build/theme.js +278 -0
  263. package/tools/ui-kit/build/theme.js.map +1 -0
  264. package/tools/ui-kit/build/types.d.ts +113 -0
  265. package/tools/ui-kit/build/types.d.ts.map +1 -0
  266. package/tools/ui-kit/build/types.js +35 -0
  267. package/tools/ui-kit/build/types.js.map +1 -0
  268. package/tools/ui-kit/package.json +19 -0
@@ -0,0 +1,191 @@
1
+ // ============================================================================
2
+ // SERVER-SIDE COMPONENT BUILDERS — pure string -> HTML. No framework.
3
+ // The hero, category cards and fix-first queue are rendered statically for an
4
+ // instant premium paint; the interactive triage table + drawer are hydrated
5
+ // client-side by runtime.ts from the injected report data.
6
+ // ============================================================================
7
+
8
+ import { esc } from "./escape.js";
9
+ import {
10
+ HealthReport,
11
+ ReportCategory,
12
+ ReportAction,
13
+ scoreToBand,
14
+ scoreToGrade,
15
+ } from "./types.js";
16
+
17
+ const SUN = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>`;
18
+ const MOON = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>`;
19
+ const SEARCH = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>`;
20
+
21
+ function verdict(score: number): string {
22
+ if (score >= 85) return "Healthy & well-structured";
23
+ if (score >= 70) return "Solid, with a few rough edges";
24
+ if (score >= 55) return "Workable, but carrying debt";
25
+ if (score >= 40) return "Significant tech debt";
26
+ return "High-risk — needs intervention";
27
+ }
28
+
29
+ export function buildHeader(report: HealthReport): string {
30
+ return /* html */ `
31
+ <header class="hdr">
32
+ <div class="brand">
33
+ <span class="dot" aria-hidden="true">
34
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
35
+ </span>
36
+ <div style="min-width:0">
37
+ <h1>${esc(report.meta.title)}</h1>
38
+ <p class="sub">${esc(report.meta.target)}</p>
39
+ </div>
40
+ </div>
41
+ <div class="hdr-actions">
42
+ <button class="btn icon" id="theme-toggle" type="button" aria-label="Toggle colour theme" title="Toggle theme">
43
+ <span class="theme-sun" hidden>${SUN}</span><span class="theme-moon">${MOON}</span>
44
+ </button>
45
+ <button class="btn" id="export-btn" type="button" title="Copy report as Markdown">
46
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="M7 10l5 5 5-5"/><path d="M12 15V3"/></svg>
47
+ Export
48
+ </button>
49
+ </div>
50
+ </header>`;
51
+ }
52
+
53
+ function gauge(score: number): string {
54
+ const r = 64;
55
+ const circ = 2 * Math.PI * r;
56
+ const pct = Math.max(0, Math.min(100, score)) / 100;
57
+ const offset = circ * (1 - pct);
58
+ return /* html */ `
59
+ <div class="gauge" role="img" aria-label="Health score ${esc(score)} out of 100">
60
+ <svg width="148" height="148" viewBox="0 0 148 148">
61
+ <circle class="track" cx="74" cy="74" r="${r}" stroke-width="11"/>
62
+ <circle class="arc" cx="74" cy="74" r="${r}" stroke-width="11"
63
+ stroke-dasharray="${circ.toFixed(1)}" stroke-dashoffset="${circ.toFixed(1)}"
64
+ data-target="${offset.toFixed(1)}"/>
65
+ </svg>
66
+ <div class="center">
67
+ <div class="score num" data-count="${esc(score)}">0</div>
68
+ <div class="of">/ 100</div>
69
+ </div>
70
+ </div>`;
71
+ }
72
+
73
+ export function buildHero(report: HealthReport): string {
74
+ const band = scoreToBand(report.score);
75
+ const grade = scoreToGrade(report.score);
76
+ const chips = (report.chips ?? [])
77
+ .map((c) => `<span class="chip"><span>${esc(c.label)}</span><b>${esc(c.value)}</b></span>`)
78
+ .join("");
79
+ return /* html */ `
80
+ <section class="hero" data-band="${band}">
81
+ ${gauge(report.score)}
82
+ <div class="hero-meta">
83
+ <div class="grade-row">
84
+ <span class="grade">Grade ${esc(grade)}</span>
85
+ <span class="verdict">${esc(verdict(report.score))}</span>
86
+ </div>
87
+ <p>${esc(report.totalIssues)} issue${report.totalIssues === 1 ? "" : "s"} found across ${esc(report.categories.length)} areas${report.meta.subtitle ? " · " + esc(report.meta.subtitle) : ""}.</p>
88
+ <div class="chips">${chips}</div>
89
+ </div>
90
+ </section>`;
91
+ }
92
+
93
+ function actionButtons(actions: ReportAction[] | undefined, primaryFirst = false): string {
94
+ if (!actions || !actions.length) return "";
95
+ return actions
96
+ .map((a, i) => {
97
+ const cls = primaryFirst && i === 0 ? "btn primary" : "btn";
98
+ return `<button class="${cls}" type="button" data-action="${esc(a.id)}">${esc(a.label)}</button>`;
99
+ })
100
+ .join("");
101
+ }
102
+
103
+ export function buildFixFirst(report: HealthReport): string {
104
+ const actions = report.topActions ?? [];
105
+ if (!actions.length) return "";
106
+ const items = actions
107
+ .map((a, i) => {
108
+ const sub = a.kind === "tool" ? `Runs ${esc(a.tool)}` : a.kind === "prompt" ? "Asks the agent" : "Opens link";
109
+ return /* html */ `
110
+ <div class="qitem" data-action="${esc(a.id)}" role="button" tabindex="0">
111
+ <span class="rank num">${i + 1}</span>
112
+ <span class="qt"><span class="t">${esc(a.label)}</span><span class="s">${sub}</span></span>
113
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:var(--faint)"><path d="m9 18 6-6-6-6"/></svg>
114
+ </div>`;
115
+ })
116
+ .join("");
117
+ return /* html */ `
118
+ <div class="sec"><h2>Fix-first queue</h2><span class="count">${actions.length} prioritised</span></div>
119
+ <div class="queue">${items}</div>`;
120
+ }
121
+
122
+ export function buildCategories(report: HealthReport): string {
123
+ const card = (c: ReportCategory): string => {
124
+ const hasScore = typeof c.score === "number";
125
+ const pct = hasScore ? Math.max(0, Math.min(100, c.score as number)) : 0;
126
+ return /* html */ `
127
+ <div class="card" data-category="${esc(c.id)}" role="button" tabindex="0" aria-label="Filter issues by ${esc(c.name)}">
128
+ <div class="top">
129
+ <span class="name">${esc(c.name)}</span>
130
+ <span class="badge ${esc(c.status)}">${hasScore ? esc(pct) : esc(c.status)}</span>
131
+ </div>
132
+ <div class="sum">${esc(c.summary)}</div>
133
+ ${hasScore ? `<div class="bar"><i class="${esc(c.status)}" data-w="${pct}" style="width:0"></i></div>` : ""}
134
+ <div class="foot"><span>${esc(c.issueCount)} issue${c.issueCount === 1 ? "" : "s"}</span><span>View →</span></div>
135
+ </div>`;
136
+ };
137
+ return /* html */ `
138
+ <div class="sec"><h2>Areas</h2><span class="count">${report.categories.length} analysed</span></div>
139
+ <div class="cards">${report.categories.map(card).join("")}</div>`;
140
+ }
141
+
142
+ export function buildTriageShell(report: HealthReport): string {
143
+ const sevFilters = ["all", "critical", "high", "medium", "low"]
144
+ .map(
145
+ (s, i) =>
146
+ `<button type="button" data-sev="${s}" aria-pressed="${i === 0 ? "true" : "false"}">${s[0].toUpperCase() + s.slice(1)}</button>`
147
+ )
148
+ .join("");
149
+ return /* html */ `
150
+ <div class="sec"><h2>Issues</h2><span class="count" id="issue-count">${report.issues.length} total</span></div>
151
+ <div class="toolbar">
152
+ <label class="search">
153
+ ${SEARCH}
154
+ <input id="search" type="search" placeholder="Search issues, files, categories…" aria-label="Search issues"/>
155
+ </label>
156
+ <div class="filter" role="group" aria-label="Filter by severity">${sevFilters}</div>
157
+ </div>
158
+ <div class="table-card">
159
+ <table>
160
+ <thead><tr>
161
+ <th data-sort="severity" aria-sort="descending">Severity<span class="arr">▼</span></th>
162
+ <th data-sort="title">Issue<span class="arr">▼</span></th>
163
+ <th data-sort="category">Area<span class="arr">▼</span></th>
164
+ <th data-sort="file">File<span class="arr">▼</span></th>
165
+ </tr></thead>
166
+ <tbody id="rows"></tbody>
167
+ </table>
168
+ <div class="empty" id="empty" hidden>
169
+ <div class="big">Nothing matches</div>
170
+ <div>Try clearing the search or severity filter.</div>
171
+ </div>
172
+ </div>`;
173
+ }
174
+
175
+ export function buildChrome(): string {
176
+ return /* html */ `
177
+ <div class="scrim" id="scrim"></div>
178
+ <aside class="drawer" id="drawer" role="dialog" aria-modal="true" aria-labelledby="d-title">
179
+ <header>
180
+ <h3 id="d-title"></h3>
181
+ <button class="btn icon" id="d-close" type="button" aria-label="Close detail">
182
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg>
183
+ </button>
184
+ </header>
185
+ <div class="body" id="d-body"></div>
186
+ <div class="acts" id="d-acts"></div>
187
+ </aside>
188
+ <div class="toast" id="toast" role="status" aria-live="polite"></div>`;
189
+ }
190
+
191
+ export { actionButtons };
@@ -0,0 +1,9 @@
1
+ /** Escape a string for safe interpolation into HTML text/attribute context. */
2
+ export function esc(value: unknown): string {
3
+ return String(value ?? "")
4
+ .replace(/&/g, "&amp;")
5
+ .replace(/</g, "&lt;")
6
+ .replace(/>/g, "&gt;")
7
+ .replace(/"/g, "&quot;")
8
+ .replace(/'/g, "&#39;");
9
+ }
@@ -0,0 +1,53 @@
1
+ import { HealthReport } from "./types.js";
2
+
3
+ /** Representative sample report — drives the standalone browser demo and tests. */
4
+ export const SAMPLE_REPORT: HealthReport = {
5
+ meta: {
6
+ title: "Codebase Health Studio",
7
+ subtitle: "React 18 · CRA",
8
+ target: "acme-dashboard",
9
+ generatedAt: "2026-06-27",
10
+ tool: "legacy-analyzer",
11
+ },
12
+ score: 62,
13
+ totalIssues: 18,
14
+ chips: [
15
+ { label: "Framework", value: "Create React App" },
16
+ { label: "React", value: "18.2.0" },
17
+ { label: "Language", value: "JavaScript" },
18
+ { label: "Components", value: "94" },
19
+ ],
20
+ topActions: [
21
+ { id: "fix-god", label: "Split the 3 god components", kind: "tool", tool: "component-fixer", params: { target: "components" }, fallback: "Refactor Dashboard.jsx, Settings.jsx and Reports.jsx into smaller components." },
22
+ { id: "plan", label: "Generate full refactor plan", kind: "tool", tool: "generate-refactor-plan", params: { path: "." }, fallback: "Generate a refactor plan for acme-dashboard." },
23
+ { id: "migrate-ts", label: "Migrate the API layer to TypeScript", kind: "prompt", prompt: "Convert src/api/*.js to TypeScript with typed responses and a centralised client." },
24
+ ],
25
+ categories: [
26
+ { id: "components", name: "Components", score: 48, status: "warn", summary: "3 god components over 300 lines; 11 with mixed responsibilities.", issueCount: 6, details: [{ label: "Total", value: "94" }, { label: "Large", value: "3" }] },
27
+ { id: "state", name: "State", score: 55, status: "warn", summary: "Redux without normalisation; derived state stored in the store.", issueCount: 3 },
28
+ { id: "api", name: "API layer", score: 40, status: "bad", summary: "Scattered axios calls, 4 duplicated endpoints, no central client.", issueCount: 4 },
29
+ { id: "routing", name: "Routing", score: 78, status: "good", summary: "react-router v6, nested routes, but no lazy loading.", issueCount: 1 },
30
+ { id: "styling", name: "Styling", score: 60, status: "warn", summary: "Mixed CSS + styled-components; 23 hardcoded colours.", issueCount: 2 },
31
+ { id: "deps", name: "Dependencies", score: 84, status: "good", summary: "2 unused deps; moment.js inflating the bundle.", issueCount: 2 },
32
+ ],
33
+ issues: [
34
+ { id: "i1", category: "components", severity: "critical", title: "God component: Dashboard.jsx (612 lines)", description: "Dashboard.jsx mixes data fetching, layout, and 4 unrelated widgets. It is the single largest re-render hotspot.", file: "src/pages/Dashboard.jsx", meta: [{ label: "Lines", value: "612" }, { label: "Responsibilities", value: "5" }], actions: [{ id: "fix-i1", label: "Split with component-fixer", kind: "tool", tool: "component-fixer", params: { file: "src/pages/Dashboard.jsx" }, fallback: "Split src/pages/Dashboard.jsx into smaller components." }, { id: "explain-i1", label: "Explain the risk", kind: "prompt", prompt: "Explain why a 612-line god component hurts maintainability and performance in React." }] },
35
+ { id: "i2", category: "api", severity: "critical", title: "Duplicated endpoint: GET /users called 4 ways", description: "Four components call GET /users with slightly different axios config — no shared client or cache.", file: "src/api/", meta: [{ label: "Call sites", value: "4" }], actions: [{ id: "fix-i2", label: "Centralise the API client", kind: "prompt", prompt: "Create a centralised typed axios client and replace the 4 duplicate GET /users call sites." }] },
36
+ { id: "i3", category: "components", severity: "high", title: "God component: Settings.jsx (438 lines)", file: "src/pages/Settings.jsx", meta: [{ label: "Lines", value: "438" }], actions: [{ id: "fix-i3", label: "Split with component-fixer", kind: "tool", tool: "component-fixer", params: { file: "src/pages/Settings.jsx" }, fallback: "Split src/pages/Settings.jsx." }] },
37
+ { id: "i4", category: "api", severity: "high", title: "No central HTTP client — axios imported in 19 files", file: "src/", actions: [] },
38
+ { id: "i5", category: "state", severity: "high", title: "Derived state stored in Redux (totals recomputed on every action)", file: "src/store/cartSlice.js", actions: [{ id: "explain-i5", label: "How to fix", kind: "prompt", prompt: "Show how to replace derived state in Redux with reselect selectors." }] },
39
+ { id: "i6", category: "deps", severity: "high", title: "moment.js adds ~230KB — replace with date-fns or Temporal", file: "package.json", meta: [{ label: "Bundle impact", value: "~230KB" }], actions: [] },
40
+ { id: "i7", category: "components", severity: "medium", title: "Prop drilling 4 levels deep for `user`", file: "src/pages/Reports.jsx", actions: [] },
41
+ { id: "i8", category: "styling", severity: "medium", title: "23 hardcoded hex colours outside the token system", file: "src/styles/", actions: [] },
42
+ { id: "i9", category: "state", severity: "medium", title: "No memoised selectors — components re-render on unrelated store changes", file: "src/store/", actions: [] },
43
+ { id: "i10", category: "api", severity: "medium", title: "Errors swallowed — 11 axios calls have no .catch", file: "src/api/", actions: [] },
44
+ { id: "i11", category: "components", severity: "medium", title: "List rendered with array index as key", file: "src/components/Table.jsx", actions: [] },
45
+ { id: "i12", category: "routing", severity: "medium", title: "No route-level code splitting (React.lazy)", file: "src/App.jsx", actions: [] },
46
+ { id: "i13", category: "deps", severity: "low", title: "2 unused dependencies in package.json", file: "package.json", actions: [] },
47
+ { id: "i14", category: "styling", severity: "low", title: "Duplicate utility classes across 6 CSS files", file: "src/styles/", actions: [] },
48
+ { id: "i15", category: "components", severity: "low", title: "12 components missing displayName", file: "src/components/", actions: [] },
49
+ { id: "i16", category: "api", severity: "low", title: "Base URL hardcoded instead of env var", file: "src/api/config.js", actions: [] },
50
+ { id: "i17", category: "components", severity: "low", title: "Inline arrow functions in 31 render paths", file: "src/", actions: [] },
51
+ { id: "i18", category: "state", severity: "low", title: "Local component state duplicates Redux store data", file: "src/pages/Profile.jsx", actions: [] },
52
+ ],
53
+ };
@@ -0,0 +1,21 @@
1
+ // Build step: emit a standalone browser demo (fixture baked in) so the report
2
+ // UI can be opened directly in a browser — the public showcase artifact.
3
+ import { writeFileSync, mkdirSync } from "node:fs";
4
+ import { dirname, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { renderReportHTML } from "./render.js";
7
+ import { renderResultHTML } from "./result-render.js";
8
+ import { SAMPLE_REPORT } from "./fixture.js";
9
+ import { SAMPLE_RESULT } from "./result-fixture.js";
10
+
11
+ const here = dirname(fileURLToPath(import.meta.url));
12
+ const outDir = resolve(here, "..", "demo");
13
+ mkdirSync(outDir, { recursive: true });
14
+
15
+ const html = renderReportHTML(SAMPLE_REPORT);
16
+ writeFileSync(resolve(outDir, "index.html"), html, "utf8");
17
+ console.error(`[ui-kit] wrote demo/index.html (${(html.length / 1024).toFixed(1)} KB)`);
18
+
19
+ const resultHtml = renderResultHTML(SAMPLE_RESULT);
20
+ writeFileSync(resolve(outDir, "result.html"), resultHtml, "utf8");
21
+ console.error(`[ui-kit] wrote demo/result.html (${(resultHtml.length / 1024).toFixed(1)} KB)`);
@@ -0,0 +1,72 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { renderReportHTML, SAMPLE_REPORT, scoreToGrade, scoreToBand } from "./index.js";
3
+ import { esc } from "./escape.js";
4
+ import type { HealthReport } from "./index.js";
5
+
6
+ describe("scoreToGrade", () => {
7
+ it("maps score ranges to grades", () => {
8
+ expect(scoreToGrade(100)).toBe("A+");
9
+ expect(scoreToGrade(88)).toBe("A");
10
+ expect(scoreToGrade(72)).toBe("B");
11
+ expect(scoreToGrade(60)).toBe("C");
12
+ expect(scoreToGrade(42)).toBe("D");
13
+ expect(scoreToGrade(20)).toBe("F");
14
+ });
15
+ });
16
+
17
+ describe("scoreToBand", () => {
18
+ it("buckets scores into accent bands", () => {
19
+ expect(scoreToBand(80)).toBe("good");
20
+ expect(scoreToBand(50)).toBe("warn");
21
+ expect(scoreToBand(30)).toBe("bad");
22
+ });
23
+ });
24
+
25
+ describe("esc", () => {
26
+ it("escapes HTML-significant characters", () => {
27
+ expect(esc(`<img src=x onerror="a">`)).toBe("&lt;img src=x onerror=&quot;a&quot;&gt;");
28
+ expect(esc("a & b")).toBe("a &amp; b");
29
+ expect(esc(null)).toBe("");
30
+ });
31
+ });
32
+
33
+ describe("renderReportHTML", () => {
34
+ const html = renderReportHTML(SAMPLE_REPORT);
35
+
36
+ it("produces a complete self-contained document", () => {
37
+ expect(html.startsWith("<!doctype html>")).toBe(true);
38
+ expect(html).toContain("<style>");
39
+ expect(html).toContain("data-theme=\"light\"");
40
+ // no external resource requests
41
+ expect(html).not.toMatch(/<link[^>]+href=/i);
42
+ expect(html).not.toMatch(/<script[^>]+src=/i);
43
+ });
44
+
45
+ it("renders the headline score and grade", () => {
46
+ expect(html).toContain('data-count="62"');
47
+ expect(html).toContain("Grade C");
48
+ });
49
+
50
+ it("renders every category and embeds the report data", () => {
51
+ for (const c of SAMPLE_REPORT.categories) expect(html).toContain(esc(c.name));
52
+ expect(html).toContain('id="report-data"');
53
+ const json = html.split('id="report-data" type="application/json">')[1].split("</script>")[0];
54
+ const parsed = JSON.parse(json.replace(/\\u003c/g, "<")) as HealthReport;
55
+ expect(parsed.issues).toHaveLength(SAMPLE_REPORT.issues.length);
56
+ });
57
+
58
+ it("ships the theme toggle and agentic action hooks", () => {
59
+ expect(html).toContain('id="theme-toggle"');
60
+ expect(html).toContain('data-action="fix-god"');
61
+ });
62
+
63
+ it("escapes a malicious title so it cannot break out of the document", () => {
64
+ const evil: HealthReport = {
65
+ ...SAMPLE_REPORT,
66
+ meta: { ...SAMPLE_REPORT.meta, title: `</script><img src=x onerror=alert(1)>` },
67
+ };
68
+ const out = renderReportHTML(evil);
69
+ expect(out).not.toContain("<img src=x onerror=alert(1)>");
70
+ expect(out).not.toContain("</script><img");
71
+ });
72
+ });
@@ -0,0 +1,6 @@
1
+ // @mcp-showcase/ui-kit — reusable single-file HTML report UI for MCP tools.
2
+ export * from "./types.js";
3
+ export { renderReportHTML } from "./render.js";
4
+ export { renderResultHTML } from "./result-render.js";
5
+ export { SAMPLE_REPORT } from "./fixture.js";
6
+ export { SAMPLE_RESULT } from "./result-fixture.js";
@@ -0,0 +1,48 @@
1
+ // ============================================================================
2
+ // renderReportHTML — assembles ONE self-contained HTML document from a
3
+ // HealthReport. Styles + runtime + data are all inlined (no external requests),
4
+ // so the result drops straight into a sandboxed MCP iframe or a browser tab.
5
+ // ============================================================================
6
+
7
+ import { STYLES } from "./theme.js";
8
+ import { RUNTIME } from "./runtime.js";
9
+ import { esc } from "./escape.js";
10
+ import {
11
+ buildHeader,
12
+ buildHero,
13
+ buildFixFirst,
14
+ buildCategories,
15
+ buildTriageShell,
16
+ buildChrome,
17
+ } from "./components.js";
18
+ import { HealthReport } from "./types.js";
19
+
20
+ /** Embed JSON safely inside a <script> tag (neutralise `</script>` break-out). */
21
+ function embedJson(data: unknown): string {
22
+ return JSON.stringify(data).replace(/</g, "\\u003c");
23
+ }
24
+
25
+ export function renderReportHTML(report: HealthReport): string {
26
+ return `<!doctype html>
27
+ <html lang="en" data-theme="light">
28
+ <head>
29
+ <meta charset="utf-8"/>
30
+ <meta name="viewport" content="width=device-width,initial-scale=1"/>
31
+ <title>${esc(report.meta.title)} — ${esc(report.meta.target)}</title>
32
+ <style>${STYLES}</style>
33
+ </head>
34
+ <body>
35
+ <div class="wrap">
36
+ ${buildHeader(report)}
37
+ ${buildHero(report)}
38
+ ${buildFixFirst(report)}
39
+ ${buildCategories(report)}
40
+ ${buildTriageShell(report)}
41
+ <div class="foot-note">${esc(report.meta.tool)} · generated ${esc(report.meta.generatedAt)} · mcp-react-toolkit</div>
42
+ </div>
43
+ ${buildChrome()}
44
+ <script id="report-data" type="application/json">${embedJson(report)}</script>
45
+ <script>${RUNTIME}</script>
46
+ </body>
47
+ </html>`;
48
+ }
@@ -0,0 +1,112 @@
1
+ // ============================================================================
2
+ // SERVER-SIDE BUILDERS for the RESULT view (generative/action tools).
3
+ // Reuses the shared chrome (header, drawer, toast) and CSS classes.
4
+ // ============================================================================
5
+
6
+ import { esc } from "./escape.js";
7
+ import { buildChrome } from "./components.js";
8
+ import { ResultReport, FileChange, ResultSection, statusToBand } from "./types.js";
9
+
10
+ const SUN = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>`;
11
+ const MOON = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>`;
12
+ const STATUS_LABEL: Record<string, string> = { success: "Success", partial: "Partial", noop: "No changes" };
13
+
14
+ export function buildResultHeader(report: ResultReport): string {
15
+ return /* html */ `
16
+ <header class="hdr">
17
+ <div class="brand">
18
+ <span class="dot" aria-hidden="true">
19
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
20
+ </span>
21
+ <div style="min-width:0">
22
+ <h1>${esc(report.meta.title)}</h1>
23
+ <p class="sub">${esc(report.meta.target)}</p>
24
+ </div>
25
+ </div>
26
+ <div class="hdr-actions">
27
+ <button class="btn icon" id="theme-toggle" type="button" aria-label="Toggle colour theme" title="Toggle theme">
28
+ <span class="theme-sun" hidden>${SUN}</span><span class="theme-moon">${MOON}</span>
29
+ </button>
30
+ <button class="btn" id="export-btn" type="button" title="Copy summary as Markdown">
31
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="M7 10l5 5 5-5"/><path d="M12 15V3"/></svg>
32
+ Export
33
+ </button>
34
+ </div>
35
+ </header>`;
36
+ }
37
+
38
+ export function buildResultHero(report: ResultReport): string {
39
+ const band = statusToBand(report.status);
40
+ const chips = (report.stats ?? [])
41
+ .map((s) => `<span class="chip"><span>${esc(s.label)}</span><b>${esc(s.value)}</b></span>`)
42
+ .join("");
43
+ return /* html */ `
44
+ <section class="result-hero" data-band="${band}">
45
+ <span class="glyph" aria-hidden="true">
46
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>
47
+ </span>
48
+ <div class="rh-main">
49
+ <div class="rh-title">${esc(report.headline)}</div>
50
+ ${report.meta.subtitle ? `<div class="rh-sub">${esc(report.meta.subtitle)}</div>` : ""}
51
+ ${chips ? `<div class="chips" style="margin-top:10px">${chips}</div>` : ""}
52
+ </div>
53
+ <span class="status-pill ${band}">${esc(STATUS_LABEL[report.status] ?? report.status)}</span>
54
+ </section>`;
55
+ }
56
+
57
+ export function buildChanges(report: ResultReport): string {
58
+ const changes = report.changes ?? [];
59
+ if (!changes.length) return "";
60
+ const row = (c: FileChange, i: number): string => {
61
+ const delta =
62
+ c.additions != null || c.deletions != null
63
+ ? `<span class="change-delta">${c.additions != null ? `<span class="add">+${esc(c.additions)}</span> ` : ""}${c.deletions != null ? `<span class="del">-${esc(c.deletions)}</span>` : ""}</span>`
64
+ : "";
65
+ const clickable = c.diff ? "clickable" : "";
66
+ return /* html */ `
67
+ <div class="change-row ${clickable}" ${c.diff ? `data-change="${i}" role="button" tabindex="0"` : ""}>
68
+ <span class="kind ${esc(c.kind)}">${esc(c.kind)}</span>
69
+ <span class="change-main">
70
+ <span class="change-path">${esc(c.path)}</span>
71
+ ${c.summary ? `<span class="change-sum">${esc(c.summary)}</span>` : ""}
72
+ </span>
73
+ ${delta}
74
+ </div>`;
75
+ };
76
+ return /* html */ `
77
+ <div class="sec"><h2>Changes</h2><span class="count">${changes.length} file${changes.length === 1 ? "" : "s"}</span></div>
78
+ <div class="changes">${changes.map(row).join("")}</div>`;
79
+ }
80
+
81
+ export function buildSections(report: ResultReport): string {
82
+ const sections = report.sections ?? [];
83
+ if (!sections.length) return "";
84
+ const section = (s: ResultSection): string => {
85
+ const items = s.items
86
+ .map(
87
+ (it) => `
88
+ <div class="section-item">
89
+ <span class="ip ${it.status ? esc(it.status) : ""}"></span>
90
+ <div style="min-width:0"><div class="it">${esc(it.title)}</div>${it.detail ? `<div class="id">${esc(it.detail)}</div>` : ""}</div>
91
+ </div>`
92
+ )
93
+ .join("");
94
+ return /* html */ `
95
+ <div class="sec"><h2>${esc(s.title)}</h2><span class="count">${s.items.length}</span></div>
96
+ <div class="section">${items}</div>`;
97
+ };
98
+ return sections.map(section).join("");
99
+ }
100
+
101
+ export function buildNextSteps(report: ResultReport): string {
102
+ const actions = report.nextActions ?? [];
103
+ if (!actions.length) return "";
104
+ const btns = actions
105
+ .map((a, i) => `<button class="btn${i === 0 ? " primary" : ""}" type="button" data-action="${esc(a.id)}">${esc(a.label)}</button>`)
106
+ .join("");
107
+ return /* html */ `
108
+ <div class="sec"><h2>Next steps</h2></div>
109
+ <div class="next-steps">${btns}</div>`;
110
+ }
111
+
112
+ export { buildChrome };
@@ -0,0 +1,40 @@
1
+ import { ResultReport } from "./types.js";
2
+
3
+ /** Sample action report — drives the result-view demo and tests. */
4
+ export const SAMPLE_RESULT: ResultReport = {
5
+ meta: {
6
+ title: "Component Factory",
7
+ subtitle: "shadcn/ui · Button",
8
+ target: "@repo/ui",
9
+ generatedAt: "2026-06-27",
10
+ tool: "component-factory",
11
+ },
12
+ headline: "Created 3 files for Button",
13
+ status: "success",
14
+ stats: [
15
+ { label: "Created", value: "3" },
16
+ { label: "Template", value: "shadcn/ui" },
17
+ { label: "Variants", value: "6" },
18
+ ],
19
+ changes: [
20
+ { path: "src/components/Button/Button.tsx", kind: "created", summary: "Component with 6 variants + 4 sizes", additions: 84, language: "tsx", diff: "+ import { cva } from 'class-variance-authority';\n+ export const Button = ({ variant, size, ...props }) => {\n+ return <button className={cn(buttonVariants({ variant, size }))} {...props} />;\n+ };" },
21
+ { path: "src/components/Button/Button.test.tsx", kind: "created", summary: "Vitest + RTL suite, 8 cases", additions: 52, language: "tsx" },
22
+ { path: "src/components/Button/Button.stories.tsx", kind: "created", summary: "Storybook stories: Default, variants, sizes", additions: 39, language: "tsx" },
23
+ ],
24
+ sections: [
25
+ {
26
+ title: "Steps",
27
+ items: [
28
+ { title: "Resolved shadcn/ui Button template", status: "ok" },
29
+ { title: "Generated variants with CVA", status: "ok" },
30
+ { title: "Wrote tests + stories", status: "ok" },
31
+ { title: "Skipped: index barrel not updated", detail: "Add `export * from './Button'` to src/components/index.ts", status: "warn" },
32
+ ],
33
+ },
34
+ ],
35
+ nextActions: [
36
+ { id: "review", label: "Review the component", kind: "tool", tool: "component-reviewer", params: { path: "src/components/Button/Button.tsx" }, fallback: "Review src/components/Button/Button.tsx." },
37
+ { id: "story", label: "Generate more stories", kind: "tool", tool: "storybook-generator", params: { path: "src/components/Button/Button.tsx" }, fallback: "Generate Storybook stories for Button." },
38
+ { id: "barrel", label: "Update barrel export", kind: "prompt", prompt: "Add `export * from './Button'` to src/components/index.ts" },
39
+ ],
40
+ };
@@ -0,0 +1,47 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { renderResultHTML, SAMPLE_RESULT, statusToBand } from "./index.js";
3
+ import { esc } from "./escape.js";
4
+ import type { ResultReport } from "./index.js";
5
+
6
+ describe("statusToBand", () => {
7
+ it("maps status to accent band", () => {
8
+ expect(statusToBand("success")).toBe("good");
9
+ expect(statusToBand("partial")).toBe("warn");
10
+ expect(statusToBand("noop")).toBe("good");
11
+ });
12
+ });
13
+
14
+ describe("renderResultHTML", () => {
15
+ const html = renderResultHTML(SAMPLE_RESULT);
16
+
17
+ it("produces a complete self-contained document with no external requests", () => {
18
+ expect(html.startsWith("<!doctype html>")).toBe(true);
19
+ expect(html).not.toMatch(/<link[^>]+href=/i);
20
+ expect(html).not.toMatch(/<script[^>]+src=/i);
21
+ });
22
+
23
+ it("renders the headline, status and every change path", () => {
24
+ expect(html).toContain("Created 3 files for Button");
25
+ expect(html).toContain("Success");
26
+ for (const c of SAMPLE_RESULT.changes ?? []) expect(html).toContain(esc(c.path));
27
+ });
28
+
29
+ it("ships the theme toggle, next-step actions and embedded data", () => {
30
+ expect(html).toContain('id="theme-toggle"');
31
+ expect(html).toContain('data-action="review"');
32
+ expect(html).toContain('id="report-data"');
33
+ const json = html.split('id="report-data" type="application/json">')[1].split("</script>")[0];
34
+ const parsed = JSON.parse(json.replace(/\\u003c/g, "<")) as ResultReport;
35
+ expect(parsed.changes).toHaveLength(3);
36
+ });
37
+
38
+ it("escapes a malicious file path so it cannot break out", () => {
39
+ const evil: ResultReport = {
40
+ ...SAMPLE_RESULT,
41
+ changes: [{ path: `</script><img src=x onerror=alert(1)>`, kind: "created" }],
42
+ };
43
+ const out = renderResultHTML(evil);
44
+ expect(out).not.toContain("<img src=x onerror=alert(1)>");
45
+ expect(out).not.toContain("</script><img");
46
+ });
47
+ });