@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.
- package/.githooks/.sr-hooks-hash +1 -0
- package/.githooks/commit-msg +3 -0
- package/.githooks/pre-commit +3 -0
- package/AGENTS.md +32 -19
- package/CHANGELOG.md +62 -0
- package/CONTRIBUTING.md +18 -19
- package/README.md +21 -24
- package/action.yml +1 -1
- package/assets/insights/index.svg +45 -4
- package/assets/insights/metrics-constellation.svg +55 -0
- package/assets/insights/metrics-growth.svg +55 -0
- package/assets/insights/metrics-heatmap.svg +55 -0
- package/assets/insights/metrics-impact.svg +55 -0
- package/assets/insights/metrics-rhythm.svg +55 -0
- package/assets/insights/metrics-velocity.svg +55 -0
- package/examples/classic/README.md +36 -2
- package/examples/classic/index.svg +45 -4
- package/examples/classic/metrics-constellation.svg +55 -0
- package/examples/classic/metrics-growth.svg +55 -0
- package/examples/classic/metrics-heatmap.svg +55 -0
- package/examples/classic/metrics-impact.svg +55 -0
- package/examples/classic/metrics-rhythm.svg +55 -0
- package/examples/classic/metrics-velocity.svg +55 -0
- package/examples/ecosystem/README.md +39 -28
- package/examples/ecosystem/index.svg +45 -4
- package/examples/ecosystem/metrics-constellation.svg +55 -0
- package/examples/ecosystem/metrics-growth.svg +55 -0
- package/examples/ecosystem/metrics-heatmap.svg +55 -0
- package/examples/ecosystem/metrics-impact.svg +55 -0
- package/examples/ecosystem/metrics-rhythm.svg +55 -0
- package/examples/ecosystem/metrics-velocity.svg +55 -0
- package/examples/minimal/README.md +36 -2
- package/examples/minimal/index.svg +45 -4
- package/examples/minimal/metrics-constellation.svg +55 -0
- package/examples/minimal/metrics-growth.svg +55 -0
- package/examples/minimal/metrics-heatmap.svg +55 -0
- package/examples/minimal/metrics-impact.svg +55 -0
- package/examples/minimal/metrics-rhythm.svg +55 -0
- package/examples/minimal/metrics-velocity.svg +55 -0
- package/examples/modern/README.md +62 -50
- package/examples/modern/index.svg +45 -4
- package/examples/modern/metrics-constellation.svg +55 -0
- package/examples/modern/metrics-growth.svg +55 -0
- package/examples/modern/metrics-heatmap.svg +55 -0
- package/examples/modern/metrics-impact.svg +55 -0
- package/examples/modern/metrics-rhythm.svg +55 -0
- package/examples/modern/metrics-velocity.svg +55 -0
- package/llms.txt +4 -4
- package/package.json +1 -1
- package/skills/github-insights/SKILL.md +35 -81
- package/sr.yaml +9 -0
- package/src/api.ts +2 -140
- package/src/components/contribution-heatmap.tsx +43 -0
- package/src/components/contribution-rhythm.tsx +152 -0
- package/src/components/full-svg.test.tsx +4 -1
- package/src/components/full-svg.tsx +14 -7
- package/src/components/growth-arc.tsx +119 -0
- package/src/components/impact-trail.tsx +90 -0
- package/src/components/language-velocity.tsx +181 -0
- package/src/components/project-constellation.tsx +97 -0
- package/src/components/section.test.tsx +5 -3
- package/src/components/section.tsx +5 -13
- package/src/components/style-defs.tsx +44 -3
- package/src/index.ts +28 -47
- package/src/metrics.test.ts +50 -57
- package/src/metrics.ts +277 -95
- package/src/readme.test.ts +2 -4
- package/src/templates.test.ts +19 -16
- package/src/templates.ts +30 -16
- package/src/theme.ts +11 -1
- package/src/types.ts +34 -7
- package/assets/insights/metrics-calendar.svg +0 -14
- package/assets/insights/metrics-complexity.svg +0 -14
- package/assets/insights/metrics-contributions.svg +0 -14
- package/assets/insights/metrics-expertise.svg +0 -14
- package/assets/insights/metrics-languages.svg +0 -14
- package/assets/insights/metrics-pulse.svg +0 -14
- package/examples/classic/metrics-calendar.svg +0 -14
- package/examples/classic/metrics-complexity.svg +0 -14
- package/examples/classic/metrics-contributions.svg +0 -14
- package/examples/classic/metrics-expertise.svg +0 -14
- package/examples/classic/metrics-languages.svg +0 -14
- package/examples/classic/metrics-pulse.svg +0 -14
- package/examples/ecosystem/metrics-calendar.svg +0 -14
- package/examples/ecosystem/metrics-complexity.svg +0 -14
- package/examples/ecosystem/metrics-contributions.svg +0 -14
- package/examples/ecosystem/metrics-expertise.svg +0 -14
- package/examples/ecosystem/metrics-languages.svg +0 -14
- package/examples/ecosystem/metrics-pulse.svg +0 -14
- package/examples/minimal/metrics-calendar.svg +0 -14
- package/examples/minimal/metrics-complexity.svg +0 -14
- package/examples/minimal/metrics-contributions.svg +0 -14
- package/examples/minimal/metrics-expertise.svg +0 -14
- package/examples/minimal/metrics-languages.svg +0 -14
- package/examples/minimal/metrics-pulse.svg +0 -14
- package/examples/modern/metrics-calendar.svg +0 -14
- package/examples/modern/metrics-complexity.svg +0 -14
- package/examples/modern/metrics-contributions.svg +0 -14
- package/examples/modern/metrics-expertise.svg +0 -14
- package/examples/modern/metrics-languages.svg +0 -14
- package/examples/modern/metrics-pulse.svg +0 -14
- package/src/components/bar-chart.test.tsx +0 -38
- package/src/components/bar-chart.tsx +0 -54
- package/src/components/contribution-calendar.test.tsx +0 -44
- package/src/components/contribution-calendar.tsx +0 -94
- package/src/components/contribution-cards.test.tsx +0 -36
- package/src/components/contribution-cards.tsx +0 -58
- package/src/components/donut-chart.test.tsx +0 -36
- package/src/components/donut-chart.tsx +0 -102
- package/src/components/project-cards.test.tsx +0 -46
- package/src/components/project-cards.tsx +0 -66
- package/src/components/stat-cards.test.tsx +0 -32
- package/src/components/stat-cards.tsx +0 -57
- package/src/components/tech-highlights.test.tsx +0 -63
- package/src/components/tech-highlights.tsx +0 -109
- 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 }
|