@stats-forge/github-stats-forge-core 0.0.2 → 0.1.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 (208) hide show
  1. package/README.md +0 -10
  2. package/build/api/api-result.d.ts +6 -7
  3. package/build/api/api-result.d.ts.map +1 -1
  4. package/build/api/api-result.js +4 -5
  5. package/build/api/gist.d.ts +3 -10
  6. package/build/api/gist.d.ts.map +1 -1
  7. package/build/api/gist.js +4 -11
  8. package/build/api/index.d.ts +12 -12
  9. package/build/api/params.d.ts +11 -8
  10. package/build/api/params.d.ts.map +1 -1
  11. package/build/api/params.js +16 -14
  12. package/build/api/pin.d.ts +3 -19
  13. package/build/api/pin.d.ts.map +1 -1
  14. package/build/api/pin.js +6 -22
  15. package/build/api/stats.d.ts +10 -4
  16. package/build/api/stats.d.ts.map +1 -1
  17. package/build/api/stats.js +6 -32
  18. package/build/api/top-langs.d.ts +12 -6
  19. package/build/api/top-langs.d.ts.map +1 -1
  20. package/build/api/top-langs.js +6 -26
  21. package/build/api/wakatime.d.ts +13 -5
  22. package/build/api/wakatime.d.ts.map +1 -1
  23. package/build/api/wakatime.js +4 -21
  24. package/build/calculateRank.d.ts +0 -9
  25. package/build/calculateRank.d.ts.map +1 -1
  26. package/build/calculateRank.js +12 -17
  27. package/build/cards/gist.d.ts +2 -4
  28. package/build/cards/gist.d.ts.map +1 -1
  29. package/build/cards/gist.js +27 -36
  30. package/build/cards/options.d.ts +1 -1
  31. package/build/cards/repo.d.ts +2 -4
  32. package/build/cards/repo.d.ts.map +1 -1
  33. package/build/cards/repo.js +61 -93
  34. package/build/cards/stats.d.ts +3 -8
  35. package/build/cards/stats.d.ts.map +1 -1
  36. package/build/cards/stats.js +119 -165
  37. package/build/cards/top-languages.d.ts +5 -31
  38. package/build/cards/top-languages.d.ts.map +1 -1
  39. package/build/cards/top-languages.js +176 -384
  40. package/build/cards/wakatime.d.ts +4 -6
  41. package/build/cards/wakatime.d.ts.map +1 -1
  42. package/build/cards/wakatime.js +118 -185
  43. package/build/common/Card.d.ts +14 -62
  44. package/build/common/Card.d.ts.map +1 -1
  45. package/build/common/Card.js +97 -199
  46. package/build/common/I18n.d.ts +0 -6
  47. package/build/common/I18n.d.ts.map +1 -1
  48. package/build/common/I18n.js +3 -6
  49. package/build/common/color.d.ts +4 -17
  50. package/build/common/color.d.ts.map +1 -1
  51. package/build/common/color.js +67 -49
  52. package/build/common/config.d.ts +4 -14
  53. package/build/common/config.d.ts.map +1 -1
  54. package/build/common/config.js +3 -16
  55. package/build/common/constants.d.ts +4 -2
  56. package/build/common/constants.d.ts.map +1 -1
  57. package/build/common/constants.js +3 -1
  58. package/build/common/date.d.ts +0 -2
  59. package/build/common/date.d.ts.map +1 -1
  60. package/build/common/date.js +0 -2
  61. package/build/common/emojiMap.d.ts +8 -0
  62. package/build/common/emojiMap.d.ts.map +1 -0
  63. package/build/common/emojiMap.js +10 -0
  64. package/build/common/emojiMap.json +1915 -0
  65. package/build/common/error.d.ts +1 -10
  66. package/build/common/error.d.ts.map +1 -1
  67. package/build/common/error.js +1 -10
  68. package/build/common/fmt.d.ts +3 -15
  69. package/build/common/fmt.d.ts.map +1 -1
  70. package/build/common/fmt.js +7 -35
  71. package/build/common/http.d.ts +2 -7
  72. package/build/common/http.d.ts.map +1 -1
  73. package/build/common/http.js +8 -17
  74. package/build/common/icons.d.ts +18 -20
  75. package/build/common/icons.d.ts.map +1 -1
  76. package/build/common/icons.js +103 -67
  77. package/build/common/languageColors.d.ts +0 -1
  78. package/build/common/languageColors.d.ts.map +1 -1
  79. package/build/common/languageColors.js +1 -4
  80. package/build/common/languageColors.json +18 -2
  81. package/build/common/ops.d.ts +3 -14
  82. package/build/common/ops.d.ts.map +1 -1
  83. package/build/common/ops.js +19 -30
  84. package/build/common/render.d.ts +28 -82
  85. package/build/common/render.d.ts.map +1 -1
  86. package/build/common/render.js +175 -211
  87. package/build/common/retryer.d.ts +2 -7
  88. package/build/common/retryer.d.ts.map +1 -1
  89. package/build/common/retryer.js +4 -11
  90. package/build/fetchers/gist.d.ts +2 -5
  91. package/build/fetchers/gist.d.ts.map +1 -1
  92. package/build/fetchers/gist.js +1 -5
  93. package/build/fetchers/index.d.ts +12 -12
  94. package/build/fetchers/repo.d.ts +2 -11
  95. package/build/fetchers/repo.d.ts.map +1 -1
  96. package/build/fetchers/repo.js +20 -33
  97. package/build/fetchers/stats.d.ts +2 -32
  98. package/build/fetchers/stats.d.ts.map +1 -1
  99. package/build/fetchers/stats.js +27 -86
  100. package/build/fetchers/top-languages.d.ts +2 -9
  101. package/build/fetchers/top-languages.d.ts.map +1 -1
  102. package/build/fetchers/top-languages.js +15 -31
  103. package/build/fetchers/types.d.ts +1 -1
  104. package/build/fetchers/wakatime.d.ts +2 -6
  105. package/build/fetchers/wakatime.d.ts.map +1 -1
  106. package/build/fetchers/wakatime.js +1 -5
  107. package/build/graphql/contributionsDocument.d.ts +3 -3
  108. package/build/graphql/contributionsDocument.d.ts.map +1 -1
  109. package/build/graphql/contributionsDocument.js +0 -1
  110. package/build/graphql/generated/gist.d.ts +1 -1
  111. package/build/graphql/generated/repo.d.ts +1 -1
  112. package/build/graphql/generated/stats.d.ts +3 -3
  113. package/build/graphql/generated/top-languages.d.ts +2 -2
  114. package/build/graphql/graphqlDocument.d.ts +0 -1
  115. package/build/graphql/graphqlDocument.d.ts.map +1 -1
  116. package/build/graphql/graphqlDocument.js +0 -1
  117. package/build/graphql/reposContributedToDocument.d.ts +4 -5
  118. package/build/graphql/reposContributedToDocument.d.ts.map +1 -1
  119. package/build/graphql/reposContributedToDocument.js +0 -2
  120. package/build/markup/css.d.ts +35 -0
  121. package/build/markup/css.d.ts.map +1 -0
  122. package/build/markup/css.js +64 -0
  123. package/build/markup/element.d.ts +41 -0
  124. package/build/markup/element.d.ts.map +1 -0
  125. package/build/markup/element.js +127 -0
  126. package/build/markup/html.d.ts +13 -0
  127. package/build/markup/html.d.ts.map +1 -0
  128. package/build/markup/html.js +27 -0
  129. package/build/markup/index.d.ts +9 -0
  130. package/build/markup/index.d.ts.map +1 -0
  131. package/build/markup/index.js +6 -0
  132. package/build/markup/serialize.d.ts +16 -0
  133. package/build/markup/serialize.d.ts.map +1 -0
  134. package/build/markup/serialize.js +17 -0
  135. package/build/themes/index.d.ts +332 -333
  136. package/build/themes/index.d.ts.map +1 -1
  137. package/build/themes/index.js +1 -1
  138. package/build/translations.d.ts +2 -6
  139. package/build/translations.d.ts.map +1 -1
  140. package/build/translations.js +621 -628
  141. package/package.json +17 -26
  142. package/src/api/api-result.ts +10 -9
  143. package/src/api/gist.ts +11 -18
  144. package/src/api/index.ts +12 -12
  145. package/src/api/params.ts +27 -21
  146. package/src/api/pin.ts +13 -28
  147. package/src/api/stats.ts +13 -39
  148. package/src/api/top-langs.ts +13 -33
  149. package/src/api/wakatime.ts +11 -28
  150. package/src/calculateRank.ts +12 -23
  151. package/src/cards/gist.ts +40 -43
  152. package/src/cards/options.ts +1 -1
  153. package/src/cards/repo.ts +97 -107
  154. package/src/cards/stats.ts +165 -184
  155. package/src/cards/top-languages.ts +297 -413
  156. package/src/cards/wakatime.ts +193 -221
  157. package/src/common/Card.ts +165 -217
  158. package/src/common/I18n.ts +3 -6
  159. package/src/common/color.ts +80 -57
  160. package/src/common/config.ts +5 -18
  161. package/src/common/constants.ts +5 -2
  162. package/src/common/date.ts +0 -2
  163. package/src/common/emojiMap.json +1915 -0
  164. package/src/common/emojiMap.ts +13 -0
  165. package/src/common/error.ts +2 -11
  166. package/src/common/fmt.ts +7 -44
  167. package/src/common/http.ts +9 -15
  168. package/src/common/icons.ts +117 -42
  169. package/src/common/languageColors.json +18 -2
  170. package/src/common/languageColors.ts +1 -4
  171. package/src/common/ops.ts +26 -34
  172. package/src/common/render.ts +279 -232
  173. package/src/common/retryer.ts +8 -15
  174. package/src/fetchers/gist.ts +8 -12
  175. package/src/fetchers/index.ts +12 -12
  176. package/src/fetchers/repo.ts +27 -40
  177. package/src/fetchers/stats.ts +44 -102
  178. package/src/fetchers/top-languages.ts +26 -40
  179. package/src/fetchers/types.ts +1 -1
  180. package/src/fetchers/wakatime.ts +5 -9
  181. package/src/graphql/contributionsDocument.ts +7 -5
  182. package/src/graphql/generated/gist.ts +1 -1
  183. package/src/graphql/generated/repo.ts +1 -1
  184. package/src/graphql/generated/stats.ts +2 -2
  185. package/src/graphql/generated/top-languages.ts +2 -2
  186. package/src/graphql/graphqlDocument.ts +0 -1
  187. package/src/graphql/reposContributedToDocument.ts +6 -7
  188. package/src/markup/css.ts +118 -0
  189. package/src/markup/element.ts +246 -0
  190. package/src/markup/html.ts +34 -0
  191. package/src/markup/index.ts +9 -0
  192. package/src/markup/serialize.ts +27 -0
  193. package/src/themes/index.ts +1 -1
  194. package/src/translations.ts +622 -629
  195. package/build/cards/index.d.ts +0 -11
  196. package/build/cards/index.d.ts.map +0 -1
  197. package/build/cards/index.js +0 -7
  198. package/build/common/html.d.ts +0 -8
  199. package/build/common/html.d.ts.map +0 -1
  200. package/build/common/html.js +0 -14
  201. package/build/index.d.ts +0 -15
  202. package/build/index.d.ts.map +0 -1
  203. package/build/index.js +0 -10
  204. package/src/_emoji-name-map.d.ts +0 -10
  205. package/src/_github-username-regex.d.ts +0 -4
  206. package/src/cards/index.ts +0 -21
  207. package/src/common/html.ts +0 -17
  208. package/src/index.ts +0 -18
@@ -1,15 +1,17 @@
1
- import { Card } from '../common/Card.js';
2
- import { getLightDarkColors } from '../common/color.js';
3
- import type { CardColors } from '../common/color.js';
4
- import { CardError } from '../common/error.js';
5
- import { I18n } from '../common/I18n.js';
6
- import { icons, rankIcon } from '../common/icons.js';
7
- import { buildSearchFilter, clampValue } from '../common/ops.js';
8
- import { createTextNode, flexLayout, measureText } from '../common/render.js';
9
- import type { StatsData } from '../fetchers/types.js';
10
- import { statCardLocales, wakatimeCardLocales } from '../translations.js';
11
-
12
- import type { CardOptions, CommonCardOptions } from './options.js';
1
+ import { Card } from '../common/Card.ts';
2
+ import { getLightDarkColors } from '../common/color.ts';
3
+ import type { CardColors } from '../common/color.ts';
4
+ import { CardError } from '../common/error.ts';
5
+ import { I18n } from '../common/I18n.ts';
6
+ import { icons, rankIcon } from '../common/icons.ts';
7
+ import { buildSearchFilter, clampValue } from '../common/ops.ts';
8
+ import { createTextNode, flexLayout, measureText } from '../common/render.ts';
9
+ import type { StatsData } from '../fetchers/types.ts';
10
+ import type { Child, CssChild } from '../markup/index.ts';
11
+ import { atRule, cssComment, el, rule } from '../markup/index.ts';
12
+ import { statCardLocales, wakatimeCardLocales } from '../translations.ts';
13
+
14
+ import type { CardOptions, CommonCardOptions } from './options.ts';
13
15
 
14
16
  const CARD_MIN_WIDTH = 287;
15
17
  const CARD_DEFAULT_WIDTH = 287;
@@ -18,6 +20,18 @@ const RANK_CARD_DEFAULT_WIDTH = 450;
18
20
  const RANK_ONLY_CARD_MIN_WIDTH = 290;
19
21
  const RANK_ONLY_CARD_DEFAULT_WIDTH = 290;
20
22
 
23
+ const STAT_FONT_SIZE = 14;
24
+ /** Padding the card keeps at its edges; matches `Card`'s own `paddingX`. */
25
+ const CARD_PADDING_X = 25;
26
+ /** How far a stat row is translated into the card; see `createTextNode`. */
27
+ const STAT_ROW_X = 25;
28
+ /** Room the rank ring needs at the right edge, so a value never runs into it. */
29
+ const RANK_GUTTER = 120;
30
+ /** `createTextNode`'s own label offset, which it applies only when icons are shown. */
31
+ const LABEL_X_OFFSET = 25;
32
+ /** Smallest gap kept between the longest label and its value. */
33
+ const LABEL_VALUE_GAP = 16;
34
+
21
35
  /** Rank indicators the card can draw; the api validates `rank_icon` against this. */
22
36
  const RANK_ICONS = ['default', 'github', 'percentile'] as const;
23
37
  type RankIcon = (typeof RANK_ICONS)[number];
@@ -44,7 +58,7 @@ interface StatCardOptions extends CommonCardOptions {
44
58
 
45
59
  /** Meta data for a stat, used to build its text node and accessibility label. */
46
60
  interface StatItem {
47
- icon: string;
61
+ icon: Child;
48
62
  label: string;
49
63
  value: number | string;
50
64
  id: string;
@@ -53,7 +67,7 @@ interface StatItem {
53
67
  }
54
68
 
55
69
  /** Long locales that need more space for text. Keep sorted alphabetically. */
56
- const LONG_LOCALES: Array<string> = [
70
+ const LONG_LOCALES: ReadonlySet<string> = new Set([
57
71
  'az',
58
72
  'bg',
59
73
  'cs',
@@ -80,58 +94,37 @@ const LONG_LOCALES: Array<string> = [
80
94
  'uk-ua',
81
95
  'uz',
82
96
  'zh-tw',
83
- ];
97
+ ]);
84
98
 
85
99
  /**
86
100
  * Calculates progress along the boundary of the circle, i.e. its circumference.
87
101
  *
88
- * @param value The rank value to calculate progress for.
89
102
  * @returns Progress value.
90
103
  */
91
104
  const calculateCircleProgress = (value: number): number => {
92
105
  const radius = 40;
93
- const c = Math.PI * (radius * 2);
94
-
95
- if (value < 0) {
96
- value = 0;
97
- }
98
- if (value > 100) {
99
- value = 100;
100
- }
106
+ const circumference = Math.PI * (radius * 2);
107
+ const clamped = Math.min(Math.max(value, 0), 100);
101
108
 
102
- return ((100 - value) / 100) * c;
109
+ return ((100 - clamped) / 100) * circumference;
103
110
  };
104
111
 
105
112
  /**
106
113
  * Retrieves the animation to display progress along the circumference of circle
107
114
  * from the beginning to the given value in a clockwise direction.
108
115
  *
109
- * @param props The props object.
110
- * @param props.progress The progress value to animate to.
111
116
  * @returns Progress animation css.
112
117
  */
113
- const getProgressAnimation = ({ progress }: { progress: number }): string => {
114
- return `
115
- @keyframes rankAnimation {
116
- from {
117
- stroke-dashoffset: ${calculateCircleProgress(0)};
118
- }
119
- to {
120
- stroke-dashoffset: ${calculateCircleProgress(progress)};
121
- }
122
- }
123
- `;
124
- };
118
+ const getProgressAnimation = ({ progress }: { progress: number }): CssChild =>
119
+ atRule(
120
+ '@keyframes rankAnimation',
121
+ rule('from', { 'stroke-dashoffset': calculateCircleProgress(0) }),
122
+ rule('to', { 'stroke-dashoffset': calculateCircleProgress(progress) }),
123
+ );
125
124
 
126
125
  /**
127
126
  * Retrieves CSS styles for a card.
128
127
  *
129
- * @param colors The colors to use for the card.
130
- * @param colors.textColor The text color.
131
- * @param colors.iconColor The icon color.
132
- * @param colors.ringColor The ring color.
133
- * @param colors.show_icons Whether to show icons.
134
- * @param colors.progress The progress value to animate to.
135
128
  * @returns Card CSS styles.
136
129
  */
137
130
  const getStyles = ({
@@ -146,64 +139,56 @@ const getStyles = ({
146
139
  ringColor: string;
147
140
  show_icons: boolean;
148
141
  progress: number;
149
- }): string => {
150
- return `
151
- .stat {
152
- font: 600 14px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif; fill: ${textColor};
153
- }
154
- @supports(-moz-appearance: auto) {
155
- /* Selector detects Firefox */
156
- .stat { font-size:12px; }
157
- }
158
- .stagger {
159
- opacity: 0;
160
- animation: fadeInAnimation 0.3s ease-in-out forwards;
161
- }
162
- .rank-text {
163
- font: 800 24px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor};
164
- animation: scaleInAnimation 0.3s ease-in-out forwards;
165
- }
166
- .rank-percentile-header {
167
- font-size: 14px;
168
- }
169
- .rank-percentile-text {
170
- font-size: 16px;
171
- }
172
-
173
- .not_bold { font-weight: 400 }
174
- .bold { font-weight: 700 }
175
- .icon {
176
- fill: ${iconColor};
177
- display: ${show_icons ? 'block' : 'none'};
178
- }
179
-
180
- .rank-circle-rim {
181
- stroke: ${ringColor};
182
- fill: none;
183
- stroke-width: 6;
184
- opacity: 0.2;
185
- }
186
- .rank-circle {
187
- stroke: ${ringColor};
188
- stroke-dasharray: 250;
189
- fill: none;
190
- stroke-width: 6;
191
- stroke-linecap: round;
192
- opacity: 0.8;
193
- transform-origin: -10px 8px;
194
- transform: rotate(-90deg);
195
- animation: rankAnimation 1s forwards ease-in-out;
196
- }
197
- ${getProgressAnimation({ progress })}
198
- `;
199
- };
142
+ }): Array<CssChild> => [
143
+ rule('.stat', {
144
+ font: `400 ${STAT_FONT_SIZE}px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif`,
145
+ fill: textColor,
146
+ 'font-variant-numeric': 'tabular-nums',
147
+ }),
148
+ atRule(
149
+ '@supports(-moz-appearance: auto)',
150
+ cssComment('Selector detects Firefox'),
151
+ rule('.stat', { 'font-size': '12px' }),
152
+ ),
153
+ rule('.stagger', { opacity: 0, animation: 'fadeInAnimation 0.3s ease-in-out forwards' }),
154
+ rule('.rank-text', {
155
+ font: "700 22px 'Segoe UI', Ubuntu, Sans-Serif",
156
+ fill: textColor,
157
+ animation: 'scaleInAnimation 0.3s ease-in-out forwards',
158
+ }),
159
+ rule('.rank-percentile-header', { 'font-size': '14px' }),
160
+ rule('.rank-percentile-text', { 'font-size': '16px' }),
161
+ cssComment('Labels recede so that the values read first.'),
162
+ rule('.not_bold', { 'font-weight': 400, opacity: 0.75 }),
163
+ rule('.bold', { 'font-weight': 600 }),
164
+ rule('.icon', {
165
+ fill: iconColor,
166
+ opacity: 0.75,
167
+ display: show_icons ? 'block' : 'none',
168
+ }),
169
+ rule('.rank-circle-rim', {
170
+ stroke: ringColor,
171
+ fill: 'none',
172
+ 'stroke-width': 4,
173
+ opacity: 0.15,
174
+ }),
175
+ rule('.rank-circle', {
176
+ stroke: ringColor,
177
+ 'stroke-dasharray': 250,
178
+ fill: 'none',
179
+ 'stroke-width': 4,
180
+ 'stroke-linecap': 'round',
181
+ opacity: 1,
182
+ 'transform-origin': '-10px 8px',
183
+ transform: 'rotate(-90deg)',
184
+ animation: 'rankAnimation 0.8s forwards ease-in-out',
185
+ }),
186
+ getProgressAnimation({ progress }),
187
+ ];
200
188
 
201
189
  /**
202
190
  * Return the label for commits according to the selected options
203
191
  *
204
- * @param include_all_commits Option to include all years
205
- * @param commits_year Option to include only selected year
206
- * @param i18n The I18n instance.
207
192
  * @returns The label corresponding to the options.
208
193
  */
209
194
  const getTotalCommitsYearLabel = (
@@ -220,11 +205,6 @@ const getTotalCommitsYearLabel = (
220
205
  /**
221
206
  * Renders the stats card.
222
207
  *
223
- * @param stats The stats data.
224
- * @param options The card options.
225
- * @param username GitHub username, used to build stat search links.
226
- * @param repo Repositories to scope the search links to.
227
- * @param owner Owners to scope the search links to.
228
208
  * @returns The stats card SVG object.
229
209
  */
230
210
  const renderStatsCard = (
@@ -276,7 +256,7 @@ const renderStatsCard = (
276
256
  show = [],
277
257
  } = options;
278
258
 
279
- const lheight = parseInt(String(line_height), 10);
259
+ const lheight = Number.parseInt(String(line_height), 10);
280
260
 
281
261
  const { lightColors, darkColors } = getLightDarkColors(options);
282
262
 
@@ -290,13 +270,13 @@ const renderStatsCard = (
290
270
  });
291
271
 
292
272
  // Meta data for creating text nodes with createTextNode function
293
- const STATS: Record<string, StatItem> = {};
294
-
295
- STATS['stars'] = {
296
- icon: icons.star,
297
- label: i18n.t('statcard.totalstars'),
298
- value: totalStars,
299
- id: 'stars',
273
+ const STATS: Record<string, StatItem> = {
274
+ stars: {
275
+ icon: icons.star,
276
+ label: i18n.t('statcard.totalstars'),
277
+ value: totalStars,
278
+ id: 'stars',
279
+ },
300
280
  };
301
281
 
302
282
  if (show.includes('contributions')) {
@@ -445,88 +425,96 @@ const renderStatsCard = (
445
425
  };
446
426
  }
447
427
 
448
- const isLongLocale = locale ? LONG_LOCALES.includes(locale) : false;
428
+ const isLongLocale = locale ? LONG_LOCALES.has(locale) : false;
449
429
 
450
- // check if all used labels are short
451
- const longLabels = Object.entries(STATS)
452
- .filter(([key]) => !hide.includes(key))
453
- .some(([, stat]) => stat.label.length > 18);
454
-
455
- // filter out hidden stats defined by user & create the text nodes
456
- const statItems = Object.entries(STATS)
457
- .filter(([key]) => !hide.includes(key))
458
- // pass index so that we can calculate the line spacing
459
- .map(([, stat], index) =>
460
- createTextNode({
461
- icon: stat.icon,
462
- label: stat.label,
463
- value: stat.value,
464
- id: stat.id,
465
- unitSymbol: stat.unitSymbol,
466
- index,
467
- showIcons: show_icons,
468
- shiftValuePos: 29.01 + (longLabels ? 50 : 0) + (isLongLocale ? 50 : 0),
469
- bold: text_bold,
470
- numberFormat: number_format,
471
- numberPrecision: number_precision,
472
- link: stat.link,
473
- }),
474
- );
430
+ // filter out hidden stats defined by user
431
+ const visibleStats = Object.entries(STATS).filter(([key]) => !hide.includes(key));
475
432
 
476
- if (statItems.length === 0 && hide_rank) {
433
+ if (visibleStats.length === 0 && hide_rank) {
477
434
  throw new CardError('Could not render stats card.', {
478
435
  code: 'invalid_param',
479
436
  secondaryMessage: 'Either stats or rank are required.',
480
437
  });
481
438
  }
482
439
 
483
- // Calculate the card height depending on how many items there are
484
- // but if rank circle is visible clamp the minimum height to `150`
440
+ // check if all used labels are short
441
+ const longLabels = visibleStats.some(([, stat]) => stat.label.length > 18);
442
+
443
+ // The rank ring sets the floor: 150 beside the stats, 180 when it is the whole card.
485
444
  const height = Math.max(
486
- 45 + (statItems.length + 1) * lheight,
487
- hide_rank ? 0 : statItems.length ? 150 : 180,
445
+ 45 + (visibleStats.length + 1) * lheight,
446
+ hide_rank ? 0 : visibleStats.length > 0 ? 150 : 180,
488
447
  );
489
448
 
490
449
  // the lower the user's percentile the better
491
450
  const progress = 100 - rank.percentile;
492
451
 
493
- const calculateTextWidth = () => {
494
- return measureText(
495
- custom_title
496
- ? custom_title
497
- : statItems.length
498
- ? i18n.t('statcard.title')
499
- : i18n.t('statcard.ranktitle'),
452
+ const calculateTextWidth = (): number =>
453
+ measureText(
454
+ custom_title ||
455
+ (visibleStats.length > 0 ? i18n.t('statcard.title') : i18n.t('statcard.ranktitle')),
500
456
  );
501
- };
502
457
 
503
458
  /*
504
459
  When hide_rank=true, the minimum card width is 270 px + the title length and padding.
505
460
  When hide_rank=false, the minimum card_width is 340 px + the icon width (if show_icons=true).
506
461
  Numbers are picked by looking at existing dimensions on production.
507
462
  */
508
- const iconWidth = show_icons && statItems.length ? 16 + /* padding */ 1 : 0;
463
+ const iconWidth = show_icons && visibleStats.length > 0 ? 16 + /* padding */ 1 : 0;
509
464
  const minCardWidth =
510
465
  (hide_rank
511
466
  ? clampValue(50 /* padding */ + calculateTextWidth() * 2, CARD_MIN_WIDTH, Infinity)
512
- : statItems.length
467
+ : visibleStats.length > 0
513
468
  ? RANK_CARD_MIN_WIDTH
514
469
  : RANK_ONLY_CARD_MIN_WIDTH) + iconWidth;
515
470
  const defaultCardWidth =
516
471
  (hide_rank
517
472
  ? CARD_DEFAULT_WIDTH
518
- : statItems.length
473
+ : visibleStats.length > 0
519
474
  ? RANK_CARD_DEFAULT_WIDTH
520
475
  : RANK_ONLY_CARD_DEFAULT_WIDTH) + iconWidth;
521
476
  const width = card_width
522
- ? isNaN(card_width)
477
+ ? Number.isNaN(card_width)
523
478
  ? Math.max(defaultCardWidth, minCardWidth)
524
479
  : card_width
525
480
  : Math.max(defaultCardWidth, minCardWidth);
526
481
 
482
+ // A value ends at the card's inner edge, clear of the rank ring — or right after
483
+ // the longest label, on a card too narrow for that.
484
+ const widestLabel =
485
+ visibleStats.length > 0
486
+ ? Math.max(...visibleStats.map(([, stat]) => measureText(`${stat.label}:`, STAT_FONT_SIZE)))
487
+ : 0;
488
+ const valueAnchorX = Math.round(
489
+ Math.max(
490
+ width - CARD_PADDING_X - STAT_ROW_X - (hide_rank ? 0 : RANK_GUTTER),
491
+ (show_icons ? LABEL_X_OFFSET : 0) + widestLabel + LABEL_VALUE_GAP,
492
+ ),
493
+ );
494
+
495
+ // pass index so that we can calculate the line spacing
496
+ const statItems = visibleStats.map(([, stat], index) =>
497
+ createTextNode({
498
+ icon: stat.icon,
499
+ label: stat.label,
500
+ value: stat.value,
501
+ id: stat.id,
502
+ unitSymbol: stat.unitSymbol,
503
+ index,
504
+ showIcons: show_icons,
505
+ shiftValuePos: 29.01 + (longLabels ? 50 : 0) + (isLongLocale ? 50 : 0),
506
+ valueAnchorX,
507
+ bold: text_bold,
508
+ labelBold: false,
509
+ numberFormat: number_format,
510
+ numberPrecision: number_precision,
511
+ link: stat.link,
512
+ }),
513
+ );
514
+
527
515
  const card = new Card({
528
516
  customTitle: custom_title,
529
- defaultTitle: statItems.length ? i18n.t('statcard.title') : i18n.t('statcard.ranktitle'),
517
+ defaultTitle: visibleStats.length > 0 ? i18n.t('statcard.title') : i18n.t('statcard.ranktitle'),
530
518
  width,
531
519
  height,
532
520
  border_radius,
@@ -535,7 +523,7 @@ const renderStatsCard = (
535
523
 
536
524
  card.setHideBorder(hide_border);
537
525
  card.setHideTitle(hide_title);
538
- const cardStyles = ({ ringColor, textColor, iconColor }: CardColors): string =>
526
+ const cardStyles = ({ ringColor, textColor, iconColor }: CardColors): Array<CssChild> =>
539
527
  getStyles({ ringColor, textColor, iconColor, show_icons, progress });
540
528
 
541
529
  card.setCSS({ light: cardStyles, dark: cardStyles });
@@ -555,34 +543,33 @@ const renderStatsCard = (
555
543
  * @returns Rank circle translation value.
556
544
  */
557
545
  const calculateRankXTranslation = (): number => {
558
- if (statItems.length) {
546
+ if (visibleStats.length > 0) {
559
547
  const minXTranslation = RANK_CARD_MIN_WIDTH + iconWidth - 70;
560
548
  if (width > RANK_CARD_DEFAULT_WIDTH) {
561
549
  const xMaxExpansion = minXTranslation + (450 - minCardWidth) / 2;
562
550
  return xMaxExpansion + width - RANK_CARD_DEFAULT_WIDTH;
563
- } else {
564
- return minXTranslation + (width - minCardWidth) / 2;
565
551
  }
566
- } else {
567
- return width / 2 + 20 - 10;
552
+ return minXTranslation + (width - minCardWidth) / 2;
568
553
  }
554
+ return width / 2 + 20 - 10;
569
555
  };
570
556
 
571
557
  // Conditionally rendered elements
572
- const rankCircle = hide_rank
573
- ? ''
574
- : `<g data-testid="rank-circle"
575
- transform="translate(${calculateRankXTranslation()}, ${height / 2 - 50})">
576
- <circle class="rank-circle-rim" cx="-10" cy="8" r="40" />
577
- <circle class="rank-circle" cx="-10" cy="8" r="40" />
578
- <g class="rank-text">
579
- ${rankIcon(rank_icon, rank.level, rank.percentile)}
580
- </g>
581
- </g>`;
558
+ const rankCircle =
559
+ !hide_rank &&
560
+ el(
561
+ 'g',
562
+ {
563
+ 'data-testid': 'rank-circle',
564
+ transform: `translate(${calculateRankXTranslation()}, ${height / 2 - 50})`,
565
+ },
566
+ el('circle', { class: 'rank-circle-rim', cx: -10, cy: 8, r: 40 }),
567
+ el('circle', { class: 'rank-circle', cx: -10, cy: 8, r: 40 }),
568
+ el('g', { class: 'rank-text' }, rankIcon(rank_icon, rank.level, rank.percentile)),
569
+ );
582
570
 
583
571
  // Accessibility Labels
584
- const labels = Object.entries(STATS)
585
- .filter(([key]) => !hide.includes(key))
572
+ const labels = visibleStats
586
573
  .map(([key, stat]) => {
587
574
  if (key === 'commits') {
588
575
  return `${i18n.t('statcard.commits')} ${getTotalCommitsYearLabel(
@@ -600,16 +587,10 @@ const renderStatsCard = (
600
587
  desc: labels,
601
588
  });
602
589
 
603
- return card.render(`
604
- ${rankCircle}
605
- <svg x="0" y="0">
606
- ${flexLayout({
607
- items: statItems,
608
- gap: lheight,
609
- direction: 'column',
610
- }).join('')}
611
- </svg>
612
- `);
590
+ return card.render([
591
+ rankCircle,
592
+ el('svg', { x: 0, y: 0 }, flexLayout({ items: statItems, gap: lheight, direction: 'column' })),
593
+ ]);
613
594
  };
614
595
 
615
596
  export { RANK_ICONS, renderStatsCard };