@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/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 (num >= 999950)
172
- return trim(num / 1e6) + "M";
173
- if (num >= 999.95)
174
- return trim(num / 1000) + "k";
175
- return String(Math.round(num));
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
@@ -860,24 +897,33 @@ function getLangColor(lang) {
860
897
  }
861
898
  // src/utils/icons.ts
862
899
  var icons = {
863
- star: `<path fill-rule="evenodd" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"/>`,
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 fill-rule="evenodd" d="M7.177 3.073L9.573.677A.25.25 0 0110 .854v4.792a.25.25 0 01-.427.177L7.177 3.427a.25.25 0 010-.354zM3.75 2.5a.75.75 0 100 1.5.75.75 0 000-1.5zm-2.25.75a2.25 2.25 0 113 2.122v5.256a2.251 2.251 0 11-1.5 0V5.372A2.25 2.25 0 011.5 3.25zM11 2.5h-1V4h1a1 1 0 011 1v5.628a2.251 2.251 0 101.5 0V5A2.5 2.5 0 0011 2.5zm1 10.25a.75.75 0 111.5 0 .75.75 0 01-1.5 0zM3.75 12a.75.75 0 100 1.5.75.75 0 000-1.5z"/>`,
866
- issue: `<path fill-rule="evenodd" d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm9 3a1 1 0 11-2 0 1 1 0 012 0zm-.25-6.25a.75.75 0 00-1.5 0v3.5a.75.75 0 001.5 0v-3.5z"/>`,
867
- repo: `<path fill-rule="evenodd" d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25v-.878a2.25 2.25 0 10-1.5 0v.878a.75.75 0 01-.75.75h-4.5A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"></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 viewBox = name === "x" ? "0 0 24 24" : "0 0 16 16";
919
+ const meta = iconMeta[name];
920
+ const viewBox = meta?.viewBox ?? "0 0 16 16";
875
921
  const colorStr = color.startsWith("#") ? color : `#${color}`;
876
- const fill = name === "x" ? `stroke="${colorStr}" stroke-width="2" fill="none"` : `fill="${colorStr}"`;
877
- return `<svg width="${size}" height="${size}" viewBox="${viewBox}" ${fill} role="img">${path}</svg>`;
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.replace(/"/g, "&apos;");
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 = (user.avatarUrl || "").replace(/&/g, "&amp;");
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 = 25;
1029
- const PY = 25;
1030
- const avatarSize = 64;
1086
+ const PX = 24;
1087
+ const PY = 22;
1088
+ const avatarSize = 60;
1031
1089
  const avatarR = avatarSize / 2;
1032
- const metaLen = user.login.length + (pronouns ? pronouns.length + 3 : 0);
1033
- const maxBioLen = bioLines.length ? Math.max(...bioLines.map((l) => l.length)) : 0;
1034
- const profileTextW = Math.max(displayName.length * 11, metaLen * 7.5, maxBioLen * 6.5, twitter.length * 7 + 20);
1035
- const profileW = PX + avatarSize + 16 + profileTextW + PX;
1036
- const statsW = visible.length > 0 ? visible.length * 85 + PX * 2 : 0;
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 + 14;
1041
- const nameY = PY + 22;
1042
- const loginY = nameY + 18;
1043
- const bioStartY = loginY + 16;
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 + 10 - PY;
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 + 4);
1051
- const statsY = PY + profileH + 14;
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 ? 50 : 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 ? 16 : 12);
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 + 12;
1063
- const legendRowH = 16;
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 + 12 + legendRows * legendRowH + 8 : 0;
1086
- const H = barY + (hasLangs ? langSectionH : 0) + 12 + (hasLangs ? 0 : 4);
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
- parts.push('<svg width="' + W + '" height="' + H + '" viewBox="0 0 ' + W + " " + H + '" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="cardTitle cardDesc">');
1089
- parts.push('<title id="cardTitle">' + escapeXml(displayName + "'s GitHub Stats") + "</title>");
1090
- parts.push('<desc id="cardDesc">GitHub profile card for ' + escapeXml(displayName) + " with " + kFormat(stats.stars) + " stars, " + kFormat(stats.commits) + " commits, and " + activeLangsList.length + " highlighted languages.</desc>");
1091
- const cssLines = [];
1092
- cssLines.push("* { font-family: " + fontFamily + "; }");
1093
- cssLines.push(".header { font-size: 18px; font-weight: 700; fill: " + titleColor + "; }");
1094
- cssLines.push(".stat { font-size: 14px; font-weight: 600; fill: " + textColor + "; }");
1095
- cssLines.push(".bold { font-weight: 700; }");
1096
- cssLines.push(".icon { fill: " + iconColor + "; }");
1097
- cssLines.push(".lang-name { font-size: 11px; fill: " + textColor + "; opacity: 0.8; }");
1098
- cssLines.push(".lang-progress { fill: " + textColor + "; opacity: 0.08; }");
1099
- cssLines.push(".meta { font-size: 12px; font-weight: 500; fill: " + textColor + "; opacity: 0.7; }");
1100
- cssLines.push(".bio { font-size: 11px; font-weight: 400; fill: " + textColor + "; opacity: 0.68; }");
1101
- cssLines.push(".stat-label { font-size: 9px; font-weight: 600; fill: " + textColor + "; opacity: 0.5; letter-spacing: 0.08em; }");
1102
- cssLines.push(".stat-value { font-size: 15px; font-weight: 800; fill: " + textColor + "; }");
1103
- cssLines.push(".twitter-text { font-size: 11px; font-weight: 500; fill: " + textColor + "; opacity: 0.65; }");
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
- cssLines.push("@keyframes fadeInAnimation { from { opacity: 0; } to { opacity: 1; } }");
1106
- cssLines.push(".stagger { opacity: 0; animation: fadeInAnimation 0.3s ease-in-out forwards; }");
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("<style>" + cssLines.join(" ") + "</style>");
1109
- parts.push("<defs>");
1110
- parts.push('<clipPath id="av"><circle cx="' + (PX + avatarR) + '" cy="' + (PY + avatarR) + '" r="' + avatarR + '"/></clipPath>');
1111
- parts.push('<radialGradient id="gl" cx="12%" cy="15%" r="65%">' + '<stop offset="0%" stop-color="' + iconColor + '" stop-opacity="0.08"/>' + '<stop offset="100%" stop-color="' + iconColor + '" stop-opacity="0"/>' + "</radialGradient>");
1112
- parts.push('<radialGradient id="gl2" cx="92%" cy="95%" r="55%">' + '<stop offset="0%" stop-color="' + titleColor + '" stop-opacity="0.035"/>' + '<stop offset="100%" stop-color="' + titleColor + '" stop-opacity="0"/>' + "</radialGradient>");
1113
- parts.push('<linearGradient id="ring" x1="0" y1="0" x2="1" y2="1">' + '<stop offset="0%" stop-color="' + titleColor + '"/>' + '<stop offset="100%" stop-color="' + iconColor + '"/>' + "</linearGradient>");
1114
- parts.push('<linearGradient id="card-bg" x1="0" y1="0" x2="' + W + '" y2="' + H + '" gradientUnits="userSpaceOnUse">' + '<stop offset="0%" stop-color="' + bgColor + '"/>' + '<stop offset="82%" stop-color="' + bgColor + '"/>' + '<stop offset="100%" stop-color="' + titleColor + '" stop-opacity="0.04"/>' + "</linearGradient>");
1115
- parts.push('<linearGradient id="rim" x1="0" y1="0" x2="' + W + '" y2="0" gradientUnits="userSpaceOnUse">' + '<stop offset="0%" stop-color="' + iconColor + '" stop-opacity="0"/>' + '<stop offset="45%" stop-color="' + iconColor + '" stop-opacity="0.28"/>' + '<stop offset="100%" stop-color="' + iconColor + '" stop-opacity="0"/>' + "</linearGradient>");
1116
- parts.push("</defs>");
1117
- parts.push('<rect x="0.5" y="0.5" width="' + (W - 1) + '" height="' + (H - 1) + '" rx="10" fill="' + "url(#card-bg)" + '" stroke-opacity="1"/>');
1118
- parts.push('<rect x="0.5" y="0.5" width="' + (W - 1) + '" height="' + (H - 1) + '" rx="10" fill="url(#gl)"/>');
1119
- parts.push('<rect x="0.5" y="0.5" width="' + (W - 1) + '" height="' + (H - 1) + '" rx="10" fill="url(#gl2)"/>');
1120
- parts.push('<line x1="' + PX + '" y1="1.5" x2="' + (W - PX) + '" y2="1.5" stroke="url(#rim)" stroke-width="1"/>');
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('<rect x="0.5" y="0.5" width="' + (W - 1) + '" height="' + (H - 1) + '" rx="10" fill="none" stroke="' + borderColor + '" stroke-opacity="0.5"/>');
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('<g transform="translate(0,0)">' + '<circle cx="' + (PX + avatarR + 1) + '" cy="' + (PY + avatarR + 1) + '" r="' + (avatarR + 3) + '" fill="' + titleColor + '" opacity="0.08"/>' + '<image href="' + avatar + '" x="' + PX + '" y="' + PY + '" width="' + avatarSize + '" height="' + avatarSize + '" clip-path="url(#av)" preserveAspectRatio="xMidYMid slice"/>' + '<circle cx="' + (PX + avatarR) + '" cy="' + (PY + avatarR) + '" r="' + (avatarR + 1.5) + '" fill="none" stroke="url(#ring)" stroke-opacity="0.55" stroke-width="1.5"/>' + "</g>");
1125
- parts.push('<g transform="translate(0,0)">');
1126
- parts.push('<text x="' + infoX + '" y="' + nameY + '" class="header">' + nameEsc + "</text>");
1127
- const metaStr = pronouns ? "@" + loginEsc + " · " + pronouns : "@" + loginEsc;
1128
- parts.push('<text x="' + infoX + '" y="' + loginY + '" class="meta">' + metaStr + "</text>");
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
- parts.push('<text x="' + infoX + '" y="' + (bioStartY + i * 14) + '" class="bio">' + escapeXml(bioLines[i].trim()) + "</text>");
1239
+ textParts.push(el("text", { x: infoX, y: bioStartY + i * 14, class: "bio" }, escapeXml(bioLines[i].trim())));
1131
1240
  }
1132
1241
  if (twitter) {
1133
- parts.push('<g transform="translate(' + infoX + "," + (twitterY - 9) + ')">' + icon("x", textColor, 11) + '<text x="15" y="9" class="twitter-text">@' + twitter + "</text></g>");
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("</g>");
1244
+ parts.push(el("g", {}, textParts.join("")));
1136
1245
  if (visible.length > 0) {
1137
- parts.push('<rect x="' + PX + '" y="' + (statsY - 8) + '" width="' + contentW + '" height="' + (statsH + 8) + '" rx="8" fill="' + textColor + '" opacity="0.035"/>');
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 cellX = PX + statW * i + 4;
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
- parts.push('<rect x="' + cellX + '" y="' + (statsY - 4) + '" width="' + Math.max(0, statW - 8) + '" height="' + (statsH - 2) + '" rx="8" fill="' + textColor + '" opacity="0.028"/>');
1148
- parts.push('<g transform="translate(' + cx + "," + iy + ')" class="stat' + staggerClass + '"' + staggerStyle + ">");
1149
- parts.push('<text x="0" y="26" class="stat-value" text-anchor="middle">' + kFormat(val) + "</text>");
1150
- const labelText = d.label.toUpperCase();
1151
- const labelW = labelText.length * 6.8;
1152
- const rowW = 10 + 4 + labelW;
1153
- parts.push('<g transform="translate(' + -rowW / 2 + ',31.5)">' + icon(d.iconName, iconColor, 10) + "</g>");
1154
- parts.push('<text x="' + (-rowW / 2 + 14) + '" y="40" class="stat-label">' + labelText + "</text>");
1155
- parts.push("</g>");
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("</g>");
1278
+ parts.push(el("g", {}, statParts.join("")));
1158
1279
  }
1159
1280
  if (hasLangs) {
1160
- parts.push('<line x1="' + PX + '" y1="' + (barY - 8) + '" x2="' + (W - PX) + '" y2="' + (barY - 8) + '" stroke="' + textColor + '" stroke-opacity="0.08"/>');
1161
- parts.push('<g transform="translate(0,0)">');
1162
- parts.push('<clipPath id="lang-clip"><rect x="' + PX + '" y="' + barY + '" width="' + (animate ? "0" : contentW) + '" height="' + barH + '" rx="5">' + (animate ? '<animate attributeName="width" from="0" to="' + contentW + '" dur="0.6s" fill="freeze"/>' : "") + "</rect></clipPath>");
1163
- parts.push('<rect x="' + PX + '" y="' + barY + '" width="' + contentW + '" height="' + barH + '" rx="5" class="lang-progress"/>');
1164
- parts.push('<g clip-path="url(#lang-clip)">');
1165
- const activeLangs = visualLangs;
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 < activeLangs.length; i++) {
1168
- const lang = activeLangs[i];
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 === activeLangs.length - 1 ? contentW - off : lang.size / totalSize * contentW;
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 ? "" : "<title>" + escapeXml(lang.name) + " " + pct + "%</title>";
1175
- parts.push('<rect x="' + x + '" y="' + barY + '" width="' + w + '" height="' + barH + '" fill="' + lang.color + '">' + tooltip + "</rect>");
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
- parts.push("</g>");
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
- const staggerStyle = animate ? ' style="animation-delay: ' + (i + 3) * 150 + 'ms"' : "";
1186
- const staggerClass = animate ? ' class="stagger"' : "";
1187
- parts.push("<g" + staggerClass + staggerStyle + ">");
1188
- parts.push('<circle cx="' + (lx + 5) + '" cy="' + ly + '" r="4" fill="' + item.color + '"/>');
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("</g>");
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
- wrapText,
1199
- themes,
1200
- resolveColors,
1201
- renderCard,
1202
- kFormat,
1203
- icons,
1204
- icon,
1205
- getLangColor,
1206
- escapeXml,
1335
+ FONT_FACE,
1207
1336
  FONT_FAMILY,
1208
- FONT_FACE
1337
+ escapeXml,
1338
+ getLangColor,
1339
+ icon,
1340
+ icons,
1341
+ kFormat,
1342
+ measureText,
1343
+ renderCard,
1344
+ resolveColors,
1345
+ themes,
1346
+ wrapText
1209
1347
  };
@@ -1,2 +1,4 @@
1
- export declare const FONT_FAMILY = "ui-sans-serif, system-ui, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\"";
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 = "";
@@ -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[];
@@ -6,4 +6,6 @@ export declare const icons: {
6
6
  repo: string;
7
7
  x: string;
8
8
  };
9
- export declare function icon(name: keyof typeof icons, color: string, size?: number): string;
9
+ type IconName = keyof typeof icons;
10
+ export declare function icon(name: IconName, color: string, size?: number): string;
11
+ export {};
@@ -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.1.0",
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": "^1.4.29"
71
+ "elysia": "1.4.29"
73
72
  },
74
73
  "devDependencies": {
75
74
  "@tailwindcss/cli": "^4.3.3",
76
- "@types/bun": "^1.3.14",
77
- "oxfmt": "0.60.0",
78
- "oxlint": "1.75.0",
75
+ "@types/bun": "^1.4.0",
76
+ "oxfmt": "0.64.0",
77
+ "oxlint": "1.79.0",
79
78
  "tailwindcss": "^4.3.3",
80
- "typescript": "^7.0.2"
79
+ "typescript": "^6.0.3"
81
80
  },
82
- "packageManager": "bun@1.3.14"
81
+ "packageManager": "bun@1.4.0"
83
82
  }