@shiinasaku/github-card 4.1.0 → 4.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.
- package/README.md +155 -98
- package/dist/lib.d.ts +1 -1
- package/dist/lib.js +258 -120
- package/dist/utils/font.d.ts +3 -1
- package/dist/utils/format.d.ts +7 -0
- package/dist/utils/icons.d.ts +3 -1
- package/dist/utils/index.d.ts +1 -1
- package/package.json +8 -9
- package/src/card.ts +307 -404
- package/src/github.ts +107 -49
- package/src/index.ts +7 -6
- package/src/lib.ts +1 -1
- package/src/utils/font.ts +4 -2
- package/src/utils/format.ts +41 -3
- package/src/utils/github-client.ts +13 -8
- package/src/utils/icons.ts +26 -9
- package/src/utils/index.ts +1 -1
- package/fonts/Roboto-subset.woff2 +0 -0
package/dist/lib.js
CHANGED
|
@@ -167,12 +167,49 @@ function resolveColors(opts) {
|
|
|
167
167
|
}
|
|
168
168
|
// src/utils/format.ts
|
|
169
169
|
function kFormat(num) {
|
|
170
|
+
const sign = num < 0 ? "-" : "";
|
|
171
|
+
const abs = Math.abs(num);
|
|
170
172
|
const trim = (v) => v.toFixed(1).replace(/\.0$/, "");
|
|
171
|
-
if (
|
|
172
|
-
return trim(
|
|
173
|
-
if (
|
|
174
|
-
return trim(
|
|
175
|
-
|
|
173
|
+
if (abs >= 999950000)
|
|
174
|
+
return sign + trim(abs / 1e9) + "B";
|
|
175
|
+
if (abs >= 999950)
|
|
176
|
+
return sign + trim(abs / 1e6) + "M";
|
|
177
|
+
if (abs >= 999.95)
|
|
178
|
+
return sign + trim(abs / 1000) + "k";
|
|
179
|
+
return sign + String(Math.round(abs));
|
|
180
|
+
}
|
|
181
|
+
function measureText(text, size, weight = 400) {
|
|
182
|
+
if (!text)
|
|
183
|
+
return 0;
|
|
184
|
+
const factor = size / 16;
|
|
185
|
+
const weightFactor = weight >= 700 ? 1.08 : weight >= 600 ? 1.04 : 1;
|
|
186
|
+
let width = 0;
|
|
187
|
+
for (const ch of text) {
|
|
188
|
+
const code = ch.codePointAt(0);
|
|
189
|
+
let w;
|
|
190
|
+
if (ch === " ")
|
|
191
|
+
w = 0.28;
|
|
192
|
+
else if (ch === "." || ch === "," || ch === ":" || ch === ";")
|
|
193
|
+
w = 0.3;
|
|
194
|
+
else if (ch === "i" || ch === "l" || ch === "I" || ch === "t" || ch === "j" || ch === "|")
|
|
195
|
+
w = 0.3;
|
|
196
|
+
else if (ch === "m" || ch === "w" || ch === "W" || ch === "M" || ch === "@")
|
|
197
|
+
w = 0.95;
|
|
198
|
+
else if (code >= 4352 && code <= 4607)
|
|
199
|
+
w = 1;
|
|
200
|
+
else if (code >= 19968 && code <= 40959)
|
|
201
|
+
w = 1;
|
|
202
|
+
else if (code >= 12352 && code <= 12543)
|
|
203
|
+
w = 1;
|
|
204
|
+
else if (code >= 44032 && code <= 55215)
|
|
205
|
+
w = 1;
|
|
206
|
+
else if (code > 127)
|
|
207
|
+
w = 0.72;
|
|
208
|
+
else
|
|
209
|
+
w = 0.58;
|
|
210
|
+
width += w;
|
|
211
|
+
}
|
|
212
|
+
return width * factor * weightFactor;
|
|
176
213
|
}
|
|
177
214
|
function escapeXml(str) {
|
|
178
215
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
@@ -860,24 +897,33 @@ function getLangColor(lang) {
|
|
|
860
897
|
}
|
|
861
898
|
// src/utils/icons.ts
|
|
862
899
|
var icons = {
|
|
863
|
-
star: `<path
|
|
900
|
+
star: `<path d="M10.92 2.868a1.25 1.25 0 0 1 2.16 0l2.795 4.798l5.428 1.176a1.25 1.25 0 0 1 .667 2.054l-3.7 4.141l.56 5.525a1.25 1.25 0 0 1-1.748 1.27L12 19.592l-5.082 2.24a1.25 1.25 0 0 1-1.748-1.27l.56-5.525l-3.7-4.14a1.25 1.25 0 0 1 .667-2.055l5.428-1.176z"/>`,
|
|
864
901
|
commit: `<path fill-rule="evenodd" d="M10.5 7.75a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0zm1.43.75a4.002 4.002 0 01-7.86 0H.75a.75.75 0 110-1.5h3.32a4.001 4.001 0 017.86 0h3.32a.75.75 0 110 1.5h-3.32z"/>`,
|
|
865
|
-
pr: `<path
|
|
866
|
-
issue: `<path
|
|
867
|
-
repo: `<path
|
|
902
|
+
pr: `<path d="M4 18a2 2 0 1 0 4 0a2 2 0 1 0-4 0M4 6a2 2 0 1 0 4 0a2 2 0 1 0-4 0m12 12a2 2 0 1 0 4 0a2 2 0 1 0-4 0M6 8v8"/><path d="M11 6h5a2 2 0 0 1 2 2v8"/><path d="m14 9l-3-3l3-3"/>`,
|
|
903
|
+
issue: `<path d="M15.314 2a2 2 0 0 1 1.414.586l4.686 4.686A2 2 0 0 1 22 8.686v6.628a2 2 0 0 1-.586 1.414l-4.686 4.686a2 2 0 0 1-1.414.586H8.686a2 2 0 0 1-1.414-.586l-4.686-4.686A2 2 0 0 1 2 15.314V8.686a2 2 0 0 1 .586-1.414l4.686-4.686A2 2 0 0 1 8.686 2zm0 2H8.686L4 8.686v6.628L8.686 20h6.628L20 15.314V8.686zM12 15a1 1 0 1 1 0 2a1 1 0 0 1 0-2m0-9a1 1 0 0 1 1 1v6a1 1 0 1 1-2 0V7a1 1 0 0 1 1-1"/>`,
|
|
904
|
+
repo: `<path d="M6 19.623c-.69-.195-1.232-.491-1.682-.941C3 17.364 3 15.242 3 11s0-6.364 1.318-7.682S7.758 2 12 2s6.364 0 7.682 1.318S21 6.758 21 11s0 6.364-1.318 7.682c-.45.45-.993.746-1.682.941"/><path d="M12 20.193c-.414 0-.748.32-1.414.958c-.647.619-.97.929-1.242.831a.5.5 0 0 1-.096-.046C9 21.779 9 21.31 9 20.376v-3.125c0-1.532 0-2.299.44-2.775C9.878 14 10.585 14 12 14s2.121 0 2.56.476s.44 1.243.44 2.775v3.125c0 .935 0 1.403-.248 1.56a.5.5 0 0 1-.096.046c-.272.098-.595-.212-1.242-.831c-.666-.639-1-.958-1.414-.958M8 10h8M8 6h4"/>`,
|
|
868
905
|
x: `<path d="M4 4l11.733 16h4.267l-11.733 -16l-4.267 0" /><path d="M4 20l6.768 -6.768m2.46 -2.46l6.772 -6.772" />`
|
|
869
906
|
};
|
|
907
|
+
var iconMeta = {
|
|
908
|
+
star: { viewBox: "0 0 24 24", kind: "fill" },
|
|
909
|
+
commit: { viewBox: "0 0 16 16", kind: "fill" },
|
|
910
|
+
pr: { viewBox: "0 0 24 24", kind: "stroke", strokeWidth: "2" },
|
|
911
|
+
issue: { viewBox: "0 0 24 24", kind: "fill" },
|
|
912
|
+
repo: { viewBox: "0 0 24 24", kind: "stroke", strokeWidth: "1.5" },
|
|
913
|
+
x: { viewBox: "0 0 24 24", kind: "stroke", strokeWidth: "2" }
|
|
914
|
+
};
|
|
870
915
|
function icon(name, color, size = 16) {
|
|
871
916
|
const path = icons[name];
|
|
872
917
|
if (!path)
|
|
873
918
|
return "";
|
|
874
|
-
const
|
|
919
|
+
const meta = iconMeta[name];
|
|
920
|
+
const viewBox = meta?.viewBox ?? "0 0 16 16";
|
|
875
921
|
const colorStr = color.startsWith("#") ? color : `#${color}`;
|
|
876
|
-
const
|
|
877
|
-
return `<svg width="${size}" height="${size}" viewBox="${viewBox}" ${
|
|
922
|
+
const attrs = meta?.kind === "stroke" ? `fill="none" stroke="${colorStr}" stroke-width="${meta.strokeWidth ?? "1.8"}" stroke-linecap="round" stroke-linejoin="round"` : `fill="${colorStr}"`;
|
|
923
|
+
return `<svg width="${size}" height="${size}" viewBox="${viewBox}" ${attrs} role="img">${path}</svg>`;
|
|
878
924
|
}
|
|
879
925
|
// src/utils/font.ts
|
|
880
|
-
var FONT_FAMILY = 'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"';
|
|
926
|
+
var FONT_FAMILY = 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"';
|
|
881
927
|
var FONT_FACE = "";
|
|
882
928
|
// src/utils/colors.ts
|
|
883
929
|
var twColors = {
|
|
@@ -970,6 +1016,17 @@ var STAT_DEFS = [
|
|
|
970
1016
|
{ key: "repos", iconName: "repo", label: "Repos" },
|
|
971
1017
|
{ key: "prs", iconName: "pr", label: "PRs" }
|
|
972
1018
|
];
|
|
1019
|
+
function el(tag, attrs, children = "") {
|
|
1020
|
+
let a = "";
|
|
1021
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
1022
|
+
if (v === undefined || v === null || v === "")
|
|
1023
|
+
continue;
|
|
1024
|
+
a += ` ${k}="${escapeXml(String(v))}"`;
|
|
1025
|
+
}
|
|
1026
|
+
if (!children)
|
|
1027
|
+
return `<${tag}${a} />`;
|
|
1028
|
+
return `<${tag}${a}>${children}</${tag}>`;
|
|
1029
|
+
}
|
|
973
1030
|
function raw(token) {
|
|
974
1031
|
const m = resolveTw(token, "fill").match(/fill="([^"]+)"/);
|
|
975
1032
|
return m?.[1] ?? token;
|
|
@@ -1007,7 +1064,7 @@ function renderCard(user, stats, langs, opts = {}) {
|
|
|
1007
1064
|
const textColor = raw(c.text);
|
|
1008
1065
|
const iconColor = raw(c.icon);
|
|
1009
1066
|
const borderColor = raw(c.border);
|
|
1010
|
-
const fontFamily = FONT_FAMILY
|
|
1067
|
+
const fontFamily = FONT_FAMILY;
|
|
1011
1068
|
const displayName = user.name || user.login;
|
|
1012
1069
|
const nameEsc = escapeXml(clamp(displayName, 28));
|
|
1013
1070
|
const loginEsc = escapeXml(clamp(user.login, 24));
|
|
@@ -1015,7 +1072,8 @@ function renderCard(user, stats, langs, opts = {}) {
|
|
|
1015
1072
|
const bioRaw = !compact && user.bio ? user.bio : "";
|
|
1016
1073
|
const bioLines = bioRaw ? wrapText(bioRaw, 44, 2) : [];
|
|
1017
1074
|
const twitter = !compact && user.twitter ? escapeXml(clamp(user.twitter, 22)) : "";
|
|
1018
|
-
const avatar =
|
|
1075
|
+
const avatar = user.avatarUrl || "";
|
|
1076
|
+
const metaStr = pronouns ? `@${loginEsc} · ${pronouns}` : `@${loginEsc}`;
|
|
1019
1077
|
const visible = STAT_DEFS.filter((d) => !hidden.has(d.key));
|
|
1020
1078
|
const activeLangsList = langs.filter((l) => {
|
|
1021
1079
|
const name = l.name.toLowerCase().trim();
|
|
@@ -1025,42 +1083,45 @@ function renderCard(user, stats, langs, opts = {}) {
|
|
|
1025
1083
|
return false;
|
|
1026
1084
|
return true;
|
|
1027
1085
|
});
|
|
1028
|
-
const PX =
|
|
1029
|
-
const PY =
|
|
1030
|
-
const avatarSize =
|
|
1086
|
+
const PX = 24;
|
|
1087
|
+
const PY = 22;
|
|
1088
|
+
const avatarSize = 60;
|
|
1031
1089
|
const avatarR = avatarSize / 2;
|
|
1032
|
-
const
|
|
1033
|
-
const
|
|
1034
|
-
const
|
|
1035
|
-
const
|
|
1036
|
-
const
|
|
1090
|
+
const textGap = 16;
|
|
1091
|
+
const nameW = measureText(displayName, 17, 700);
|
|
1092
|
+
const metaW = measureText(metaStr, 12.5, 500);
|
|
1093
|
+
const bioW = bioLines.length ? Math.max(...bioLines.map((l) => measureText(l, 11.5, 400))) : 0;
|
|
1094
|
+
const twitterW = twitter ? measureText(`@${twitter}`, 11.5, 500) : 0;
|
|
1095
|
+
const profileTextW = Math.max(nameW, metaW, bioW, twitterW);
|
|
1096
|
+
const profileW = PX + avatarSize + textGap + profileTextW + PX;
|
|
1097
|
+
const statsW = visible.length > 0 ? visible.length * 84 + PX * 2 : 0;
|
|
1037
1098
|
const langsW = activeLangsList.length > 0 ? 300 : 0;
|
|
1038
1099
|
const W = Math.round(Math.min(540, Math.max(340, profileW, statsW, langsW)));
|
|
1039
1100
|
const contentW = W - PX * 2;
|
|
1040
|
-
const infoX = PX + avatarSize +
|
|
1041
|
-
const nameY = PY +
|
|
1042
|
-
const loginY = nameY +
|
|
1043
|
-
const bioStartY = loginY +
|
|
1101
|
+
const infoX = PX + avatarSize + textGap;
|
|
1102
|
+
const nameY = PY + 20;
|
|
1103
|
+
const loginY = nameY + 17;
|
|
1104
|
+
const bioStartY = loginY + 15;
|
|
1044
1105
|
const twitterY = bioStartY + bioLines.length * 14 + (bioLines.length ? 4 : 0);
|
|
1045
|
-
let profileH = loginY +
|
|
1106
|
+
let profileH = loginY + 8 - PY;
|
|
1046
1107
|
if (bioLines.length)
|
|
1047
1108
|
profileH = bioStartY + bioLines.length * 14 - PY;
|
|
1048
1109
|
if (twitter)
|
|
1049
1110
|
profileH = twitterY + 10 - PY;
|
|
1050
|
-
profileH = Math.max(profileH, avatarSize +
|
|
1051
|
-
const statsY = PY + profileH +
|
|
1111
|
+
profileH = Math.max(profileH, avatarSize + 6);
|
|
1112
|
+
const statsY = PY + profileH + 16;
|
|
1052
1113
|
const statW = visible.length > 0 ? contentW / visible.length : 0;
|
|
1053
|
-
const statsH = visible.length > 0 ?
|
|
1114
|
+
const statsH = visible.length > 0 ? 58 : 0;
|
|
1054
1115
|
const totalSize = activeLangsList.reduce((s, l) => s + l.size, 0) || 1;
|
|
1055
1116
|
const sorted = [...activeLangsList].sort((a, b) => b.size - a.size);
|
|
1056
1117
|
const visualLangs = buildLanguageSegments(sorted, totalSize, contentW);
|
|
1057
|
-
const barY = statsY + statsH + (statsH ?
|
|
1118
|
+
const barY = statsY + statsH + (statsH ? 18 : 14);
|
|
1058
1119
|
const barH = 10;
|
|
1059
1120
|
const maxLegend = compact ? 0 : 6;
|
|
1060
1121
|
const legendLangs = visualLangs.slice(0, maxLegend);
|
|
1061
1122
|
const otherSize = visualLangs.slice(maxLegend).reduce((s, l) => s + l.size, 0);
|
|
1062
|
-
const legendY = barY + barH +
|
|
1063
|
-
const legendRowH =
|
|
1123
|
+
const legendY = barY + barH + 14;
|
|
1124
|
+
const legendRowH = 17;
|
|
1064
1125
|
const legendCols = 3;
|
|
1065
1126
|
const legendColW = contentW / legendCols;
|
|
1066
1127
|
const legendItems = legendLangs.map((l) => ({
|
|
@@ -1082,128 +1143,205 @@ function renderCard(user, stats, langs, opts = {}) {
|
|
|
1082
1143
|
}
|
|
1083
1144
|
const legendRows = compact ? 0 : Math.ceil(legendItems.length / legendCols);
|
|
1084
1145
|
const hasLangs = activeLangsList.length > 0;
|
|
1085
|
-
const langSectionH = hasLangs ? barH +
|
|
1086
|
-
const H = barY + (hasLangs ? langSectionH : 0) +
|
|
1146
|
+
const langSectionH = hasLangs ? barH + 14 + legendRows * legendRowH + 10 : 0;
|
|
1147
|
+
const H = barY + (hasLangs ? langSectionH : 0) + 14 + (hasLangs ? 0 : 6);
|
|
1087
1148
|
const parts = [];
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1149
|
+
const svgAttrs = {
|
|
1150
|
+
width: W,
|
|
1151
|
+
height: H,
|
|
1152
|
+
viewBox: `0 0 ${W} ${H}`,
|
|
1153
|
+
fill: "none",
|
|
1154
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
1155
|
+
role: "img",
|
|
1156
|
+
"aria-labelledby": "cardTitle cardDesc"
|
|
1157
|
+
};
|
|
1158
|
+
let svgOpen = "<svg";
|
|
1159
|
+
for (const [k, v] of Object.entries(svgAttrs))
|
|
1160
|
+
svgOpen += ` ${k}="${escapeXml(String(v))}"`;
|
|
1161
|
+
parts.push(svgOpen + ">");
|
|
1162
|
+
parts.push(el("title", { id: "cardTitle" }, `${escapeXml(displayName)}'s GitHub Stats`));
|
|
1163
|
+
parts.push(el("desc", { id: "cardDesc" }, `GitHub profile card for ${escapeXml(displayName)} with ${kFormat(stats.stars)} stars, ${kFormat(stats.commits)} commits, and ${activeLangsList.length} highlighted languages.`));
|
|
1164
|
+
const css = [];
|
|
1165
|
+
css.push(`* { font-family: ${fontFamily}; text-rendering: geometricPrecision; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }`);
|
|
1166
|
+
css.push(`.header { font-size: 17px; font-weight: 700; fill: ${titleColor}; letter-spacing: -0.015em; }`);
|
|
1167
|
+
css.push(`.stat { font-size: 14px; font-weight: 600; fill: ${textColor}; }`);
|
|
1168
|
+
css.push(`.icon { fill: ${iconColor}; }`);
|
|
1169
|
+
css.push(`.lang-name { font-size: 11px; fill: ${textColor}; opacity: 0.82; }`);
|
|
1170
|
+
css.push(`.lang-progress { fill: ${textColor}; opacity: 0.07; }`);
|
|
1171
|
+
css.push(`.meta { font-size: 12.5px; font-weight: 500; fill: ${textColor}; opacity: 0.72; }`);
|
|
1172
|
+
css.push(`.bio { font-size: 11.5px; font-weight: 400; fill: ${textColor}; opacity: 0.68; }`);
|
|
1173
|
+
css.push(`.stat-label { font-size: 10px; font-weight: 700; fill: ${textColor}; opacity: 0.68; letter-spacing: 0.06em; }`);
|
|
1174
|
+
css.push(`.stat-value { font-size: 16px; font-weight: 700; fill: ${textColor}; letter-spacing: -0.015em; font-variant-numeric: tabular-nums; font-feature-settings: "tnum"; }`);
|
|
1175
|
+
css.push(`.twitter-text { font-size: 11.5px; font-weight: 500; fill: ${textColor}; opacity: 0.65; }`);
|
|
1104
1176
|
if (animate) {
|
|
1105
|
-
|
|
1106
|
-
|
|
1177
|
+
css.push("@keyframes fadeInAnimation { from { opacity: 0; } to { opacity: 1; } }");
|
|
1178
|
+
css.push(".stagger { opacity: 0; animation: fadeInAnimation 0.3s ease-in-out forwards; }");
|
|
1107
1179
|
}
|
|
1108
|
-
parts.push("
|
|
1109
|
-
parts.push("
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
parts.push(
|
|
1118
|
-
parts.push(
|
|
1119
|
-
parts.push(
|
|
1120
|
-
parts.push(
|
|
1180
|
+
parts.push(el("style", {}, css.join(" ")));
|
|
1181
|
+
parts.push(el("defs", {}, [
|
|
1182
|
+
el("clipPath", { id: "av" }, el("circle", { cx: PX + avatarR, cy: PY + avatarR, r: avatarR })),
|
|
1183
|
+
el("radialGradient", { id: "gl", cx: "12%", cy: "15%", r: "65%" }, el("stop", { offset: "0%", "stop-color": iconColor, "stop-opacity": "0.08" }) + el("stop", { offset: "100%", "stop-color": iconColor, "stop-opacity": "0" })),
|
|
1184
|
+
el("radialGradient", { id: "gl2", cx: "92%", cy: "95%", r: "55%" }, el("stop", { offset: "0%", "stop-color": titleColor, "stop-opacity": "0.035" }) + el("stop", { offset: "100%", "stop-color": titleColor, "stop-opacity": "0" })),
|
|
1185
|
+
el("linearGradient", { id: "ring", x1: "0", y1: "0", x2: "1", y2: "1" }, el("stop", { offset: "0%", "stop-color": titleColor }) + el("stop", { offset: "100%", "stop-color": iconColor })),
|
|
1186
|
+
el("linearGradient", { id: "card-bg", x1: "0", y1: "0", x2: W, y2: H, gradientUnits: "userSpaceOnUse" }, el("stop", { offset: "0%", "stop-color": bgColor }) + el("stop", { offset: "82%", "stop-color": bgColor }) + el("stop", { offset: "100%", "stop-color": titleColor, "stop-opacity": "0.04" })),
|
|
1187
|
+
el("linearGradient", { id: "rim", x1: "0", y1: "0", x2: W, y2: "0", gradientUnits: "userSpaceOnUse" }, el("stop", { offset: "0%", "stop-color": iconColor, "stop-opacity": "0" }) + el("stop", { offset: "45%", "stop-color": iconColor, "stop-opacity": "0.28" }) + el("stop", { offset: "100%", "stop-color": iconColor, "stop-opacity": "0" }))
|
|
1188
|
+
].join("")));
|
|
1189
|
+
parts.push(el("rect", { x: "0.5", y: "0.5", width: W - 1, height: H - 1, rx: 12, fill: "url(#card-bg)" }));
|
|
1190
|
+
parts.push(el("rect", { x: "0.5", y: "0.5", width: W - 1, height: H - 1, rx: 12, fill: "url(#gl)" }));
|
|
1191
|
+
parts.push(el("rect", { x: "0.5", y: "0.5", width: W - 1, height: H - 1, rx: 12, fill: "url(#gl2)" }));
|
|
1192
|
+
parts.push(el("line", {
|
|
1193
|
+
x1: PX,
|
|
1194
|
+
y1: "1.5",
|
|
1195
|
+
x2: W - PX,
|
|
1196
|
+
y2: "1.5",
|
|
1197
|
+
stroke: "url(#rim)",
|
|
1198
|
+
"stroke-width": 1
|
|
1199
|
+
}));
|
|
1121
1200
|
if (!hideBorder) {
|
|
1122
|
-
parts.push(
|
|
1201
|
+
parts.push(el("rect", {
|
|
1202
|
+
x: "0.5",
|
|
1203
|
+
y: "0.5",
|
|
1204
|
+
width: W - 1,
|
|
1205
|
+
height: H - 1,
|
|
1206
|
+
rx: 12,
|
|
1207
|
+
fill: "none",
|
|
1208
|
+
stroke: borderColor,
|
|
1209
|
+
"stroke-opacity": "0.5"
|
|
1210
|
+
}));
|
|
1123
1211
|
}
|
|
1124
|
-
parts.push(
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1212
|
+
parts.push(el("g", {}, el("circle", {
|
|
1213
|
+
cx: PX + avatarR + 1,
|
|
1214
|
+
cy: PY + avatarR + 1,
|
|
1215
|
+
r: avatarR + 3,
|
|
1216
|
+
fill: titleColor,
|
|
1217
|
+
opacity: "0.08"
|
|
1218
|
+
}) + el("image", {
|
|
1219
|
+
href: avatar,
|
|
1220
|
+
x: PX,
|
|
1221
|
+
y: PY,
|
|
1222
|
+
width: avatarSize,
|
|
1223
|
+
height: avatarSize,
|
|
1224
|
+
"clip-path": "url(#av)",
|
|
1225
|
+
preserveAspectRatio: "xMidYMid slice"
|
|
1226
|
+
}) + el("circle", {
|
|
1227
|
+
cx: PX + avatarR,
|
|
1228
|
+
cy: PY + avatarR,
|
|
1229
|
+
r: avatarR + 1.5,
|
|
1230
|
+
fill: "none",
|
|
1231
|
+
stroke: "url(#ring)",
|
|
1232
|
+
"stroke-opacity": "0.55",
|
|
1233
|
+
"stroke-width": "1.5"
|
|
1234
|
+
})));
|
|
1235
|
+
const textParts = [];
|
|
1236
|
+
textParts.push(el("text", { x: infoX, y: nameY, class: "header" }, nameEsc));
|
|
1237
|
+
textParts.push(el("text", { x: infoX, y: loginY, class: "meta" }, metaStr));
|
|
1129
1238
|
for (let i = 0;i < bioLines.length; i++) {
|
|
1130
|
-
|
|
1239
|
+
textParts.push(el("text", { x: infoX, y: bioStartY + i * 14, class: "bio" }, escapeXml(bioLines[i].trim())));
|
|
1131
1240
|
}
|
|
1132
1241
|
if (twitter) {
|
|
1133
|
-
|
|
1242
|
+
textParts.push(el("g", { transform: `translate(${infoX},${twitterY - 9})` }, icon("x", textColor, 11) + el("text", { x: 15, y: 9, class: "twitter-text" }, `@${twitter}`)));
|
|
1134
1243
|
}
|
|
1135
|
-
parts.push("
|
|
1244
|
+
parts.push(el("g", {}, textParts.join("")));
|
|
1136
1245
|
if (visible.length > 0) {
|
|
1137
|
-
|
|
1138
|
-
parts.push('<g transform="translate(0,0)">');
|
|
1246
|
+
const statParts = [];
|
|
1139
1247
|
for (let i = 0;i < visible.length; i++) {
|
|
1140
1248
|
const d = visible[i];
|
|
1141
1249
|
const val = stats[d.key];
|
|
1142
1250
|
const cx = PX + statW * i + statW / 2;
|
|
1143
|
-
const
|
|
1144
|
-
const iy = statsY + 2;
|
|
1145
|
-
const staggerStyle = animate ? ' style="animation-delay: ' + (i + 3) * 150 + 'ms"' : "";
|
|
1251
|
+
const iy = statsY + 4;
|
|
1146
1252
|
const staggerClass = animate ? " stagger" : "";
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
const
|
|
1151
|
-
const
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1253
|
+
const iconSize = 15;
|
|
1254
|
+
const iconY = 2;
|
|
1255
|
+
const valueY = 37;
|
|
1256
|
+
const labelY = 52;
|
|
1257
|
+
const cellParts = [];
|
|
1258
|
+
cellParts.push(el("g", { transform: `translate(${-iconSize / 2},${iconY})`, class: "stat-icon" }, icon(d.iconName, iconColor, iconSize)));
|
|
1259
|
+
cellParts.push(el("text", { x: 0, y: valueY, class: "stat-value", "text-anchor": "middle" }, kFormat(val)));
|
|
1260
|
+
cellParts.push(el("text", { x: 0, y: labelY, class: "stat-label", "text-anchor": "middle" }, d.label.toUpperCase()));
|
|
1261
|
+
statParts.push(el("g", {
|
|
1262
|
+
transform: `translate(${cx},${iy})`,
|
|
1263
|
+
class: `stat${staggerClass}`,
|
|
1264
|
+
style: animate ? `animation-delay: ${(i + 3) * 150}ms` : undefined
|
|
1265
|
+
}, cellParts.join("")));
|
|
1266
|
+
if (i < visible.length - 1) {
|
|
1267
|
+
const dx = PX + statW * (i + 1);
|
|
1268
|
+
statParts.push(el("line", {
|
|
1269
|
+
x1: dx,
|
|
1270
|
+
y1: statsY + 4,
|
|
1271
|
+
x2: dx,
|
|
1272
|
+
y2: statsY + statsH - 6,
|
|
1273
|
+
stroke: textColor,
|
|
1274
|
+
"stroke-opacity": "0.12"
|
|
1275
|
+
}));
|
|
1276
|
+
}
|
|
1156
1277
|
}
|
|
1157
|
-
parts.push("
|
|
1278
|
+
parts.push(el("g", {}, statParts.join("")));
|
|
1158
1279
|
}
|
|
1159
1280
|
if (hasLangs) {
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1281
|
+
const langParts = [];
|
|
1282
|
+
langParts.push(el("line", {
|
|
1283
|
+
x1: PX,
|
|
1284
|
+
y1: barY - 8,
|
|
1285
|
+
x2: W - PX,
|
|
1286
|
+
y2: barY - 8,
|
|
1287
|
+
stroke: textColor,
|
|
1288
|
+
"stroke-opacity": "0.08"
|
|
1289
|
+
}));
|
|
1290
|
+
langParts.push(el("clipPath", { id: "lang-clip" }, el("rect", {
|
|
1291
|
+
x: PX,
|
|
1292
|
+
y: barY,
|
|
1293
|
+
width: animate ? "0" : contentW,
|
|
1294
|
+
height: barH,
|
|
1295
|
+
rx: 5
|
|
1296
|
+
}, animate ? el("animate", {
|
|
1297
|
+
attributeName: "width",
|
|
1298
|
+
from: "0",
|
|
1299
|
+
to: contentW,
|
|
1300
|
+
dur: "0.6s",
|
|
1301
|
+
fill: "freeze"
|
|
1302
|
+
}) : "")));
|
|
1303
|
+
langParts.push(el("rect", { x: PX, y: barY, width: contentW, height: barH, rx: 5, class: "lang-progress" }));
|
|
1304
|
+
const barParts = [];
|
|
1166
1305
|
let off = 0;
|
|
1167
|
-
for (let i = 0;i <
|
|
1168
|
-
const lang =
|
|
1306
|
+
for (let i = 0;i < visualLangs.length; i++) {
|
|
1307
|
+
const lang = visualLangs[i];
|
|
1169
1308
|
const pct = (lang.size / totalSize * 100).toFixed(1);
|
|
1170
|
-
let w = i ===
|
|
1309
|
+
let w = i === visualLangs.length - 1 ? contentW - off : lang.size / totalSize * contentW;
|
|
1171
1310
|
if (w < 0)
|
|
1172
1311
|
w = 0;
|
|
1173
1312
|
const x = PX + off;
|
|
1174
|
-
const tooltip = compact ? "" : "
|
|
1175
|
-
|
|
1313
|
+
const tooltip = compact ? "" : el("title", {}, `${escapeXml(lang.name)} ${pct}%`);
|
|
1314
|
+
barParts.push(el("rect", { x, y: barY, width: w, height: barH, fill: lang.color }, tooltip));
|
|
1176
1315
|
off += w;
|
|
1177
1316
|
}
|
|
1178
|
-
|
|
1317
|
+
langParts.push(el("g", { "clip-path": "url(#lang-clip)" }, barParts.join("")));
|
|
1179
1318
|
for (let i = 0;i < legendItems.length; i++) {
|
|
1180
1319
|
const item = legendItems[i];
|
|
1181
1320
|
const col = i % legendCols;
|
|
1182
1321
|
const row = Math.floor(i / legendCols);
|
|
1183
1322
|
const lx = PX + col * legendColW;
|
|
1184
1323
|
const ly = legendY + row * legendRowH;
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
parts.push('<text x="' + (lx + 14) + '" y="' + (ly + 4) + '" class="lang-name" font-weight="500">' + escapeXml(item.name) + " " + item.pct + "%</text>");
|
|
1190
|
-
parts.push("</g>");
|
|
1324
|
+
langParts.push(el("g", {
|
|
1325
|
+
class: animate ? "stagger" : undefined,
|
|
1326
|
+
style: animate ? `animation-delay: ${(i + 3) * 150}ms` : undefined
|
|
1327
|
+
}, el("circle", { cx: lx + 5, cy: ly, r: 4, fill: item.color }) + el("text", { x: lx + 14, y: ly + 4, class: "lang-name", "font-weight": "500" }, `${escapeXml(item.name)} ${item.pct}%`)));
|
|
1191
1328
|
}
|
|
1192
|
-
parts.push("
|
|
1329
|
+
parts.push(el("g", {}, langParts.join("")));
|
|
1193
1330
|
}
|
|
1194
1331
|
parts.push("</svg>");
|
|
1195
1332
|
return parts.join("");
|
|
1196
1333
|
}
|
|
1197
1334
|
export {
|
|
1198
|
-
|
|
1199
|
-
themes,
|
|
1200
|
-
resolveColors,
|
|
1201
|
-
renderCard,
|
|
1202
|
-
kFormat,
|
|
1203
|
-
icons,
|
|
1204
|
-
icon,
|
|
1205
|
-
getLangColor,
|
|
1206
|
-
escapeXml,
|
|
1335
|
+
FONT_FACE,
|
|
1207
1336
|
FONT_FAMILY,
|
|
1208
|
-
|
|
1337
|
+
escapeXml,
|
|
1338
|
+
getLangColor,
|
|
1339
|
+
icon,
|
|
1340
|
+
icons,
|
|
1341
|
+
kFormat,
|
|
1342
|
+
measureText,
|
|
1343
|
+
renderCard,
|
|
1344
|
+
resolveColors,
|
|
1345
|
+
themes,
|
|
1346
|
+
wrapText
|
|
1209
1347
|
};
|
package/dist/utils/font.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
/** System font stack — no shipped font, respects the user's OS. */
|
|
2
|
+
export declare const FONT_FAMILY = "ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\"";
|
|
3
|
+
/** @deprecated No shipped font — kept for API backwards compatibility. */
|
|
2
4
|
export declare const FONT_FACE = "";
|
package/dist/utils/format.d.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
1
|
export declare function kFormat(num: number): string;
|
|
2
|
+
/**
|
|
3
|
+
* Approximate rendered width of a string in SVG user units for the default
|
|
4
|
+
* sans-serif stack. Widths are per-character averages tuned to the card's
|
|
5
|
+
* font sizes and weights, so layout can reserve enough space without relying
|
|
6
|
+
* on browser text measurement (which is unavailable server-side).
|
|
7
|
+
*/
|
|
8
|
+
export declare function measureText(text: string, size: number, weight?: number): number;
|
|
2
9
|
export declare function escapeXml(str: string): string;
|
|
3
10
|
export declare function wrapText(input: string, maxLen: number, maxLines: number): string[];
|
package/dist/utils/icons.d.ts
CHANGED
|
@@ -6,4 +6,6 @@ export declare const icons: {
|
|
|
6
6
|
repo: string;
|
|
7
7
|
x: string;
|
|
8
8
|
};
|
|
9
|
-
|
|
9
|
+
type IconName = keyof typeof icons;
|
|
10
|
+
export declare function icon(name: IconName, color: string, size?: number): string;
|
|
11
|
+
export {};
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { resolveColors, themes, type Theme } from "./themes";
|
|
2
|
-
export { kFormat, escapeXml, wrapText } from "./format";
|
|
2
|
+
export { kFormat, escapeXml, wrapText, measureText } from "./format";
|
|
3
3
|
export { getLangColor } from "./languages";
|
|
4
4
|
export { icons, icon } from "./icons";
|
|
5
5
|
export { FONT_FACE, FONT_FAMILY } from "./font";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shiinasaku/github-card",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.2.0",
|
|
4
4
|
"description": "Beautiful GitHub stats SVG cards. Embed in README, use as API, or render programmatically.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"badge",
|
|
@@ -23,12 +23,11 @@
|
|
|
23
23
|
},
|
|
24
24
|
"repository": {
|
|
25
25
|
"type": "git",
|
|
26
|
-
"url": "https://github.com/ShiinaSaku/Github-Card.git"
|
|
26
|
+
"url": "git+https://github.com/ShiinaSaku/Github-Card.git"
|
|
27
27
|
},
|
|
28
28
|
"files": [
|
|
29
29
|
"dist",
|
|
30
30
|
"src",
|
|
31
|
-
"fonts",
|
|
32
31
|
"README.md",
|
|
33
32
|
"LICENSE"
|
|
34
33
|
],
|
|
@@ -69,15 +68,15 @@
|
|
|
69
68
|
"@elysia/cors": "^1.4.2",
|
|
70
69
|
"@elysia/openapi": "^1.4.15",
|
|
71
70
|
"@elysia/server-timing": "^1.4.1",
|
|
72
|
-
"elysia": "
|
|
71
|
+
"elysia": "1.4.29"
|
|
73
72
|
},
|
|
74
73
|
"devDependencies": {
|
|
75
74
|
"@tailwindcss/cli": "^4.3.3",
|
|
76
|
-
"@types/bun": "^1.
|
|
77
|
-
"oxfmt": "0.
|
|
78
|
-
"oxlint": "1.
|
|
75
|
+
"@types/bun": "^1.4.0",
|
|
76
|
+
"oxfmt": "0.64.0",
|
|
77
|
+
"oxlint": "1.79.0",
|
|
79
78
|
"tailwindcss": "^4.3.3",
|
|
80
|
-
"typescript": "^
|
|
79
|
+
"typescript": "^6.0.3"
|
|
81
80
|
},
|
|
82
|
-
"packageManager": "bun@1.
|
|
81
|
+
"packageManager": "bun@1.4.0"
|
|
83
82
|
}
|