@urmzd/github-insights 2.1.0 → 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 +45 -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 +2 -140
  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 +28 -47
  52. package/src/metrics.test.ts +50 -57
  53. package/src/metrics.ts +277 -95
  54. package/src/readme.test.ts +2 -4
  55. package/src/templates.test.ts +19 -16
  56. package/src/templates.ts +30 -16
  57. package/src/theme.ts +11 -1
  58. package/src/types.ts +28 -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,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
- });
@@ -1,109 +0,0 @@
1
- import { Fragment, h } from "../jsx-factory.js";
2
- import { escapeXml, truncate, wrapText } from "../svg-utils.js";
3
- import { BAR_COLORS, LAYOUT } from "../theme.js";
4
- import type { RenderResult, TechHighlight } from "../types.js";
5
-
6
- export function renderTechHighlights(
7
- highlights: TechHighlight[],
8
- y: number,
9
- ): RenderResult {
10
- if (highlights.length === 0) return { svg: "", height: 0 };
11
-
12
- const { padX, barHeight, barMaxWidth } = LAYOUT;
13
- const labelMaxChars = 60;
14
- const skillMaxChars = 120;
15
- const skillLineHeight = 16;
16
- const labelLineHeight = 26;
17
- const scoreX = padX + barMaxWidth + 10;
18
- const skillY = 16;
19
- const rowGap = 14;
20
-
21
- let svg = "";
22
- let height = 0;
23
-
24
- for (let hi = 0; hi < highlights.length; hi++) {
25
- const group = highlights[hi];
26
- const color = BAR_COLORS[hi % BAR_COLORS.length];
27
- const score = Math.max(0, Math.min(100, group.score));
28
- const fillWidth = (score / 100) * barMaxWidth;
29
-
30
- const baseY = y + height;
31
-
32
- // Category label (uppercase, left-aligned, on its own line)
33
- svg += (
34
- <text x={padX} y={baseY + 14} className="t t-subhdr">
35
- {escapeXml(truncate(group.category.toUpperCase(), labelMaxChars))}
36
- </text>
37
- );
38
-
39
- // Bar track (full width, low opacity)
40
- svg += (
41
- <rect
42
- x={padX}
43
- y={baseY + labelLineHeight}
44
- width={barMaxWidth}
45
- height={barHeight}
46
- rx={4}
47
- fill={color}
48
- fill-opacity="0.15"
49
- />
50
- );
51
-
52
- // Bar fill (proportional to score)
53
- if (fillWidth > 0) {
54
- svg += (
55
- <rect
56
- x={padX}
57
- y={baseY + labelLineHeight}
58
- width={fillWidth}
59
- height={barHeight}
60
- rx={4}
61
- fill={color}
62
- fill-opacity="0.85"
63
- />
64
- );
65
- }
66
-
67
- // Score label (right of bar)
68
- svg += (
69
- <text
70
- x={scoreX}
71
- y={baseY + labelLineHeight + barHeight / 2 + 4}
72
- className="t t-value"
73
- >
74
- {`${score}%`}
75
- </text>
76
- );
77
-
78
- // Skill items text (below bar, muted, wrapped to avoid overflow)
79
- const skillText = group.items
80
- .map((item) => truncate(item, 30))
81
- .join(" \u00B7 ");
82
-
83
- const skillLines = wrapText(skillText, skillMaxChars);
84
- for (let li = 0; li < skillLines.length; li++) {
85
- svg += (
86
- <text
87
- x={padX}
88
- y={
89
- baseY + labelLineHeight + barHeight + skillY + li * skillLineHeight
90
- }
91
- className="t t-card-detail"
92
- >
93
- {escapeXml(skillLines[li])}
94
- </text>
95
- );
96
- }
97
-
98
- height +=
99
- labelLineHeight +
100
- barHeight +
101
- skillY +
102
- (skillLines.length - 1) * skillLineHeight +
103
- rowGap;
104
- }
105
-
106
- return { svg, height };
107
- }
108
-
109
- void Fragment;
package/teasr.toml DELETED
@@ -1,14 +0,0 @@
1
- [server]
2
- command = "npx serve metrics --listen 3123"
3
- url = "http://localhost:3123"
4
- timeout = 10000
5
-
6
- [output]
7
- dir = "./showcase"
8
- formats = ["png"]
9
-
10
- [[scenes]]
11
- type = "web"
12
- url = "/index.svg"
13
- name = "metrics-svg"
14
- viewport = { width = 1200, height = 900 }