@urmzd/github-insights 2.0.1 → 2.2.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 (103) hide show
  1. package/.githooks/commit-msg +4 -0
  2. package/.githooks/pre-commit +4 -0
  3. package/AGENTS.md +32 -19
  4. package/CHANGELOG.md +54 -0
  5. package/CONTRIBUTING.md +18 -19
  6. package/README.md +21 -24
  7. package/action.yml +1 -1
  8. package/assets/insights/index.svg +45 -4
  9. package/assets/insights/metrics-constellation.svg +55 -0
  10. package/assets/insights/metrics-impact.svg +55 -0
  11. package/assets/insights/metrics-rhythm.svg +55 -0
  12. package/assets/insights/metrics-velocity.svg +55 -0
  13. package/examples/classic/README.md +36 -2
  14. package/examples/classic/index.svg +45 -4
  15. package/examples/classic/metrics-constellation.svg +55 -0
  16. package/examples/classic/metrics-impact.svg +55 -0
  17. package/examples/classic/metrics-rhythm.svg +55 -0
  18. package/examples/classic/metrics-velocity.svg +55 -0
  19. package/examples/ecosystem/README.md +39 -28
  20. package/examples/ecosystem/index.svg +45 -4
  21. package/examples/ecosystem/metrics-constellation.svg +55 -0
  22. package/examples/ecosystem/metrics-impact.svg +55 -0
  23. package/examples/ecosystem/metrics-rhythm.svg +55 -0
  24. package/examples/ecosystem/metrics-velocity.svg +55 -0
  25. package/examples/minimal/README.md +36 -2
  26. package/examples/minimal/index.svg +45 -4
  27. package/examples/minimal/metrics-constellation.svg +55 -0
  28. package/examples/minimal/metrics-impact.svg +55 -0
  29. package/examples/minimal/metrics-rhythm.svg +55 -0
  30. package/examples/minimal/metrics-velocity.svg +55 -0
  31. package/examples/modern/README.md +62 -50
  32. package/examples/modern/index.svg +45 -4
  33. package/examples/modern/metrics-constellation.svg +55 -0
  34. package/examples/modern/metrics-impact.svg +55 -0
  35. package/examples/modern/metrics-rhythm.svg +55 -0
  36. package/examples/modern/metrics-velocity.svg +55 -0
  37. package/llms.txt +4 -4
  38. package/package.json +1 -1
  39. package/skills/github-insights/SKILL.md +35 -81
  40. package/sr.yaml +9 -0
  41. package/src/api.ts +3 -141
  42. package/src/components/contribution-rhythm.tsx +152 -0
  43. package/src/components/full-svg.test.tsx +4 -1
  44. package/src/components/full-svg.tsx +14 -7
  45. package/src/components/impact-trail.tsx +90 -0
  46. package/src/components/language-velocity.tsx +181 -0
  47. package/src/components/project-constellation.tsx +97 -0
  48. package/src/components/section.test.tsx +5 -3
  49. package/src/components/section.tsx +5 -13
  50. package/src/components/style-defs.tsx +44 -3
  51. package/src/index.ts +34 -47
  52. package/src/metrics.test.ts +50 -57
  53. package/src/metrics.ts +293 -97
  54. package/src/readme.test.ts +2 -4
  55. package/src/templates.test.ts +116 -16
  56. package/src/templates.ts +68 -27
  57. package/src/theme.ts +11 -1
  58. package/src/types.ts +31 -7
  59. package/assets/insights/metrics-calendar.svg +0 -14
  60. package/assets/insights/metrics-complexity.svg +0 -14
  61. package/assets/insights/metrics-contributions.svg +0 -14
  62. package/assets/insights/metrics-expertise.svg +0 -14
  63. package/assets/insights/metrics-languages.svg +0 -14
  64. package/assets/insights/metrics-pulse.svg +0 -14
  65. package/examples/classic/metrics-calendar.svg +0 -14
  66. package/examples/classic/metrics-complexity.svg +0 -14
  67. package/examples/classic/metrics-contributions.svg +0 -14
  68. package/examples/classic/metrics-expertise.svg +0 -14
  69. package/examples/classic/metrics-languages.svg +0 -14
  70. package/examples/classic/metrics-pulse.svg +0 -14
  71. package/examples/ecosystem/metrics-calendar.svg +0 -14
  72. package/examples/ecosystem/metrics-complexity.svg +0 -14
  73. package/examples/ecosystem/metrics-contributions.svg +0 -14
  74. package/examples/ecosystem/metrics-expertise.svg +0 -14
  75. package/examples/ecosystem/metrics-languages.svg +0 -14
  76. package/examples/ecosystem/metrics-pulse.svg +0 -14
  77. package/examples/minimal/metrics-calendar.svg +0 -14
  78. package/examples/minimal/metrics-complexity.svg +0 -14
  79. package/examples/minimal/metrics-contributions.svg +0 -14
  80. package/examples/minimal/metrics-expertise.svg +0 -14
  81. package/examples/minimal/metrics-languages.svg +0 -14
  82. package/examples/minimal/metrics-pulse.svg +0 -14
  83. package/examples/modern/metrics-calendar.svg +0 -14
  84. package/examples/modern/metrics-complexity.svg +0 -14
  85. package/examples/modern/metrics-contributions.svg +0 -14
  86. package/examples/modern/metrics-expertise.svg +0 -14
  87. package/examples/modern/metrics-languages.svg +0 -14
  88. package/examples/modern/metrics-pulse.svg +0 -14
  89. package/src/components/bar-chart.test.tsx +0 -38
  90. package/src/components/bar-chart.tsx +0 -54
  91. package/src/components/contribution-calendar.test.tsx +0 -44
  92. package/src/components/contribution-calendar.tsx +0 -94
  93. package/src/components/contribution-cards.test.tsx +0 -36
  94. package/src/components/contribution-cards.tsx +0 -58
  95. package/src/components/donut-chart.test.tsx +0 -36
  96. package/src/components/donut-chart.tsx +0 -102
  97. package/src/components/project-cards.test.tsx +0 -46
  98. package/src/components/project-cards.tsx +0 -66
  99. package/src/components/stat-cards.test.tsx +0 -32
  100. package/src/components/stat-cards.tsx +0 -57
  101. package/src/components/tech-highlights.test.tsx +0 -63
  102. package/src/components/tech-highlights.tsx +0 -109
  103. package/teasr.toml +0 -14
@@ -1,54 +0,0 @@
1
- import { Fragment, h } from "../jsx-factory.js";
2
- import { escapeXml, truncate } from "../svg-utils.js";
3
- import { BAR_COLORS, LAYOUT } from "../theme.js";
4
- import type { BarItem, RenderResult } from "../types.js";
5
-
6
- export function renderBarChart(
7
- items: BarItem[],
8
- y: number,
9
- options: Record<string, unknown> = {},
10
- ): RenderResult {
11
- if (items.length === 0) return { svg: "", height: 0 };
12
-
13
- const { barHeight, barRowHeight, barMaxWidth, padX } = LAYOUT;
14
- const useItemColors = options.useItemColors === true;
15
- const maxValue = Math.max(...items.map((d) => d.value));
16
-
17
- const svg = (
18
- <>
19
- {items.map((item, i) => {
20
- const ry = y + i * barRowHeight;
21
- const barWidth = Math.max((item.value / maxValue) * barMaxWidth, 4);
22
- const color = useItemColors
23
- ? item.color || BAR_COLORS[i % BAR_COLORS.length]
24
- : BAR_COLORS[i % BAR_COLORS.length];
25
- const label = escapeXml(truncate(item.name, 40));
26
- const valueLabel = item.percent
27
- ? `${item.percent}%`
28
- : String(item.value);
29
-
30
- return (
31
- <>
32
- <text x={padX} y={ry + 14} className="t t-label">
33
- {label}
34
- </text>
35
- <rect
36
- x={padX}
37
- y={ry + 26}
38
- width={barWidth}
39
- height={barHeight}
40
- rx="3"
41
- fill={color}
42
- opacity="0.85"
43
- />
44
- <text x={padX + barWidth + 8} y={ry + 40} className="t t-value">
45
- {valueLabel}
46
- </text>
47
- </>
48
- );
49
- })}
50
- </>
51
- );
52
-
53
- return { svg, height: items.length * barRowHeight };
54
- }
@@ -1,44 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { makeContributionCalendar } from "../__fixtures__/repos.js";
3
- import { Fragment, h } from "../jsx-factory.js";
4
- import { renderContributionCalendar } from "./contribution-calendar.js";
5
-
6
- void Fragment;
7
-
8
- describe("renderContributionCalendar", () => {
9
- const calendar = makeContributionCalendar();
10
-
11
- it("returns { svg, height }", () => {
12
- const result = renderContributionCalendar(calendar, 0);
13
- expect(result).toHaveProperty("svg");
14
- expect(result).toHaveProperty("height");
15
- expect(typeof result.svg).toBe("string");
16
- expect(result.height).toBeGreaterThan(0);
17
- });
18
-
19
- it("renders rect cells for each contribution day", () => {
20
- const { svg } = renderContributionCalendar(calendar, 0);
21
- // 2 weeks x 7 days = 14 rects
22
- const rectCount = (svg.match(/<rect /g) || []).length;
23
- expect(rectCount).toBe(14);
24
- });
25
-
26
- it("includes month labels", () => {
27
- const { svg } = renderContributionCalendar(calendar, 0);
28
- expect(svg).toContain("Jan");
29
- });
30
-
31
- it("includes day labels", () => {
32
- const { svg } = renderContributionCalendar(calendar, 0);
33
- expect(svg).toContain("Mon");
34
- expect(svg).toContain("Wed");
35
- expect(svg).toContain("Fri");
36
- });
37
-
38
- it("uses colors from the calendar data", () => {
39
- const { svg } = renderContributionCalendar(calendar, 0);
40
- expect(svg).toContain("#0e4429");
41
- expect(svg).toContain("#006d32");
42
- expect(svg).toContain("#39d353");
43
- });
44
- });
@@ -1,94 +0,0 @@
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 { ContributionCalendar, RenderResult } from "../types.js";
5
-
6
- const CELL_SIZE = 11;
7
- const CELL_GAP = 2;
8
- const STEP = CELL_SIZE + CELL_GAP;
9
- const DAY_LABEL_WIDTH = 30;
10
- const MONTH_LABEL_HEIGHT = 16;
11
- const DAY_LABELS = ["", "Mon", "", "Wed", "", "Fri", ""];
12
- const MONTH_NAMES = [
13
- "Jan",
14
- "Feb",
15
- "Mar",
16
- "Apr",
17
- "May",
18
- "Jun",
19
- "Jul",
20
- "Aug",
21
- "Sep",
22
- "Oct",
23
- "Nov",
24
- "Dec",
25
- ];
26
-
27
- export function renderContributionCalendar(
28
- calendar: ContributionCalendar,
29
- y: number,
30
- ): RenderResult {
31
- const { padX } = LAYOUT;
32
- const weeks = calendar.weeks;
33
- const gridX = padX + DAY_LABEL_WIDTH;
34
- const gridY = y + MONTH_LABEL_HEIGHT;
35
-
36
- // Build month labels from the first day of each week
37
- const monthLabels: { label: string; x: number }[] = [];
38
- let lastMonth = -1;
39
- for (let w = 0; w < weeks.length; w++) {
40
- const days = weeks[w].contributionDays;
41
- if (days.length === 0) continue;
42
- const month = new Date(days[0].date).getMonth();
43
- if (month !== lastMonth) {
44
- monthLabels.push({
45
- label: MONTH_NAMES[month],
46
- x: gridX + w * STEP,
47
- });
48
- lastMonth = month;
49
- }
50
- }
51
-
52
- const svg = (
53
- <>
54
- {/* Month labels */}
55
- {monthLabels.map((m) => (
56
- <text x={m.x} y={y + 11} className="t t-value">
57
- {escapeXml(m.label)}
58
- </text>
59
- ))}
60
- {/* Day labels */}
61
- {DAY_LABELS.map((label, d) =>
62
- label ? (
63
- <text
64
- x={padX}
65
- y={gridY + d * STEP + CELL_SIZE - 1}
66
- className="t t-value"
67
- >
68
- {escapeXml(label)}
69
- </text>
70
- ) : (
71
- ""
72
- ),
73
- )}
74
- {/* Cells */}
75
- {weeks.map((week, w) =>
76
- week.contributionDays.map((day, d) => (
77
- <rect
78
- x={gridX + w * STEP}
79
- y={gridY + d * STEP}
80
- width={CELL_SIZE}
81
- height={CELL_SIZE}
82
- rx="2"
83
- fill={day.color || THEME.cardBg}
84
- />
85
- )),
86
- )}
87
- </>
88
- );
89
-
90
- const height = MONTH_LABEL_HEIGHT + 7 * STEP;
91
- return { svg, height };
92
- }
93
-
94
- void Fragment;
@@ -1,36 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { Fragment, h } from "../jsx-factory.js";
3
- import type { ContributionHighlight } from "../types.js";
4
- import { renderContributionCards } from "./contribution-cards.js";
5
-
6
- void Fragment;
7
-
8
- describe("renderContributionCards", () => {
9
- const highlights: ContributionHighlight[] = [
10
- { project: "org/repo-one", detail: "★ 500 · TypeScript" },
11
- { project: "org/repo-two", detail: "★ 200 · Go" },
12
- ];
13
-
14
- it("returns { svg, height }", () => {
15
- const result = renderContributionCards(highlights, 0);
16
- expect(result).toHaveProperty("svg");
17
- expect(result).toHaveProperty("height");
18
- });
19
-
20
- it("height accounts for cards and gaps", () => {
21
- const result = renderContributionCards(highlights, 0);
22
- // 2 cards of 44px + 1 gap of 8px = 96
23
- expect(result.height).toBe(2 * 44 + 1 * 8);
24
- });
25
-
26
- it("svg contains project names", () => {
27
- const result = renderContributionCards(highlights, 0);
28
- expect(result.svg).toContain("org/repo-one");
29
- expect(result.svg).toContain("org/repo-two");
30
- });
31
-
32
- it("handles empty input", () => {
33
- const result = renderContributionCards([], 0);
34
- expect(result.height).toBe(0);
35
- });
36
- });
@@ -1,58 +0,0 @@
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 { ContributionHighlight, RenderResult } from "../types.js";
5
-
6
- export function renderContributionCards(
7
- highlights: ContributionHighlight[],
8
- y: number,
9
- ): RenderResult {
10
- const { padX } = LAYOUT;
11
- const cardW = 760;
12
- const cardH = 44;
13
- const gap = 8;
14
-
15
- const svg = (
16
- <>
17
- {highlights.map((hl, i) => {
18
- const cy = y + i * (cardH + gap);
19
- const color = BAR_COLORS[i % BAR_COLORS.length];
20
-
21
- return (
22
- <>
23
- <rect
24
- x={padX}
25
- y={cy}
26
- width={cardW}
27
- height={cardH}
28
- rx="6"
29
- fill={THEME.cardBg}
30
- stroke={THEME.border}
31
- stroke-width="1"
32
- />
33
- <rect
34
- x={padX}
35
- y={cy}
36
- width="4"
37
- height={cardH}
38
- rx="2"
39
- fill={color}
40
- />
41
- <text x={padX + 16} y={cy + 18} className="t t-card-title">
42
- {escapeXml(truncate(hl.project, 40))}
43
- </text>
44
- <text x={padX + 16} y={cy + 34} className="t t-card-detail">
45
- {escapeXml(truncate(hl.detail, 80))}
46
- </text>
47
- </>
48
- );
49
- })}
50
- </>
51
- );
52
-
53
- return {
54
- svg,
55
- height:
56
- highlights.length * (cardH + gap) - (highlights.length > 0 ? gap : 0),
57
- };
58
- }
@@ -1,36 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { Fragment, h } from "../jsx-factory.js";
3
- import type { LanguageItem } from "../types.js";
4
- import { renderDonutChart } from "./donut-chart.js";
5
-
6
- void Fragment;
7
-
8
- describe("renderDonutChart", () => {
9
- const items: LanguageItem[] = [
10
- { name: "TypeScript", value: 60000, percent: "60.0", color: "#3178c6" },
11
- { name: "Python", value: 40000, percent: "40.0", color: "#3572A5" },
12
- ];
13
-
14
- it("returns { svg, height }", () => {
15
- const result = renderDonutChart(items, 0);
16
- expect(result).toHaveProperty("svg");
17
- expect(result).toHaveProperty("height");
18
- });
19
-
20
- it("height is at least 180", () => {
21
- const result = renderDonutChart(items, 0);
22
- expect(result.height).toBeGreaterThanOrEqual(180);
23
- });
24
-
25
- it("svg contains language names", () => {
26
- const result = renderDonutChart(items, 0);
27
- expect(result.svg).toContain("TypeScript");
28
- expect(result.svg).toContain("Python");
29
- });
30
-
31
- it("svg contains percentages", () => {
32
- const result = renderDonutChart(items, 0);
33
- expect(result.svg).toContain("60.0%");
34
- expect(result.svg).toContain("40.0%");
35
- });
36
- });
@@ -1,102 +0,0 @@
1
- import { Fragment, h } from "../jsx-factory.js";
2
- import { escapeXml } from "../svg-utils.js";
3
- import { BAR_COLORS, LAYOUT, THEME } from "../theme.js";
4
- import type { LanguageItem, RenderResult } from "../types.js";
5
-
6
- export function renderDonutChart(
7
- items: LanguageItem[],
8
- y: number,
9
- ): RenderResult {
10
- const { padX } = LAYOUT;
11
- const cx = padX + 90;
12
- const cy = y + 90;
13
- const r = 70;
14
- const strokeW = 28;
15
- const circumference = 2 * Math.PI * r;
16
-
17
- let offset = 0;
18
- const segments = items.map((item, i) => {
19
- const pct = parseFloat(item.percent) / 100;
20
- const dash = pct * circumference;
21
- const color = item.color || BAR_COLORS[i % BAR_COLORS.length];
22
- const seg = (
23
- <circle
24
- cx={cx}
25
- cy={cy}
26
- r={r}
27
- fill="none"
28
- stroke={color}
29
- stroke-width={strokeW}
30
- stroke-dasharray={`${dash} ${circumference - dash}`}
31
- stroke-dashoffset={-offset}
32
- transform={`rotate(-90 ${cx} ${cy})`}
33
- opacity="0.85"
34
- />
35
- );
36
- offset += dash;
37
- return seg;
38
- });
39
-
40
- const legendX = padX + 220;
41
- const legendItemH = 24;
42
- const legend = items.map((item, i) => {
43
- const ly = y + 10 + i * legendItemH;
44
- const color = item.color || BAR_COLORS[i % BAR_COLORS.length];
45
- return (
46
- <>
47
- <rect
48
- x={legendX}
49
- y={ly}
50
- width="12"
51
- height="12"
52
- rx="2"
53
- fill={color}
54
- opacity="0.85"
55
- />
56
- <text x={legendX + 20} y={ly + 10} className="t t-label">
57
- {escapeXml(item.name)}
58
- </text>
59
- <text
60
- x={legendX + 200}
61
- y={ly + 10}
62
- className="t t-value"
63
- text-anchor="end"
64
- >
65
- {item.percent}%
66
- </text>
67
- </>
68
- );
69
- });
70
-
71
- const height = Math.max(180, items.length * legendItemH + 20);
72
-
73
- const svg = (
74
- <>
75
- {segments.join("")}
76
- <text
77
- x={cx}
78
- y={cy + 5}
79
- className="t"
80
- fill={THEME.text}
81
- font-size="14"
82
- font-weight="700"
83
- text-anchor="middle"
84
- >
85
- {String(items.length)}
86
- </text>
87
- <text
88
- x={cx}
89
- y={cy + 20}
90
- className="t"
91
- fill={THEME.muted}
92
- font-size="10"
93
- text-anchor="middle"
94
- >
95
- languages
96
- </text>
97
- {legend.join("")}
98
- </>
99
- );
100
-
101
- return { svg, height };
102
- }
@@ -1,46 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { Fragment, h } from "../jsx-factory.js";
3
- import type { ProjectItem } from "../types.js";
4
- import { renderProjectCards } from "./project-cards.js";
5
-
6
- void Fragment;
7
-
8
- describe("renderProjectCards", () => {
9
- const projects: ProjectItem[] = [
10
- {
11
- name: "big-app",
12
- url: "https://github.com/u/big-app",
13
- description: "A large app",
14
- stars: 120,
15
- },
16
- {
17
- name: "small-lib",
18
- url: "https://github.com/u/small-lib",
19
- description: "",
20
- stars: 45,
21
- },
22
- ];
23
-
24
- it("returns { svg, height }", () => {
25
- const result = renderProjectCards(projects, 0);
26
- expect(result).toHaveProperty("svg");
27
- expect(result).toHaveProperty("height");
28
- });
29
-
30
- it("height is positive for non-empty input", () => {
31
- const result = renderProjectCards(projects, 0);
32
- expect(result.height).toBeGreaterThan(0);
33
- });
34
-
35
- it("svg contains project names and star counts", () => {
36
- const result = renderProjectCards(projects, 0);
37
- expect(result.svg).toContain("big-app");
38
- expect(result.svg).toContain("\u2605 120");
39
- expect(result.svg).toContain("small-lib");
40
- });
41
-
42
- it("handles empty input", () => {
43
- const result = renderProjectCards([], 0);
44
- expect(result.height).toBe(0);
45
- });
46
- });
@@ -1,66 +0,0 @@
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 { ProjectItem, RenderResult } from "../types.js";
5
-
6
- export function renderProjectCards(
7
- projects: ProjectItem[],
8
- y: number,
9
- ): RenderResult {
10
- const { padX } = LAYOUT;
11
- const cardW = 760;
12
- const gap = 10;
13
- let svg = "";
14
- let totalHeight = 0;
15
-
16
- for (let i = 0; i < projects.length; i++) {
17
- const cy = y + totalHeight;
18
- const color = BAR_COLORS[i % BAR_COLORS.length];
19
- const p = projects[i];
20
- const desc = truncate(p.description, 90);
21
-
22
- let innerH = 20;
23
- if (desc) innerH += 16;
24
- const cardH = Math.max(innerH + 16, 44);
25
-
26
- // Card background + accent bar
27
- svg += (
28
- <>
29
- <rect
30
- x={padX}
31
- y={cy}
32
- width={cardW}
33
- height={cardH}
34
- rx="6"
35
- fill={THEME.cardBg}
36
- stroke={THEME.border}
37
- stroke-width="1"
38
- />
39
- <rect x={padX} y={cy} width="4" height={cardH} rx="2" fill={color} />
40
- <text x={padX + 16} y={cy + 18} className="t t-card-title">
41
- {escapeXml(truncate(p.name, 40))}
42
- </text>
43
- <text
44
- x={padX + cardW - 16}
45
- y={cy + 18}
46
- className="t t-value"
47
- text-anchor="end"
48
- >
49
- {`\u2605 ${p.stars.toLocaleString()}`}
50
- </text>
51
- </>
52
- );
53
-
54
- if (desc) {
55
- svg += (
56
- <text x={padX + 16} y={cy + 34} className="t t-card-detail">
57
- {escapeXml(desc)}
58
- </text>
59
- );
60
- }
61
-
62
- totalHeight += cardH + gap;
63
- }
64
-
65
- return { svg, height: totalHeight > 0 ? totalHeight - gap : 0 };
66
- }
@@ -1,32 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { Fragment, h } from "../jsx-factory.js";
3
- import type { StatItem } from "../types.js";
4
- import { renderStatCards } from "./stat-cards.js";
5
-
6
- void Fragment;
7
-
8
- describe("renderStatCards", () => {
9
- const stats: StatItem[] = [
10
- { label: "COMMITS", value: "500" },
11
- { label: "PRS", value: "42" },
12
- ];
13
-
14
- it("returns { svg, height }", () => {
15
- const result = renderStatCards(stats, 0);
16
- expect(result).toHaveProperty("svg");
17
- expect(result).toHaveProperty("height");
18
- });
19
-
20
- it("height is card height (72)", () => {
21
- const result = renderStatCards(stats, 0);
22
- expect(result.height).toBe(72);
23
- });
24
-
25
- it("svg contains labels and values", () => {
26
- const result = renderStatCards(stats, 0);
27
- expect(result.svg).toContain("COMMITS");
28
- expect(result.svg).toContain("500");
29
- expect(result.svg).toContain("PRS");
30
- expect(result.svg).toContain("42");
31
- });
32
- });
@@ -1,57 +0,0 @@
1
- import { Fragment, h } from "../jsx-factory.js";
2
- import { escapeXml } from "../svg-utils.js";
3
- import { BAR_COLORS, LAYOUT, THEME } from "../theme.js";
4
- import type { RenderResult, StatItem } from "../types.js";
5
-
6
- export function renderStatCards(stats: StatItem[], y: number): RenderResult {
7
- const { padX } = LAYOUT;
8
- const cardW = 140;
9
- const cardH = 72;
10
- const gap = 15;
11
- const colors = [
12
- BAR_COLORS[0],
13
- BAR_COLORS[1],
14
- BAR_COLORS[2],
15
- BAR_COLORS[4],
16
- BAR_COLORS[5],
17
- ];
18
-
19
- const svg = (
20
- <>
21
- {stats.map((stat, i) => {
22
- const cx = padX + i * (cardW + gap);
23
- const color = colors[i % colors.length];
24
-
25
- return (
26
- <>
27
- <rect
28
- x={cx}
29
- y={y}
30
- width={cardW}
31
- height={cardH}
32
- rx="8"
33
- fill={THEME.cardBg}
34
- stroke={THEME.border}
35
- stroke-width="1"
36
- />
37
- <circle cx={cx + 14} cy={y + 16} r="4" fill={color} />
38
- <text x={cx + 24} y={y + 20} className="t t-stat-label">
39
- {escapeXml(stat.label)}
40
- </text>
41
- <text
42
- x={cx + cardW / 2}
43
- y={y + 52}
44
- fill={color}
45
- className="t t-stat-value"
46
- text-anchor="middle"
47
- >
48
- {escapeXml(String(stat.value))}
49
- </text>
50
- </>
51
- );
52
- })}
53
- </>
54
- );
55
-
56
- return { svg, height: cardH };
57
- }
@@ -1,63 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import type { TechHighlight } from "../types.js";
3
- import { renderTechHighlights } from "./tech-highlights.js";
4
-
5
- describe("renderTechHighlights", () => {
6
- it("renders category names in uppercase", () => {
7
- const highlights: TechHighlight[] = [
8
- { category: "Frontend", items: ["React", "Vue"], score: 85 },
9
- { category: "Backend", items: ["Express", "Django"], score: 70 },
10
- ];
11
- const { svg } = renderTechHighlights(highlights, 0);
12
- expect(svg).toContain("FRONTEND");
13
- expect(svg).toContain("BACKEND");
14
- });
15
-
16
- it("renders skill items joined with middle dot", () => {
17
- const highlights: TechHighlight[] = [
18
- {
19
- category: "Languages",
20
- items: ["TypeScript", "Python", "Go"],
21
- score: 90,
22
- },
23
- ];
24
- const { svg } = renderTechHighlights(highlights, 0);
25
- expect(svg).toContain("TypeScript");
26
- expect(svg).toContain("Python");
27
- expect(svg).toContain("Go");
28
- expect(svg).toContain("\u00B7");
29
- });
30
-
31
- it("renders score percentages", () => {
32
- const highlights: TechHighlight[] = [
33
- { category: "Web Dev", items: ["React", "Astro"], score: 85 },
34
- { category: "DevOps", items: ["Docker"], score: 60 },
35
- ];
36
- const { svg } = renderTechHighlights(highlights, 0);
37
- expect(svg).toContain("85%");
38
- expect(svg).toContain("60%");
39
- });
40
-
41
- it("returns height > 0 for non-empty input", () => {
42
- const highlights: TechHighlight[] = [
43
- { category: "Tools", items: ["Docker", "Kubernetes"], score: 75 },
44
- ];
45
- const { height } = renderTechHighlights(highlights, 0);
46
- expect(height).toBeGreaterThan(0);
47
- });
48
-
49
- it("returns empty result for empty input", () => {
50
- const result = renderTechHighlights([], 0);
51
- expect(result).toEqual({ svg: "", height: 0 });
52
- });
53
-
54
- it("clamps out-of-range scores", () => {
55
- const highlights: TechHighlight[] = [
56
- { category: "Over", items: ["A"], score: 150 },
57
- { category: "Under", items: ["B"], score: -10 },
58
- ];
59
- const { svg } = renderTechHighlights(highlights, 0);
60
- expect(svg).toContain("100%");
61
- expect(svg).toContain("0%");
62
- });
63
- });