@urmzd/github-insights 2.1.0 → 2.3.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 (116) hide show
  1. package/.githooks/.sr-hooks-hash +1 -0
  2. package/.githooks/commit-msg +3 -0
  3. package/.githooks/pre-commit +3 -0
  4. package/AGENTS.md +32 -19
  5. package/CHANGELOG.md +62 -0
  6. package/CONTRIBUTING.md +18 -19
  7. package/README.md +21 -24
  8. package/action.yml +1 -1
  9. package/assets/insights/index.svg +45 -4
  10. package/assets/insights/metrics-constellation.svg +55 -0
  11. package/assets/insights/metrics-growth.svg +55 -0
  12. package/assets/insights/metrics-heatmap.svg +55 -0
  13. package/assets/insights/metrics-impact.svg +55 -0
  14. package/assets/insights/metrics-rhythm.svg +55 -0
  15. package/assets/insights/metrics-velocity.svg +55 -0
  16. package/examples/classic/README.md +36 -2
  17. package/examples/classic/index.svg +45 -4
  18. package/examples/classic/metrics-constellation.svg +55 -0
  19. package/examples/classic/metrics-growth.svg +55 -0
  20. package/examples/classic/metrics-heatmap.svg +55 -0
  21. package/examples/classic/metrics-impact.svg +55 -0
  22. package/examples/classic/metrics-rhythm.svg +55 -0
  23. package/examples/classic/metrics-velocity.svg +55 -0
  24. package/examples/ecosystem/README.md +39 -28
  25. package/examples/ecosystem/index.svg +45 -4
  26. package/examples/ecosystem/metrics-constellation.svg +55 -0
  27. package/examples/ecosystem/metrics-growth.svg +55 -0
  28. package/examples/ecosystem/metrics-heatmap.svg +55 -0
  29. package/examples/ecosystem/metrics-impact.svg +55 -0
  30. package/examples/ecosystem/metrics-rhythm.svg +55 -0
  31. package/examples/ecosystem/metrics-velocity.svg +55 -0
  32. package/examples/minimal/README.md +36 -2
  33. package/examples/minimal/index.svg +45 -4
  34. package/examples/minimal/metrics-constellation.svg +55 -0
  35. package/examples/minimal/metrics-growth.svg +55 -0
  36. package/examples/minimal/metrics-heatmap.svg +55 -0
  37. package/examples/minimal/metrics-impact.svg +55 -0
  38. package/examples/minimal/metrics-rhythm.svg +55 -0
  39. package/examples/minimal/metrics-velocity.svg +55 -0
  40. package/examples/modern/README.md +62 -50
  41. package/examples/modern/index.svg +45 -4
  42. package/examples/modern/metrics-constellation.svg +55 -0
  43. package/examples/modern/metrics-growth.svg +55 -0
  44. package/examples/modern/metrics-heatmap.svg +55 -0
  45. package/examples/modern/metrics-impact.svg +55 -0
  46. package/examples/modern/metrics-rhythm.svg +55 -0
  47. package/examples/modern/metrics-velocity.svg +55 -0
  48. package/llms.txt +4 -4
  49. package/package.json +1 -1
  50. package/skills/github-insights/SKILL.md +35 -81
  51. package/sr.yaml +9 -0
  52. package/src/api.ts +2 -140
  53. package/src/components/contribution-heatmap.tsx +43 -0
  54. package/src/components/contribution-rhythm.tsx +152 -0
  55. package/src/components/full-svg.test.tsx +4 -1
  56. package/src/components/full-svg.tsx +14 -7
  57. package/src/components/growth-arc.tsx +119 -0
  58. package/src/components/impact-trail.tsx +90 -0
  59. package/src/components/language-velocity.tsx +181 -0
  60. package/src/components/project-constellation.tsx +97 -0
  61. package/src/components/section.test.tsx +5 -3
  62. package/src/components/section.tsx +5 -13
  63. package/src/components/style-defs.tsx +44 -3
  64. package/src/index.ts +28 -47
  65. package/src/metrics.test.ts +50 -57
  66. package/src/metrics.ts +277 -95
  67. package/src/readme.test.ts +2 -4
  68. package/src/templates.test.ts +19 -16
  69. package/src/templates.ts +30 -16
  70. package/src/theme.ts +11 -1
  71. package/src/types.ts +34 -7
  72. package/assets/insights/metrics-calendar.svg +0 -14
  73. package/assets/insights/metrics-complexity.svg +0 -14
  74. package/assets/insights/metrics-contributions.svg +0 -14
  75. package/assets/insights/metrics-expertise.svg +0 -14
  76. package/assets/insights/metrics-languages.svg +0 -14
  77. package/assets/insights/metrics-pulse.svg +0 -14
  78. package/examples/classic/metrics-calendar.svg +0 -14
  79. package/examples/classic/metrics-complexity.svg +0 -14
  80. package/examples/classic/metrics-contributions.svg +0 -14
  81. package/examples/classic/metrics-expertise.svg +0 -14
  82. package/examples/classic/metrics-languages.svg +0 -14
  83. package/examples/classic/metrics-pulse.svg +0 -14
  84. package/examples/ecosystem/metrics-calendar.svg +0 -14
  85. package/examples/ecosystem/metrics-complexity.svg +0 -14
  86. package/examples/ecosystem/metrics-contributions.svg +0 -14
  87. package/examples/ecosystem/metrics-expertise.svg +0 -14
  88. package/examples/ecosystem/metrics-languages.svg +0 -14
  89. package/examples/ecosystem/metrics-pulse.svg +0 -14
  90. package/examples/minimal/metrics-calendar.svg +0 -14
  91. package/examples/minimal/metrics-complexity.svg +0 -14
  92. package/examples/minimal/metrics-contributions.svg +0 -14
  93. package/examples/minimal/metrics-expertise.svg +0 -14
  94. package/examples/minimal/metrics-languages.svg +0 -14
  95. package/examples/minimal/metrics-pulse.svg +0 -14
  96. package/examples/modern/metrics-calendar.svg +0 -14
  97. package/examples/modern/metrics-complexity.svg +0 -14
  98. package/examples/modern/metrics-contributions.svg +0 -14
  99. package/examples/modern/metrics-expertise.svg +0 -14
  100. package/examples/modern/metrics-languages.svg +0 -14
  101. package/examples/modern/metrics-pulse.svg +0 -14
  102. package/src/components/bar-chart.test.tsx +0 -38
  103. package/src/components/bar-chart.tsx +0 -54
  104. package/src/components/contribution-calendar.test.tsx +0 -44
  105. package/src/components/contribution-calendar.tsx +0 -94
  106. package/src/components/contribution-cards.test.tsx +0 -36
  107. package/src/components/contribution-cards.tsx +0 -58
  108. package/src/components/donut-chart.test.tsx +0 -36
  109. package/src/components/donut-chart.tsx +0 -102
  110. package/src/components/project-cards.test.tsx +0 -46
  111. package/src/components/project-cards.tsx +0 -66
  112. package/src/components/stat-cards.test.tsx +0 -32
  113. package/src/components/stat-cards.tsx +0 -57
  114. package/src/components/tech-highlights.test.tsx +0 -63
  115. package/src/components/tech-highlights.tsx +0 -109
  116. package/teasr.toml +0 -14
@@ -0,0 +1,119 @@
1
+ import { Fragment, h } from "../jsx-factory.js";
2
+ import { escapeXml } from "../svg-utils.js";
3
+ import { LAYOUT, THEME } from "../theme.js";
4
+ import type { GrowthArcPoint, RenderResult } from "../types.js";
5
+
6
+ export function renderGrowthArc(
7
+ points: GrowthArcPoint[],
8
+ y: number,
9
+ ): RenderResult {
10
+ if (points.length < 2) return { svg: "", height: 0 };
11
+
12
+ const { padX } = LAYOUT;
13
+ const chartWidth = 760;
14
+ const chartHeight = 120;
15
+ const labelHeight = 20;
16
+ const totalHeight = chartHeight + labelHeight;
17
+
18
+ const maxComplexity = Math.max(...points.map((p) => p.avgComplexity));
19
+ const minComplexity = Math.min(...points.map((p) => p.avgComplexity));
20
+ const range = maxComplexity - minComplexity || 1;
21
+
22
+ const stepX = chartWidth / Math.max(points.length - 1, 1);
23
+
24
+ // Compute point positions
25
+ const coords = points.map((p, i) => ({
26
+ x: padX + i * stepX,
27
+ y:
28
+ y +
29
+ chartHeight -
30
+ ((p.avgComplexity - minComplexity) / range) * (chartHeight - 20) -
31
+ 10,
32
+ ...p,
33
+ }));
34
+
35
+ // Build smooth path
36
+ let pathD = `M ${coords[0].x},${coords[0].y}`;
37
+ for (let i = 1; i < coords.length; i++) {
38
+ const prev = coords[i - 1];
39
+ const curr = coords[i];
40
+ const cpx = (prev.x + curr.x) / 2;
41
+ pathD += ` C ${cpx},${prev.y} ${cpx},${curr.y} ${curr.x},${curr.y}`;
42
+ }
43
+
44
+ // Build filled area path
45
+ let areaD = pathD;
46
+ areaD += ` L ${coords[coords.length - 1].x},${y + chartHeight}`;
47
+ areaD += ` L ${coords[0].x},${y + chartHeight} Z`;
48
+
49
+ const svg = (
50
+ <>
51
+ {/* Filled area under curve */}
52
+ <path d={areaD} fill="#58a6ff" fill-opacity="0.15" className="fade-1" />
53
+
54
+ {/* Line */}
55
+ <path
56
+ d={pathD}
57
+ fill="none"
58
+ stroke="#58a6ff"
59
+ stroke-width="2.5"
60
+ stroke-linecap="round"
61
+ stroke-linejoin="round"
62
+ className="fade-1"
63
+ />
64
+
65
+ {/* Data points */}
66
+ {coords.map((p, i) => (
67
+ <circle
68
+ cx={p.x}
69
+ cy={p.y}
70
+ r="4"
71
+ fill="#58a6ff"
72
+ className={`fade-${Math.min(i + 1, 6)}`}
73
+ />
74
+ ))}
75
+
76
+ {/* Point labels (complexity value) */}
77
+ {coords.map((p) => (
78
+ <text
79
+ x={p.x}
80
+ y={p.y - 10}
81
+ className="t t-value"
82
+ text-anchor="middle"
83
+ font-size="10"
84
+ >
85
+ {p.avgComplexity.toFixed(0)}
86
+ </text>
87
+ ))}
88
+
89
+ {/* X-axis labels (relative time) */}
90
+ {coords.map((p) => (
91
+ <text
92
+ x={p.x}
93
+ y={y + chartHeight + 14}
94
+ className="t t-value"
95
+ text-anchor="middle"
96
+ >
97
+ {escapeXml(p.label)}
98
+ </text>
99
+ ))}
100
+
101
+ {/* Repo count annotations */}
102
+ {coords.map((p) => (
103
+ <text
104
+ x={p.x}
105
+ y={y + chartHeight + 24}
106
+ className="t t-muted"
107
+ text-anchor="middle"
108
+ font-size="9"
109
+ >
110
+ {`${p.repoCount} repo${p.repoCount !== 1 ? "s" : ""}`}
111
+ </text>
112
+ ))}
113
+ </>
114
+ );
115
+
116
+ return { svg, height: totalHeight + 10 };
117
+ }
118
+
119
+ void Fragment;
@@ -0,0 +1,90 @@
1
+ import { Fragment, h } from "../jsx-factory.js";
2
+ import { escapeXml, truncate } from "../svg-utils.js";
3
+ import { BAR_COLORS, LAYOUT, THEME } from "../theme.js";
4
+ import type { ExternalRepo, RenderResult } from "../types.js";
5
+
6
+ export function renderImpactTrail(
7
+ repos: ExternalRepo[],
8
+ y: number,
9
+ ): RenderResult {
10
+ if (repos.length === 0) return { svg: "", height: 0 };
11
+
12
+ const { padX } = LAYOUT;
13
+ const rowHeight = 36;
14
+ const nameWidth = 280;
15
+ const barMaxWidth = 400;
16
+ const gap = 6;
17
+
18
+ // Sort by stars (impact proxy)
19
+ const sorted = [...repos].sort((a, b) => b.stargazerCount - a.stargazerCount);
20
+ const maxStars = Math.max(sorted[0]?.stargazerCount || 1, 1);
21
+ const maxLog = Math.log2(maxStars + 1);
22
+
23
+ let svg = "";
24
+
25
+ for (let i = 0; i < sorted.length; i++) {
26
+ const repo = sorted[i];
27
+ const ry = y + i * (rowHeight + gap);
28
+ const color = BAR_COLORS[i % BAR_COLORS.length];
29
+ const delay = Math.min(i + 1, 6);
30
+
31
+ // Bar width proportional to log(stars)
32
+ const logStars = Math.log2(repo.stargazerCount + 1);
33
+ const barWidth = Math.max(4, (logStars / maxLog) * barMaxWidth);
34
+
35
+ // Language dot
36
+ const langName = repo.primaryLanguage?.name || "";
37
+
38
+ svg += (
39
+ <>
40
+ {/* Repo name */}
41
+ <text
42
+ x={padX}
43
+ y={ry + rowHeight / 2 + 4}
44
+ className={`t t-card-title fade-${delay}`}
45
+ >
46
+ {escapeXml(truncate(repo.nameWithOwner, 38))}
47
+ </text>
48
+
49
+ {/* Impact bar */}
50
+ <rect
51
+ x={padX + nameWidth}
52
+ y={ry + rowHeight / 2 - 6}
53
+ width={barWidth}
54
+ height="12"
55
+ rx="3"
56
+ fill={color}
57
+ fill-opacity="0.7"
58
+ className={`fade-${delay}`}
59
+ />
60
+
61
+ {/* Star count */}
62
+ <text
63
+ x={padX + nameWidth + barMaxWidth + 16}
64
+ y={ry + rowHeight / 2 + 4}
65
+ className={`t t-value fade-${delay}`}
66
+ >
67
+ {`\u2605 ${repo.stargazerCount.toLocaleString()}`}
68
+ </text>
69
+
70
+ {/* Language label */}
71
+ {langName ? (
72
+ <text
73
+ x={padX + nameWidth + barMaxWidth + 80}
74
+ y={ry + rowHeight / 2 + 4}
75
+ className={`t t-value fade-${delay}`}
76
+ >
77
+ {escapeXml(langName)}
78
+ </text>
79
+ ) : (
80
+ ""
81
+ )}
82
+ </>
83
+ );
84
+ }
85
+
86
+ const totalHeight = sorted.length * (rowHeight + gap) - gap;
87
+ return { svg, height: totalHeight };
88
+ }
89
+
90
+ void Fragment;
@@ -0,0 +1,181 @@
1
+ import { Fragment, h } from "../jsx-factory.js";
2
+ import { escapeXml } from "../svg-utils.js";
3
+ import { LAYOUT, THEME } from "../theme.js";
4
+ import type { MonthlyLanguageBucket, RenderResult } from "../types.js";
5
+
6
+ export function renderLanguageVelocity(
7
+ velocity: MonthlyLanguageBucket[],
8
+ y: number,
9
+ ): RenderResult {
10
+ if (velocity.length === 0) return { svg: "", height: 0 };
11
+
12
+ const { padX } = LAYOUT;
13
+ const chartWidth = 760;
14
+ const chartHeight = 140;
15
+ const labelHeight = 20;
16
+ const totalHeight = chartHeight + labelHeight;
17
+
18
+ // Collect all unique languages across all months
19
+ const langSet = new Map<string, string>();
20
+ for (const bucket of velocity) {
21
+ for (const lang of bucket.languages) {
22
+ if (!langSet.has(lang.name)) {
23
+ langSet.set(lang.name, lang.color);
24
+ }
25
+ }
26
+ }
27
+
28
+ // Get top languages by total commits
29
+ const langTotals = new Map<string, number>();
30
+ for (const bucket of velocity) {
31
+ for (const lang of bucket.languages) {
32
+ langTotals.set(
33
+ lang.name,
34
+ (langTotals.get(lang.name) || 0) + lang.commits,
35
+ );
36
+ }
37
+ }
38
+ const topLangs = [...langTotals.entries()]
39
+ .sort((a, b) => b[1] - a[1])
40
+ .slice(0, 8)
41
+ .map(([name]) => name);
42
+
43
+ // Compute max total per month for scaling
44
+ const monthTotals = velocity.map((bucket) =>
45
+ bucket.languages
46
+ .filter((l) => topLangs.includes(l.name))
47
+ .reduce((sum, l) => sum + l.commits, 0),
48
+ );
49
+ const maxTotal = Math.max(...monthTotals, 1);
50
+
51
+ // Build stacked area data
52
+ const stepX = chartWidth / Math.max(velocity.length - 1, 1);
53
+ const paths: { path: string; color: string; name: string }[] = [];
54
+
55
+ // For each language, build a path from bottom of its stack to top
56
+ for (let li = topLangs.length - 1; li >= 0; li--) {
57
+ const langName = topLangs[li];
58
+ const color = langSet.get(langName) || "#8b949e";
59
+
60
+ // Compute cumulative values for this language
61
+ const upperPoints: { x: number; y: number }[] = [];
62
+ const lowerPoints: { x: number; y: number }[] = [];
63
+
64
+ for (let mi = 0; mi < velocity.length; mi++) {
65
+ const bucket = velocity[mi];
66
+ const x = padX + mi * stepX;
67
+
68
+ // Sum commits for all languages below this one (for stacking)
69
+ let below = 0;
70
+ let current = 0;
71
+ for (let k = 0; k < topLangs.length; k++) {
72
+ const langCommits =
73
+ bucket.languages.find((l) => l.name === topLangs[k])?.commits || 0;
74
+ if (k < li) below += langCommits;
75
+ if (k === li) current = langCommits;
76
+ }
77
+
78
+ const bottomY = y + chartHeight - (below / maxTotal) * chartHeight;
79
+ const topY =
80
+ y + chartHeight - ((below + current) / maxTotal) * chartHeight;
81
+
82
+ lowerPoints.push({ x, y: bottomY });
83
+ upperPoints.push({ x, y: topY });
84
+ }
85
+
86
+ // Build smooth path using the points
87
+ if (upperPoints.length < 2) continue;
88
+
89
+ let d = `M ${upperPoints[0].x},${upperPoints[0].y}`;
90
+
91
+ // Upper edge (left to right) with smooth curves
92
+ for (let i = 1; i < upperPoints.length; i++) {
93
+ const prev = upperPoints[i - 1];
94
+ const curr = upperPoints[i];
95
+ const cpx = (prev.x + curr.x) / 2;
96
+ d += ` C ${cpx},${prev.y} ${cpx},${curr.y} ${curr.x},${curr.y}`;
97
+ }
98
+
99
+ // Lower edge (right to left) with smooth curves
100
+ d += ` L ${lowerPoints[lowerPoints.length - 1].x},${lowerPoints[lowerPoints.length - 1].y}`;
101
+ for (let i = lowerPoints.length - 2; i >= 0; i--) {
102
+ const prev = lowerPoints[i + 1];
103
+ const curr = lowerPoints[i];
104
+ const cpx = (prev.x + curr.x) / 2;
105
+ d += ` C ${cpx},${prev.y} ${cpx},${curr.y} ${curr.x},${curr.y}`;
106
+ }
107
+
108
+ d += " Z";
109
+ paths.push({ path: d, color, name: langName });
110
+ }
111
+
112
+ // Month labels
113
+ const monthLabels = velocity
114
+ .filter((_, i) => i % Math.max(1, Math.floor(velocity.length / 6)) === 0)
115
+ .map((bucket) => {
116
+ const originalIndex = velocity.indexOf(bucket);
117
+ const x = padX + originalIndex * stepX;
118
+ const monthName = new Date(`${bucket.month}-01`).toLocaleDateString(
119
+ "en",
120
+ { month: "short" },
121
+ );
122
+ return { x, label: monthName };
123
+ });
124
+
125
+ const svg = (
126
+ <>
127
+ {/* Streamgraph paths */}
128
+ {paths.map((p, i) => (
129
+ <path
130
+ d={p.path}
131
+ fill={p.color}
132
+ fill-opacity="0.75"
133
+ className={`fade-${Math.min(i + 1, 6)}`}
134
+ />
135
+ ))}
136
+
137
+ {/* Language legend (inline, below chart) */}
138
+ {(() => {
139
+ let legendX = padX;
140
+ return topLangs.map((name) => {
141
+ const color = langSet.get(name) || "#8b949e";
142
+ const x = legendX;
143
+ legendX += name.length * 7 + 28;
144
+ return (
145
+ <>
146
+ <rect
147
+ x={x}
148
+ y={y + chartHeight + 6}
149
+ width="8"
150
+ height="8"
151
+ rx="2"
152
+ fill={color}
153
+ opacity="0.85"
154
+ />
155
+ <text x={x + 12} y={y + chartHeight + 14} className="t t-value">
156
+ {escapeXml(name)}
157
+ </text>
158
+ </>
159
+ );
160
+ });
161
+ })()}
162
+
163
+ {/* Month labels */}
164
+ {monthLabels.map((m) => (
165
+ <text
166
+ x={m.x}
167
+ y={y + chartHeight + 14}
168
+ className="t t-value"
169
+ text-anchor="start"
170
+ opacity="0"
171
+ >
172
+ {escapeXml(m.label)}
173
+ </text>
174
+ ))}
175
+ </>
176
+ );
177
+
178
+ return { svg, height: totalHeight };
179
+ }
180
+
181
+ void Fragment;
@@ -0,0 +1,97 @@
1
+ import { Fragment, h } from "../jsx-factory.js";
2
+ import { escapeXml, truncate } from "../svg-utils.js";
3
+ import { LAYOUT, THEME } from "../theme.js";
4
+ import type { ConstellationNode, RenderResult } from "../types.js";
5
+
6
+ export function renderProjectConstellation(
7
+ nodes: ConstellationNode[],
8
+ y: number,
9
+ ): RenderResult {
10
+ if (nodes.length === 0) return { svg: "", height: 0 };
11
+
12
+ const { padX } = LAYOUT;
13
+ const height = 380;
14
+
15
+ // Draw connection lines first (behind nodes)
16
+ const drawnConnections = new Set<string>();
17
+ const connectionsSvg = nodes.flatMap((node, i) =>
18
+ node.connections
19
+ .filter((j) => {
20
+ const key = [Math.min(i, j), Math.max(i, j)].join("-");
21
+ if (drawnConnections.has(key)) return false;
22
+ drawnConnections.add(key);
23
+ return true;
24
+ })
25
+ .map((j) => {
26
+ const other = nodes[j];
27
+ return (
28
+ <line
29
+ x1={padX + node.x}
30
+ y1={y + node.y}
31
+ x2={padX + other.x}
32
+ y2={y + other.y}
33
+ stroke={THEME.border}
34
+ stroke-width="1"
35
+ stroke-opacity="0.15"
36
+ stroke-dasharray="4 4"
37
+ />
38
+ );
39
+ }),
40
+ );
41
+
42
+ // Draw nodes
43
+ const nodesSvg = nodes.map((node, i) => {
44
+ const cx = padX + node.x;
45
+ const cy = y + node.y;
46
+ const delay = Math.min(i + 1, 6);
47
+
48
+ return (
49
+ <>
50
+ {/* Glow effect */}
51
+ <circle
52
+ cx={cx}
53
+ cy={cy}
54
+ r={node.radius + 4}
55
+ fill={node.color}
56
+ fill-opacity="0.08"
57
+ className={`fade-${delay}`}
58
+ />
59
+ {/* Main circle */}
60
+ <circle
61
+ cx={cx}
62
+ cy={cy}
63
+ r={node.radius}
64
+ fill={node.color}
65
+ fill-opacity="0.7"
66
+ stroke={node.color}
67
+ stroke-width="1.5"
68
+ stroke-opacity="0.9"
69
+ className={`fade-${delay}`}
70
+ />
71
+ {/* Label */}
72
+ <text
73
+ x={cx}
74
+ y={cy + node.radius + 14}
75
+ className={`t t-value fade-${delay}`}
76
+ text-anchor="middle"
77
+ >
78
+ {escapeXml(truncate(node.name, 18))}
79
+ </text>
80
+ </>
81
+ );
82
+ });
83
+
84
+ const svg = (
85
+ <>
86
+ {/* Connection lines */}
87
+ {connectionsSvg.join("")}
88
+
89
+ {/* Nodes */}
90
+ {nodesSvg.join("")}
91
+ </>
92
+ );
93
+
94
+ return { svg, height };
95
+ }
96
+
97
+ void Fragment;
@@ -61,9 +61,11 @@ describe("renderSection", () => {
61
61
  expect(result.height).toBeGreaterThan(0);
62
62
  });
63
63
 
64
- it("returns { svg, height } with items array", () => {
65
- const items = [{ name: "Go", value: 10 }];
66
- const result = renderSection("Tech", "Detected", items);
64
+ it("returns { svg, height } with another render function", () => {
65
+ const result = renderSection("Tech", "Detected", (y) => ({
66
+ svg: `<text y="${y}">Go</text>`,
67
+ height: 30,
68
+ }));
67
69
  expect(result.svg).toContain("Go");
68
70
  });
69
71
  });
@@ -1,8 +1,7 @@
1
1
  import { Fragment, h } from "../jsx-factory.js";
2
2
  import { escapeXml } from "../svg-utils.js";
3
3
  import { LAYOUT, THEME } from "../theme.js";
4
- import type { BarItem, RenderResult } from "../types.js";
5
- import { renderBarChart } from "./bar-chart.js";
4
+ import type { RenderResult } from "../types.js";
6
5
 
7
6
  export function renderSectionHeader(
8
7
  title: string,
@@ -53,8 +52,7 @@ export function renderDivider(y: number): RenderResult {
53
52
  export function renderSection(
54
53
  title: string,
55
54
  subtitle: string,
56
- itemsOrRenderBody: ((y: number) => RenderResult) | BarItem[],
57
- options: Record<string, unknown> = {},
55
+ renderBody: (y: number) => RenderResult,
58
56
  ): RenderResult {
59
57
  let y = LAYOUT.padY;
60
58
  let svg = "";
@@ -63,15 +61,9 @@ export function renderSection(
63
61
  svg += header.svg;
64
62
  y += header.height;
65
63
 
66
- if (typeof itemsOrRenderBody === "function") {
67
- const body = itemsOrRenderBody(y);
68
- svg += body.svg;
69
- y += body.height + LAYOUT.padY;
70
- } else {
71
- const bars = renderBarChart(itemsOrRenderBody, y, options);
72
- svg += bars.svg;
73
- y += bars.height + LAYOUT.padY;
74
- }
64
+ const body = renderBody(y);
65
+ svg += body.svg;
66
+ y += body.height + LAYOUT.padY;
75
67
 
76
68
  return { svg, height: y };
77
69
  }
@@ -1,13 +1,13 @@
1
1
  import { Fragment, h } from "../jsx-factory.js";
2
- import { FONT, THEME } from "../theme.js";
2
+ import { FONT, THEME, THEME_LIGHT } from "../theme.js";
3
3
 
4
4
  export function StyleDefs(): string {
5
5
  return (
6
6
  <defs>
7
7
  <style>
8
8
  {`
9
- .t { font-family: ${FONT}; }
10
- .t-h { font-size: 13px; fill: ${THEME.text}; letter-spacing: 1.5px; font-weight: 600; }
9
+ .t { font-family: ${FONT}; font-variant-numeric: tabular-lining; }
10
+ .t-h { font-size: 14px; fill: ${THEME.text}; letter-spacing: 2px; font-weight: 600; }
11
11
  .t-sub { font-size: 11px; fill: ${THEME.muted}; }
12
12
  .t-label { font-size: 12px; fill: ${THEME.secondary}; }
13
13
  .t-value { font-size: 11px; fill: ${THEME.muted}; }
@@ -18,6 +18,47 @@ export function StyleDefs(): string {
18
18
  .t-card-detail { font-size: 11px; fill: ${THEME.secondary}; }
19
19
  .t-pill { font-size: 11px; font-weight: 600; }
20
20
  .t-bullet { font-size: 12px; fill: ${THEME.text}; }
21
+ .bg-fill { fill: ${THEME.bg}; }
22
+ .card-fill { fill: ${THEME.cardBg}; }
23
+ .border-stroke { stroke: ${THEME.border}; }
24
+
25
+ @media (prefers-color-scheme: light) {
26
+ .bg-fill { fill: ${THEME_LIGHT.bg}; }
27
+ .card-fill { fill: ${THEME_LIGHT.cardBg}; }
28
+ .border-stroke { stroke: ${THEME_LIGHT.border}; }
29
+ .t-h { fill: ${THEME_LIGHT.text}; }
30
+ .t-sub { fill: ${THEME_LIGHT.muted}; }
31
+ .t-label { fill: ${THEME_LIGHT.secondary}; }
32
+ .t-value { fill: ${THEME_LIGHT.muted}; }
33
+ .t-subhdr { fill: ${THEME_LIGHT.secondary}; }
34
+ .t-stat-label { fill: ${THEME_LIGHT.secondary}; }
35
+ .t-card-title { fill: ${THEME_LIGHT.link}; }
36
+ .t-card-detail { fill: ${THEME_LIGHT.secondary}; }
37
+ .t-bullet { fill: ${THEME_LIGHT.text}; }
38
+ }
39
+
40
+ @keyframes fadeIn {
41
+ from { opacity: 0; transform: translateY(8px); }
42
+ to { opacity: 1; transform: translateY(0); }
43
+ }
44
+ @keyframes scaleIn {
45
+ from { transform: scale(0); opacity: 0; }
46
+ to { transform: scale(1); opacity: 1; }
47
+ }
48
+ @keyframes drawPath {
49
+ from { stroke-dashoffset: var(--path-length); }
50
+ to { stroke-dashoffset: 0; }
51
+ }
52
+ @keyframes radarReveal {
53
+ from { transform: scale(0); opacity: 0; }
54
+ to { transform: scale(1); opacity: 0.6; }
55
+ }
56
+ .fade-1 { animation: fadeIn 0.6s ease-out 0.1s both; }
57
+ .fade-2 { animation: fadeIn 0.6s ease-out 0.25s both; }
58
+ .fade-3 { animation: fadeIn 0.6s ease-out 0.4s both; }
59
+ .fade-4 { animation: fadeIn 0.6s ease-out 0.55s both; }
60
+ .fade-5 { animation: fadeIn 0.6s ease-out 0.7s both; }
61
+ .fade-6 { animation: fadeIn 0.6s ease-out 0.85s both; }
21
62
  `}
22
63
  </style>
23
64
  </defs>