@stats-forge/github-stats-forge-core 0.0.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) 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/contributed-to.d.ts +23 -0
  6. package/build/api/contributed-to.d.ts.map +1 -0
  7. package/build/api/contributed-to.js +43 -0
  8. package/build/api/gist.d.ts +8 -22
  9. package/build/api/gist.d.ts.map +1 -1
  10. package/build/api/gist.js +14 -36
  11. package/build/api/handler.d.ts +16 -0
  12. package/build/api/handler.d.ts.map +1 -0
  13. package/build/api/handler.js +29 -0
  14. package/build/api/index.d.ts +13 -12
  15. package/build/api/index.d.ts.map +1 -1
  16. package/build/api/index.js +1 -0
  17. package/build/api/params.d.ts +21 -12
  18. package/build/api/params.d.ts.map +1 -1
  19. package/build/api/params.js +41 -19
  20. package/build/api/pin.d.ts +11 -31
  21. package/build/api/pin.d.ts.map +1 -1
  22. package/build/api/pin.js +35 -63
  23. package/build/api/stats.d.ts +15 -16
  24. package/build/api/stats.d.ts.map +1 -1
  25. package/build/api/stats.js +60 -99
  26. package/build/api/top-langs.d.ts +13 -18
  27. package/build/api/top-langs.d.ts.map +1 -1
  28. package/build/api/top-langs.js +35 -72
  29. package/build/api/wakatime.d.ts +11 -17
  30. package/build/api/wakatime.d.ts.map +1 -1
  31. package/build/api/wakatime.js +27 -64
  32. package/build/calculateRank.d.ts +0 -9
  33. package/build/calculateRank.d.ts.map +1 -1
  34. package/build/calculateRank.js +12 -17
  35. package/build/cards/contributed-to.d.ts +19 -0
  36. package/build/cards/contributed-to.d.ts.map +1 -0
  37. package/build/cards/contributed-to.js +242 -0
  38. package/build/cards/gist.d.ts +3 -4
  39. package/build/cards/gist.d.ts.map +1 -1
  40. package/build/cards/gist.js +38 -43
  41. package/build/cards/options.d.ts +1 -1
  42. package/build/cards/repo.d.ts +14 -5
  43. package/build/cards/repo.d.ts.map +1 -1
  44. package/build/cards/repo.js +93 -103
  45. package/build/cards/stats.d.ts +18 -11
  46. package/build/cards/stats.d.ts.map +1 -1
  47. package/build/cards/stats.js +191 -222
  48. package/build/cards/top-languages.d.ts +18 -33
  49. package/build/cards/top-languages.d.ts.map +1 -1
  50. package/build/cards/top-languages.js +196 -387
  51. package/build/cards/wakatime.d.ts +17 -8
  52. package/build/cards/wakatime.d.ts.map +1 -1
  53. package/build/cards/wakatime.js +133 -187
  54. package/build/common/Card.d.ts +20 -59
  55. package/build/common/Card.d.ts.map +1 -1
  56. package/build/common/Card.js +131 -198
  57. package/build/common/I18n.d.ts +1 -7
  58. package/build/common/I18n.d.ts.map +1 -1
  59. package/build/common/I18n.js +7 -8
  60. package/build/common/brand.d.ts +76 -0
  61. package/build/common/brand.d.ts.map +1 -0
  62. package/build/common/brand.js +73 -0
  63. package/build/common/color.d.ts +4 -17
  64. package/build/common/color.d.ts.map +1 -1
  65. package/build/common/color.js +67 -49
  66. package/build/common/config.d.ts +4 -14
  67. package/build/common/config.d.ts.map +1 -1
  68. package/build/common/config.js +3 -16
  69. package/build/common/constants.d.ts +6 -2
  70. package/build/common/constants.d.ts.map +1 -1
  71. package/build/common/constants.js +5 -1
  72. package/build/common/date.d.ts +50 -5
  73. package/build/common/date.d.ts.map +1 -1
  74. package/build/common/date.js +104 -4
  75. package/build/common/emojiMap.d.ts +8 -0
  76. package/build/common/emojiMap.d.ts.map +1 -0
  77. package/build/common/emojiMap.js +10 -0
  78. package/build/common/emojiMap.json +1915 -0
  79. package/build/common/error.d.ts +1 -10
  80. package/build/common/error.d.ts.map +1 -1
  81. package/build/common/error.js +1 -10
  82. package/build/common/fmt.d.ts +3 -15
  83. package/build/common/fmt.d.ts.map +1 -1
  84. package/build/common/fmt.js +7 -35
  85. package/build/common/http.d.ts +2 -7
  86. package/build/common/http.d.ts.map +1 -1
  87. package/build/common/http.js +8 -17
  88. package/build/common/icons.d.ts +20 -20
  89. package/build/common/icons.d.ts.map +1 -1
  90. package/build/common/icons.js +108 -67
  91. package/build/common/languageColors.d.ts +0 -1
  92. package/build/common/languageColors.d.ts.map +1 -1
  93. package/build/common/languageColors.js +1 -4
  94. package/build/common/languageColors.json +18 -2
  95. package/build/common/ops.d.ts +11 -17
  96. package/build/common/ops.d.ts.map +1 -1
  97. package/build/common/ops.js +22 -39
  98. package/build/common/render.d.ts +34 -82
  99. package/build/common/render.d.ts.map +1 -1
  100. package/build/common/render.js +225 -259
  101. package/build/common/retryer.d.ts +2 -7
  102. package/build/common/retryer.d.ts.map +1 -1
  103. package/build/common/retryer.js +4 -11
  104. package/build/fetchers/contributed-to.d.ts +62 -0
  105. package/build/fetchers/contributed-to.d.ts.map +1 -0
  106. package/build/fetchers/contributed-to.js +184 -0
  107. package/build/fetchers/gist.d.ts +2 -5
  108. package/build/fetchers/gist.d.ts.map +1 -1
  109. package/build/fetchers/gist.js +1 -5
  110. package/build/fetchers/graphql-error.d.ts +13 -0
  111. package/build/fetchers/graphql-error.d.ts.map +1 -0
  112. package/build/fetchers/graphql-error.js +29 -0
  113. package/build/fetchers/index.d.ts +13 -12
  114. package/build/fetchers/index.d.ts.map +1 -1
  115. package/build/fetchers/index.js +1 -0
  116. package/build/fetchers/repo.d.ts +2 -11
  117. package/build/fetchers/repo.d.ts.map +1 -1
  118. package/build/fetchers/repo.js +20 -33
  119. package/build/fetchers/stats.d.ts +16 -40
  120. package/build/fetchers/stats.d.ts.map +1 -1
  121. package/build/fetchers/stats.js +89 -271
  122. package/build/fetchers/top-languages.d.ts +2 -9
  123. package/build/fetchers/top-languages.d.ts.map +1 -1
  124. package/build/fetchers/top-languages.js +16 -47
  125. package/build/fetchers/types.d.ts +25 -1
  126. package/build/fetchers/types.d.ts.map +1 -1
  127. package/build/fetchers/wakatime.d.ts +2 -6
  128. package/build/fetchers/wakatime.d.ts.map +1 -1
  129. package/build/fetchers/wakatime.js +1 -5
  130. package/build/graphql/contributionsCollection.d.ts +9 -0
  131. package/build/graphql/contributionsCollection.d.ts.map +1 -0
  132. package/build/graphql/contributionsCollection.js +12 -0
  133. package/build/graphql/contributionsDocument.d.ts +6 -6
  134. package/build/graphql/contributionsDocument.d.ts.map +1 -1
  135. package/build/graphql/contributionsDocument.js +8 -16
  136. package/build/graphql/generated/contributed-to.d.ts +22 -0
  137. package/build/graphql/generated/contributed-to.d.ts.map +1 -0
  138. package/build/graphql/generated/contributed-to.js +11 -0
  139. package/build/graphql/generated/gist.d.ts +1 -1
  140. package/build/graphql/generated/repo.d.ts +1 -1
  141. package/build/graphql/generated/stats.d.ts +16 -4
  142. package/build/graphql/generated/stats.d.ts.map +1 -1
  143. package/build/graphql/generated/stats.js +2 -2
  144. package/build/graphql/generated/top-languages.d.ts +2 -2
  145. package/build/graphql/graphqlDocument.d.ts +1 -2
  146. package/build/graphql/graphqlDocument.d.ts.map +1 -1
  147. package/build/graphql/graphqlDocument.js +0 -1
  148. package/build/graphql/reposContributedToDocument.d.ts +4 -5
  149. package/build/graphql/reposContributedToDocument.d.ts.map +1 -1
  150. package/build/graphql/reposContributedToDocument.js +11 -7
  151. package/build/markup/css.d.ts +35 -0
  152. package/build/markup/css.d.ts.map +1 -0
  153. package/build/markup/css.js +64 -0
  154. package/build/markup/element.d.ts +41 -0
  155. package/build/markup/element.d.ts.map +1 -0
  156. package/build/markup/element.js +127 -0
  157. package/build/markup/html.d.ts +13 -0
  158. package/build/markup/html.d.ts.map +1 -0
  159. package/build/markup/html.js +27 -0
  160. package/build/markup/index.d.ts +9 -0
  161. package/build/markup/index.d.ts.map +1 -0
  162. package/build/markup/index.js +6 -0
  163. package/build/markup/serialize.d.ts +16 -0
  164. package/build/markup/serialize.d.ts.map +1 -0
  165. package/build/markup/serialize.js +17 -0
  166. package/build/themes/index.d.ts +332 -333
  167. package/build/themes/index.d.ts.map +1 -1
  168. package/build/themes/index.js +1 -1
  169. package/build/translations.d.ts +66 -7
  170. package/build/translations.d.ts.map +1 -1
  171. package/build/translations.js +686 -629
  172. package/package.json +17 -26
  173. package/src/api/api-result.ts +10 -9
  174. package/src/api/contributed-to.ts +84 -0
  175. package/src/api/gist.ts +22 -46
  176. package/src/api/handler.ts +42 -0
  177. package/src/api/index.ts +13 -12
  178. package/src/api/params.ts +74 -29
  179. package/src/api/pin.ts +38 -65
  180. package/src/api/stats.ts +81 -118
  181. package/src/api/top-langs.ts +38 -80
  182. package/src/api/wakatime.ts +35 -73
  183. package/src/calculateRank.ts +12 -23
  184. package/src/cards/contributed-to.ts +373 -0
  185. package/src/cards/gist.ts +55 -49
  186. package/src/cards/options.ts +1 -1
  187. package/src/cards/repo.ts +134 -120
  188. package/src/cards/stats.ts +262 -252
  189. package/src/cards/top-languages.ts +315 -422
  190. package/src/cards/wakatime.ts +207 -225
  191. package/src/common/Card.ts +207 -214
  192. package/src/common/I18n.ts +7 -8
  193. package/src/common/brand.ts +109 -0
  194. package/src/common/color.ts +80 -57
  195. package/src/common/config.ts +5 -18
  196. package/src/common/constants.ts +8 -2
  197. package/src/common/date.ts +135 -5
  198. package/src/common/emojiMap.json +1915 -0
  199. package/src/common/emojiMap.ts +13 -0
  200. package/src/common/error.ts +2 -11
  201. package/src/common/fmt.ts +7 -44
  202. package/src/common/http.ts +9 -15
  203. package/src/common/icons.ts +122 -42
  204. package/src/common/languageColors.json +18 -2
  205. package/src/common/languageColors.ts +1 -4
  206. package/src/common/ops.ts +31 -48
  207. package/src/common/render.ts +334 -284
  208. package/src/common/retryer.ts +8 -15
  209. package/src/fetchers/contributed-to.ts +271 -0
  210. package/src/fetchers/gist.ts +8 -12
  211. package/src/fetchers/graphql-error.ts +36 -0
  212. package/src/fetchers/index.ts +15 -12
  213. package/src/fetchers/repo.ts +27 -40
  214. package/src/fetchers/stats.ts +157 -366
  215. package/src/fetchers/top-languages.ts +27 -55
  216. package/src/fetchers/types.ts +27 -1
  217. package/src/fetchers/wakatime.ts +5 -9
  218. package/src/graphql/contributionsCollection.ts +18 -0
  219. package/src/graphql/contributionsDocument.ts +14 -19
  220. package/src/graphql/generated/contributed-to.ts +25 -0
  221. package/src/graphql/generated/gist.ts +1 -1
  222. package/src/graphql/generated/repo.ts +1 -1
  223. package/src/graphql/generated/stats.ts +21 -8
  224. package/src/graphql/generated/top-languages.ts +2 -2
  225. package/src/graphql/graphqlDocument.ts +1 -2
  226. package/src/graphql/queries/contributed-to.graphql +8 -0
  227. package/src/graphql/queries/stats.graphql +14 -2
  228. package/src/graphql/reposContributedToDocument.ts +16 -15
  229. package/src/markup/css.ts +118 -0
  230. package/src/markup/element.ts +246 -0
  231. package/src/markup/html.ts +34 -0
  232. package/src/markup/index.ts +9 -0
  233. package/src/markup/serialize.ts +27 -0
  234. package/src/themes/index.ts +1 -1
  235. package/src/translations.ts +699 -629
  236. package/build/cards/index.d.ts +0 -11
  237. package/build/cards/index.d.ts.map +0 -1
  238. package/build/cards/index.js +0 -7
  239. package/build/common/html.d.ts +0 -8
  240. package/build/common/html.d.ts.map +0 -1
  241. package/build/common/html.js +0 -14
  242. package/build/index.d.ts +0 -15
  243. package/build/index.d.ts.map +0 -1
  244. package/build/index.js +0 -10
  245. package/src/_emoji-name-map.d.ts +0 -10
  246. package/src/_github-username-regex.d.ts +0 -4
  247. package/src/cards/index.ts +0 -21
  248. package/src/common/html.ts +0 -17
  249. package/src/index.ts +0 -18
@@ -1,21 +1,52 @@
1
+ import { CARD_ICON, CARD_WIDTH, FONT_SIZE, FONT_WEIGHT, firefoxFontSize, font, } from '../common/brand.js';
1
2
  import { Card } from '../common/Card.js';
2
3
  import { getLightDarkColors } from '../common/color.js';
4
+ import { formatRange } from '../common/date.js';
3
5
  import { CardError } from '../common/error.js';
4
6
  import { I18n } from '../common/I18n.js';
5
7
  import { icons, rankIcon } from '../common/icons.js';
6
8
  import { buildSearchFilter, clampValue } from '../common/ops.js';
7
- import { createTextNode, flexLayout, measureText } from '../common/render.js';
9
+ import { NUMBER_FORMATS, createTextNode, flexLayout, measureText } from '../common/render.js';
10
+ import { atRule, cssComment, el, rule } from '../markup/index.js';
8
11
  import { statCardLocales, wakatimeCardLocales } from '../translations.js';
9
12
  const CARD_MIN_WIDTH = 287;
10
- const CARD_DEFAULT_WIDTH = 287;
11
- const RANK_CARD_MIN_WIDTH = 420;
12
- const RANK_CARD_DEFAULT_WIDTH = 450;
13
- const RANK_ONLY_CARD_MIN_WIDTH = 290;
14
- const RANK_ONLY_CARD_DEFAULT_WIDTH = 290;
13
+ const CARD_DEFAULT_WIDTH = CARD_WIDTH.compact;
14
+ const RANK_CARD_DEFAULT_WIDTH = CARD_WIDTH.wide;
15
+ const RANK_ONLY_CARD_DEFAULT_WIDTH = CARD_WIDTH.compact;
16
+ const STAT_FONT_SIZE = FONT_SIZE.body;
17
+ /** Padding the card keeps at its edges; matches `Card`'s own `paddingX`. */
18
+ const CARD_PADDING_X = 25;
19
+ /** How far a stat row is translated into the card; see `createTextNode`. */
20
+ const STAT_ROW_X = 25;
21
+ /** Room the rank ring needs at the right edge, so a value never runs into it. */
22
+ const RANK_GUTTER = 120;
23
+ /** The ring's own `cx`, which its group's translation has to make up for. */
24
+ const RANK_CIRCLE_CX_OFFSET = 10;
25
+ /** `createTextNode`'s own label offset, which it applies only when icons are shown. */
26
+ const LABEL_X_OFFSET = 25;
27
+ /** Smallest gap kept between the longest label and its value. */
28
+ const LABEL_VALUE_GAP = 16;
15
29
  /** Rank indicators the card can draw; the api validates `rank_icon` against this. */
16
30
  const RANK_ICONS = ['default', 'github', 'percentile'];
31
+ /** Stats the card draws only when `show` names them. */
32
+ const SHOW_STATS = [
33
+ 'contributions',
34
+ 'prs_merged',
35
+ 'prs_merged_percentage',
36
+ 'reviews',
37
+ 'discussions_started',
38
+ 'discussions_answered',
39
+ 'prs_authored',
40
+ 'prs_commented',
41
+ 'prs_reviewed',
42
+ 'issues_authored',
43
+ 'issues_commented',
44
+ 'all_time_contribs',
45
+ ];
46
+ /** Stats the card always draws, and so the ones `hide` has anything to remove. */
47
+ const HIDE_STATS = ['stars', 'commits', 'prs', 'issues', 'contribs'];
17
48
  /** Long locales that need more space for text. Keep sorted alphabetically. */
18
- const LONG_LOCALES = [
49
+ const LONG_LOCALES = new Set([
19
50
  'az',
20
51
  'bg',
21
52
  'cs',
@@ -42,134 +73,92 @@ const LONG_LOCALES = [
42
73
  'uk-ua',
43
74
  'uz',
44
75
  'zh-tw',
45
- ];
76
+ ]);
46
77
  /**
47
78
  * Calculates progress along the boundary of the circle, i.e. its circumference.
48
79
  *
49
- * @param value The rank value to calculate progress for.
50
80
  * @returns Progress value.
51
81
  */
52
82
  const calculateCircleProgress = (value) => {
53
83
  const radius = 40;
54
- const c = Math.PI * (radius * 2);
55
- if (value < 0) {
56
- value = 0;
57
- }
58
- if (value > 100) {
59
- value = 100;
60
- }
61
- return ((100 - value) / 100) * c;
84
+ const circumference = Math.PI * (radius * 2);
85
+ const clamped = Math.min(Math.max(value, 0), 100);
86
+ return ((100 - clamped) / 100) * circumference;
62
87
  };
63
88
  /**
64
89
  * Retrieves the animation to display progress along the circumference of circle
65
90
  * from the beginning to the given value in a clockwise direction.
66
91
  *
67
- * @param props The props object.
68
- * @param props.progress The progress value to animate to.
69
92
  * @returns Progress animation css.
70
93
  */
71
- const getProgressAnimation = ({ progress }) => {
72
- return `
73
- @keyframes rankAnimation {
74
- from {
75
- stroke-dashoffset: ${calculateCircleProgress(0)};
76
- }
77
- to {
78
- stroke-dashoffset: ${calculateCircleProgress(progress)};
79
- }
80
- }
81
- `;
82
- };
94
+ const getProgressAnimation = ({ progress }) => atRule('@keyframes rankAnimation', rule('from', { 'stroke-dashoffset': calculateCircleProgress(0) }), rule('to', { 'stroke-dashoffset': calculateCircleProgress(progress) }));
83
95
  /**
84
96
  * Retrieves CSS styles for a card.
85
97
  *
86
- * @param colors The colors to use for the card.
87
- * @param colors.textColor The text color.
88
- * @param colors.iconColor The icon color.
89
- * @param colors.ringColor The ring color.
90
- * @param colors.show_icons Whether to show icons.
91
- * @param colors.progress The progress value to animate to.
92
98
  * @returns Card CSS styles.
93
99
  */
94
- const getStyles = ({ textColor, iconColor, ringColor, show_icons, progress, }) => {
95
- return `
96
- .stat {
97
- font: 600 14px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif; fill: ${textColor};
98
- }
99
- @supports(-moz-appearance: auto) {
100
- /* Selector detects Firefox */
101
- .stat { font-size:12px; }
102
- }
103
- .stagger {
104
- opacity: 0;
105
- animation: fadeInAnimation 0.3s ease-in-out forwards;
106
- }
107
- .rank-text {
108
- font: 800 24px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor};
109
- animation: scaleInAnimation 0.3s ease-in-out forwards;
110
- }
111
- .rank-percentile-header {
112
- font-size: 14px;
113
- }
114
- .rank-percentile-text {
115
- font-size: 16px;
116
- }
117
-
118
- .not_bold { font-weight: 400 }
119
- .bold { font-weight: 700 }
120
- .icon {
121
- fill: ${iconColor};
122
- display: ${show_icons ? 'block' : 'none'};
123
- }
124
-
125
- .rank-circle-rim {
126
- stroke: ${ringColor};
127
- fill: none;
128
- stroke-width: 6;
129
- opacity: 0.2;
130
- }
131
- .rank-circle {
132
- stroke: ${ringColor};
133
- stroke-dasharray: 250;
134
- fill: none;
135
- stroke-width: 6;
136
- stroke-linecap: round;
137
- opacity: 0.8;
138
- transform-origin: -10px 8px;
139
- transform: rotate(-90deg);
140
- animation: rankAnimation 1s forwards ease-in-out;
141
- }
142
- ${getProgressAnimation({ progress })}
143
- `;
144
- };
100
+ const getStyles = ({ textColor, iconColor, ringColor, show_icons, progress, }) => [
101
+ rule('.stat', {
102
+ font: font('regular', 'body'),
103
+ fill: textColor,
104
+ 'font-variant-numeric': 'tabular-nums',
105
+ }),
106
+ firefoxFontSize(['.stat'], 'small'),
107
+ rule('.stagger', { opacity: 0, animation: 'fadeInAnimation 0.3s ease-in-out forwards' }),
108
+ rule('.rank-text', {
109
+ font: font('bold', 'display'),
110
+ fill: textColor,
111
+ animation: 'scaleInAnimation 0.3s ease-in-out forwards',
112
+ }),
113
+ rule('.rank-percentile-header', { 'font-size': `${String(FONT_SIZE.body)}px` }),
114
+ rule('.rank-percentile-text', { 'font-size': `${String(FONT_SIZE.lead)}px` }),
115
+ cssComment('Labels recede so that the values read first.'),
116
+ rule('.not_bold', { 'font-weight': FONT_WEIGHT.regular, opacity: 0.75 }),
117
+ rule('.bold', { 'font-weight': FONT_WEIGHT.semibold }),
118
+ rule('.icon', {
119
+ fill: iconColor,
120
+ opacity: 0.75,
121
+ display: show_icons ? 'block' : 'none',
122
+ }),
123
+ rule('.rank-circle-rim', {
124
+ stroke: ringColor,
125
+ fill: 'none',
126
+ 'stroke-width': 4,
127
+ opacity: 0.15,
128
+ }),
129
+ rule('.rank-circle', {
130
+ stroke: ringColor,
131
+ 'stroke-dasharray': 250,
132
+ fill: 'none',
133
+ 'stroke-width': 4,
134
+ 'stroke-linecap': 'round',
135
+ opacity: 1,
136
+ 'transform-origin': '-10px 8px',
137
+ transform: 'rotate(-90deg)',
138
+ animation: 'rankAnimation 0.8s forwards ease-in-out',
139
+ }),
140
+ getProgressAnimation({ progress }),
141
+ ];
145
142
  /**
146
- * Return the label for commits according to the selected options
147
- *
148
- * @param include_all_commits Option to include all years
149
- * @param commits_year Option to include only selected year
150
- * @param i18n The I18n instance.
151
- * @returns The label corresponding to the options.
143
+ * @returns What the commit count covers, for the stat's label.
152
144
  */
153
- const getTotalCommitsYearLabel = (include_all_commits, commits_year, i18n) => include_all_commits
145
+ const getTotalCommitsRangeLabel = (include_all_commits, commitsRange, i18n) => include_all_commits
154
146
  ? ''
155
- : commits_year
156
- ? ` (${commits_year})`
147
+ : commitsRange
148
+ ? ` (${formatRange(commitsRange)})`
157
149
  : ` (${i18n.t('wakatimecard.lastyear')})`;
158
150
  /**
159
151
  * Renders the stats card.
160
152
  *
161
- * @param stats The stats data.
162
- * @param options The card options.
163
- * @param username GitHub username, used to build stat search links.
164
- * @param repo Repositories to scope the search links to.
165
- * @param owner Owners to scope the search links to.
166
153
  * @returns The stats card SVG object.
167
154
  */
168
- const renderStatsCard = (stats, options = {}, username, repo = [], owner = []) => {
169
- const { name, totalStars, totalCommits, totalIssues, totalPRs, totalPRsMerged, mergedPRsPercentage, totalReviews, totalDiscussionsStarted, totalDiscussionsAnswered, contributedTo, allTimeContributedTo, totalPRsAuthored, totalPRsCommented, totalPRsReviewed, totalIssuesAuthored, totalIssuesCommented, totalContributions, rank, } = stats;
170
- const { hide = [], show_icons = false, hide_title = false, hide_border = false, card_width, hide_rank = false, include_all_commits = false, commits_year, line_height = 25, text_bold = true, custom_title, border_radius, number_format = 'short', number_precision, locale, disable_animations = false, rank_icon = 'default', show = [], } = options;
171
- const lheight = parseInt(String(line_height), 10);
155
+ const renderCard = (stats, options = {}, username, repo = [], owner = []) => {
156
+ const { name, totalStars, totalCommits, commitsRange, totalIssues, totalPRs, totalPRsMerged, mergedPRsPercentage, totalReviews, totalDiscussionsStarted, totalDiscussionsAnswered, contributedTo, allTimeContributedTo, totalPRsAuthored, totalPRsCommented, totalPRsReviewed, totalIssuesAuthored, totalIssuesCommented, totalContributions, rank, } = stats;
157
+ const { hide = [], show_icons = false, hide_title = false, hide_border = false, card_width, hide_rank = false, include_all_commits = false, line_height = 25, text_bold = true, custom_title, border_radius, number_format = 'short', number_precision, locale, disable_animations = false, rank_icon = 'default', show = [], } = options;
158
+ const lheight = Number.parseInt(String(line_height), 10);
172
159
  const { lightColors, darkColors } = getLightDarkColors(options);
160
+ // Typed against the exported list, so a stat drawn here cannot be missing from it.
161
+ const shows = (stat) => show.includes(stat);
173
162
  const apostrophe = /s$/i.test(name.trim()) ? '' : 's';
174
163
  const i18n = new I18n({
175
164
  locale,
@@ -179,14 +168,15 @@ const renderStatsCard = (stats, options = {}, username, repo = [], owner = []) =
179
168
  },
180
169
  });
181
170
  // Meta data for creating text nodes with createTextNode function
182
- const STATS = {};
183
- STATS['stars'] = {
184
- icon: icons.star,
185
- label: i18n.t('statcard.totalstars'),
186
- value: totalStars,
187
- id: 'stars',
171
+ const STATS = {
172
+ stars: {
173
+ icon: icons.star,
174
+ label: i18n.t('statcard.totalstars'),
175
+ value: totalStars,
176
+ id: 'stars',
177
+ },
188
178
  };
189
- if (show.includes('contributions')) {
179
+ if (shows('contributions')) {
190
180
  STATS['contributions'] = {
191
181
  icon: icons.contributions,
192
182
  label: i18n.t('statcard.contributions'),
@@ -196,7 +186,7 @@ const renderStatsCard = (stats, options = {}, username, repo = [], owner = []) =
196
186
  }
197
187
  STATS['commits'] = {
198
188
  icon: icons.commits,
199
- label: `${i18n.t('statcard.commits')}${getTotalCommitsYearLabel(include_all_commits, commits_year, i18n)}`,
189
+ label: `${i18n.t('statcard.commits')}${getTotalCommitsRangeLabel(include_all_commits, commitsRange, i18n)}`,
200
190
  value: totalCommits,
201
191
  id: 'commits',
202
192
  };
@@ -206,7 +196,7 @@ const renderStatsCard = (stats, options = {}, username, repo = [], owner = []) =
206
196
  value: totalPRs,
207
197
  id: 'prs',
208
198
  };
209
- if (show.includes('prs_merged')) {
199
+ if (shows('prs_merged')) {
210
200
  STATS['prs_merged'] = {
211
201
  icon: icons.prs_merged,
212
202
  label: i18n.t('statcard.prs-merged'),
@@ -214,7 +204,7 @@ const renderStatsCard = (stats, options = {}, username, repo = [], owner = []) =
214
204
  id: 'prs_merged',
215
205
  };
216
206
  }
217
- if (show.includes('prs_merged_percentage')) {
207
+ if (shows('prs_merged_percentage')) {
218
208
  STATS['prs_merged_percentage'] = {
219
209
  icon: icons.prs_merged_percentage,
220
210
  label: i18n.t('statcard.prs-merged-percentage'),
@@ -225,7 +215,7 @@ const renderStatsCard = (stats, options = {}, username, repo = [], owner = []) =
225
215
  unitSymbol: '%',
226
216
  };
227
217
  }
228
- if (show.includes('reviews')) {
218
+ if (shows('reviews')) {
229
219
  STATS['reviews'] = {
230
220
  icon: icons.reviews,
231
221
  label: i18n.t('statcard.reviews'),
@@ -239,7 +229,7 @@ const renderStatsCard = (stats, options = {}, username, repo = [], owner = []) =
239
229
  value: totalIssues,
240
230
  id: 'issues',
241
231
  };
242
- if (show.includes('discussions_started')) {
232
+ if (shows('discussions_started')) {
243
233
  STATS['discussions_started'] = {
244
234
  icon: icons.discussions_started,
245
235
  label: i18n.t('statcard.discussions-started'),
@@ -247,7 +237,7 @@ const renderStatsCard = (stats, options = {}, username, repo = [], owner = []) =
247
237
  id: 'discussions_started',
248
238
  };
249
239
  }
250
- if (show.includes('discussions_answered')) {
240
+ if (shows('discussions_answered')) {
251
241
  STATS['discussions_answered'] = {
252
242
  icon: icons.discussions_answered,
253
243
  label: i18n.t('statcard.discussions-answered'),
@@ -257,7 +247,7 @@ const renderStatsCard = (stats, options = {}, username, repo = [], owner = []) =
257
247
  }
258
248
  const repoFilter = encodeURIComponent(buildSearchFilter(repo, owner));
259
249
  const encodedUsername = encodeURIComponent(username ?? '');
260
- if (show.includes('prs_authored')) {
250
+ if (shows('prs_authored')) {
261
251
  STATS['prs_authored'] = {
262
252
  icon: icons.prs,
263
253
  label: i18n.t('statcard.prs-authored'),
@@ -266,7 +256,7 @@ const renderStatsCard = (stats, options = {}, username, repo = [], owner = []) =
266
256
  link: `https://github.com/search?q=${repoFilter}author%3A${encodedUsername}&amp;type=pullrequests`,
267
257
  };
268
258
  }
269
- if (show.includes('prs_commented')) {
259
+ if (shows('prs_commented')) {
270
260
  STATS['prs_commented'] = {
271
261
  icon: icons.comments,
272
262
  label: i18n.t('statcard.prs-commented'),
@@ -275,7 +265,7 @@ const renderStatsCard = (stats, options = {}, username, repo = [], owner = []) =
275
265
  link: `https://github.com/search?q=${repoFilter}commenter%3A${encodedUsername}+-author%3A${encodedUsername}&amp;type=pullrequests`,
276
266
  };
277
267
  }
278
- if (show.includes('prs_reviewed')) {
268
+ if (shows('prs_reviewed')) {
279
269
  STATS['prs_reviewed'] = {
280
270
  icon: icons.reviews,
281
271
  label: i18n.t('statcard.prs-reviewed'),
@@ -284,7 +274,7 @@ const renderStatsCard = (stats, options = {}, username, repo = [], owner = []) =
284
274
  link: `https://github.com/search?q=${repoFilter}reviewed-by%3A${encodedUsername}+-author%3A${encodedUsername}&amp;type=pullrequests`,
285
275
  };
286
276
  }
287
- if (show.includes('issues_authored')) {
277
+ if (shows('issues_authored')) {
288
278
  STATS['issues_authored'] = {
289
279
  icon: icons.issues,
290
280
  label: i18n.t('statcard.issues-authored'),
@@ -293,7 +283,7 @@ const renderStatsCard = (stats, options = {}, username, repo = [], owner = []) =
293
283
  link: `https://github.com/search?q=${repoFilter}author%3A${encodedUsername}&amp;type=issues`,
294
284
  };
295
285
  }
296
- if (show.includes('issues_commented')) {
286
+ if (shows('issues_commented')) {
297
287
  STATS['issues_commented'] = {
298
288
  icon: icons.discussions_started,
299
289
  label: i18n.t('statcard.issues-commented'),
@@ -303,29 +293,59 @@ const renderStatsCard = (stats, options = {}, username, repo = [], owner = []) =
303
293
  };
304
294
  }
305
295
  STATS['contribs'] = {
306
- icon: icons.contribs,
296
+ icon: icons.repo,
307
297
  label: i18n.t('statcard.contribs'),
308
298
  value: contributedTo,
309
299
  id: 'contribs',
310
300
  };
311
- if (show.includes('all_time_contribs')) {
301
+ if (shows('all_time_contribs')) {
312
302
  STATS['all_time_contribs'] = {
313
- icon: icons.contribs,
303
+ icon: icons.repo,
314
304
  label: i18n.t('statcard.all-time-contribs'),
315
305
  value: allTimeContributedTo,
316
306
  id: 'all_time_contribs',
317
307
  };
318
308
  }
319
- const isLongLocale = locale ? LONG_LOCALES.includes(locale) : false;
309
+ const isLongLocale = locale ? LONG_LOCALES.has(locale) : false;
310
+ // filter out hidden stats defined by user
311
+ const visibleStats = Object.entries(STATS).filter(([key]) => !hide.includes(key));
312
+ if (visibleStats.length === 0 && hide_rank) {
313
+ throw new CardError('Could not render stats card.', {
314
+ code: 'invalid_param',
315
+ secondaryMessage: 'Either stats or rank are required.',
316
+ });
317
+ }
320
318
  // check if all used labels are short
321
- const longLabels = Object.entries(STATS)
322
- .filter(([key]) => !hide.includes(key))
323
- .some(([, stat]) => stat.label.length > 18);
324
- // filter out hidden stats defined by user & create the text nodes
325
- const statItems = Object.entries(STATS)
326
- .filter(([key]) => !hide.includes(key))
327
- // pass index so that we can calculate the line spacing
328
- .map(([, stat], index) => createTextNode({
319
+ const longLabels = visibleStats.some(([, stat]) => stat.label.length > 18);
320
+ // The rank ring sets the floor: 150 beside the stats, 180 when it is the whole card.
321
+ const height = Math.max(45 + (visibleStats.length + 1) * lheight, hide_rank ? 0 : visibleStats.length > 0 ? 150 : 180);
322
+ // the lower the user's percentile the better
323
+ const progress = 100 - rank.percentile;
324
+ const calculateTextWidth = () => measureText(custom_title ||
325
+ (visibleStats.length > 0 ? i18n.t('statcard.title') : i18n.t('statcard.ranktitle')));
326
+ const iconWidth = show_icons && visibleStats.length > 0 ? 16 + /* padding */ 1 : 0;
327
+ // The icons ride inside the step's own slack, so a default card lands on the width grid;
328
+ // only the minimum, which has no slack, still has to make room for them.
329
+ const defaultCardWidth = hide_rank
330
+ ? CARD_DEFAULT_WIDTH
331
+ : visibleStats.length > 0
332
+ ? RANK_CARD_DEFAULT_WIDTH
333
+ : RANK_ONLY_CARD_DEFAULT_WIDTH;
334
+ // Only a card sized by its own title can outgrow its step: the ranked layouts are fixed,
335
+ // so their step is also their floor.
336
+ const fittedWidth = hide_rank
337
+ ? Math.max(defaultCardWidth, clampValue(50 /* padding */ + calculateTextWidth() * 2, CARD_MIN_WIDTH, Infinity) +
338
+ iconWidth)
339
+ : defaultCardWidth;
340
+ const width = card_width ? (Number.isNaN(card_width) ? fittedWidth : card_width) : fittedWidth;
341
+ // A value ends at the card's inner edge, clear of the rank ring — or right after
342
+ // the longest label, on a card too narrow for that.
343
+ const widestLabel = visibleStats.length > 0
344
+ ? Math.max(...visibleStats.map(([, stat]) => measureText(`${stat.label}:`, STAT_FONT_SIZE)))
345
+ : 0;
346
+ const valueAnchorX = Math.round(Math.max(width - CARD_PADDING_X - STAT_ROW_X - (hide_rank ? 0 : RANK_GUTTER), (show_icons ? LABEL_X_OFFSET : 0) + widestLabel + LABEL_VALUE_GAP));
347
+ // pass index so that we can calculate the line spacing
348
+ const statItems = visibleStats.map(([, stat], index) => createTextNode({
329
349
  icon: stat.icon,
330
350
  label: stat.label,
331
351
  value: stat.value,
@@ -334,53 +354,17 @@ const renderStatsCard = (stats, options = {}, username, repo = [], owner = []) =
334
354
  index,
335
355
  showIcons: show_icons,
336
356
  shiftValuePos: 29.01 + (longLabels ? 50 : 0) + (isLongLocale ? 50 : 0),
357
+ valueAnchorX,
337
358
  bold: text_bold,
359
+ labelBold: false,
338
360
  numberFormat: number_format,
339
361
  numberPrecision: number_precision,
340
362
  link: stat.link,
341
363
  }));
342
- if (statItems.length === 0 && hide_rank) {
343
- throw new CardError('Could not render stats card.', {
344
- code: 'invalid_param',
345
- secondaryMessage: 'Either stats or rank are required.',
346
- });
347
- }
348
- // Calculate the card height depending on how many items there are
349
- // but if rank circle is visible clamp the minimum height to `150`
350
- const height = Math.max(45 + (statItems.length + 1) * lheight, hide_rank ? 0 : statItems.length ? 150 : 180);
351
- // the lower the user's percentile the better
352
- const progress = 100 - rank.percentile;
353
- const calculateTextWidth = () => {
354
- return measureText(custom_title
355
- ? custom_title
356
- : statItems.length
357
- ? i18n.t('statcard.title')
358
- : i18n.t('statcard.ranktitle'));
359
- };
360
- /*
361
- When hide_rank=true, the minimum card width is 270 px + the title length and padding.
362
- When hide_rank=false, the minimum card_width is 340 px + the icon width (if show_icons=true).
363
- Numbers are picked by looking at existing dimensions on production.
364
- */
365
- const iconWidth = show_icons && statItems.length ? 16 + /* padding */ 1 : 0;
366
- const minCardWidth = (hide_rank
367
- ? clampValue(50 /* padding */ + calculateTextWidth() * 2, CARD_MIN_WIDTH, Infinity)
368
- : statItems.length
369
- ? RANK_CARD_MIN_WIDTH
370
- : RANK_ONLY_CARD_MIN_WIDTH) + iconWidth;
371
- const defaultCardWidth = (hide_rank
372
- ? CARD_DEFAULT_WIDTH
373
- : statItems.length
374
- ? RANK_CARD_DEFAULT_WIDTH
375
- : RANK_ONLY_CARD_DEFAULT_WIDTH) + iconWidth;
376
- const width = card_width
377
- ? isNaN(card_width)
378
- ? Math.max(defaultCardWidth, minCardWidth)
379
- : card_width
380
- : Math.max(defaultCardWidth, minCardWidth);
381
364
  const card = new Card({
382
365
  customTitle: custom_title,
383
- defaultTitle: statItems.length ? i18n.t('statcard.title') : i18n.t('statcard.ranktitle'),
366
+ defaultTitle: visibleStats.length > 0 ? i18n.t('statcard.title') : i18n.t('statcard.ranktitle'),
367
+ titlePrefixIcon: CARD_ICON.stats,
384
368
  width,
385
369
  height,
386
370
  border_radius,
@@ -394,64 +378,49 @@ const renderStatsCard = (stats, options = {}, username, repo = [], owner = []) =
394
378
  card.disableAnimations();
395
379
  }
396
380
  /**
397
- * Calculates the right rank circle translation values such that the rank circle
398
- * keeps respecting the following padding:
399
- *
400
- * width > RANK_CARD_DEFAULT_WIDTH: The default right padding of 70 px will be used.
401
- * width < RANK_CARD_DEFAULT_WIDTH: The left and right padding will be enlarged
402
- * equally from a certain minimum at RANK_CARD_MIN_WIDTH.
381
+ * The ring sits centred in the same gutter the values are kept clear of, so the gap
382
+ * between them holds at every width; a rank-only card has no values, so it centres.
403
383
  *
404
384
  * @returns Rank circle translation value.
405
385
  */
406
- const calculateRankXTranslation = () => {
407
- if (statItems.length) {
408
- const minXTranslation = RANK_CARD_MIN_WIDTH + iconWidth - 70;
409
- if (width > RANK_CARD_DEFAULT_WIDTH) {
410
- const xMaxExpansion = minXTranslation + (450 - minCardWidth) / 2;
411
- return xMaxExpansion + width - RANK_CARD_DEFAULT_WIDTH;
412
- }
413
- else {
414
- return minXTranslation + (width - minCardWidth) / 2;
415
- }
416
- }
417
- else {
418
- return width / 2 + 20 - 10;
419
- }
420
- };
386
+ const calculateRankXTranslation = () => visibleStats.length > 0
387
+ ? width - CARD_PADDING_X - RANK_GUTTER / 2 + RANK_CIRCLE_CX_OFFSET
388
+ : width / 2 + 20 - 10;
421
389
  // Conditionally rendered elements
422
- const rankCircle = hide_rank
423
- ? ''
424
- : `<g data-testid="rank-circle"
425
- transform="translate(${calculateRankXTranslation()}, ${height / 2 - 50})">
426
- <circle class="rank-circle-rim" cx="-10" cy="8" r="40" />
427
- <circle class="rank-circle" cx="-10" cy="8" r="40" />
428
- <g class="rank-text">
429
- ${rankIcon(rank_icon, rank.level, rank.percentile)}
430
- </g>
431
- </g>`;
390
+ const rankCircle = !hide_rank &&
391
+ el('g', {
392
+ 'data-testid': 'rank-circle',
393
+ transform: `translate(${calculateRankXTranslation()}, ${height / 2 - 50})`,
394
+ }, el('circle', { class: 'rank-circle-rim', cx: -RANK_CIRCLE_CX_OFFSET, cy: 8, r: 40 }), el('circle', { class: 'rank-circle', cx: -RANK_CIRCLE_CX_OFFSET, cy: 8, r: 40 }), el('g', { class: 'rank-text' }, rankIcon(rank_icon, rank.level, rank.percentile)));
432
395
  // Accessibility Labels
433
- const labels = Object.entries(STATS)
434
- .filter(([key]) => !hide.includes(key))
396
+ const labels = visibleStats
435
397
  .map(([key, stat]) => {
436
398
  if (key === 'commits') {
437
- return `${i18n.t('statcard.commits')} ${getTotalCommitsYearLabel(include_all_commits, commits_year, i18n)} : ${stat.value}`;
399
+ return `${i18n.t('statcard.commits')} ${getTotalCommitsRangeLabel(include_all_commits, commitsRange, i18n)} : ${stat.value}`;
438
400
  }
439
401
  return `${stat.label}: ${stat.value}`;
440
402
  })
441
403
  .join(', ');
442
404
  card.setAccessibilityLabel({
443
- title: `${card.title}, Rank: ${rank.level}`,
405
+ title: `${card.title}, ${i18n.t('statcard.rank')}: ${rank.level}`,
444
406
  desc: labels,
445
407
  });
446
- return card.render(`
447
- ${rankCircle}
448
- <svg x="0" y="0">
449
- ${flexLayout({
450
- items: statItems,
451
- gap: lheight,
452
- direction: 'column',
453
- }).join('')}
454
- </svg>
455
- `);
408
+ return card.render([
409
+ rankCircle,
410
+ el('svg', { x: 0, y: 0 }, flexLayout({ items: statItems, gap: lheight, direction: 'column' })),
411
+ ]);
456
412
  };
457
- export { RANK_ICONS, renderStatsCard };
413
+ /**
414
+ * The card, and the values each of its options accepts, keyed by the option's own name.
415
+ * They ride on the renderer so a list cannot be found without what draws it;
416
+ * the api handler forwards them onto its own export, which is what a UI reads.
417
+ */
418
+ const renderStatsCard = Object.assign(renderCard, {
419
+ OPTIONS: {
420
+ rank_icon: RANK_ICONS,
421
+ show: SHOW_STATS,
422
+ hide: HIDE_STATS,
423
+ number_format: NUMBER_FORMATS,
424
+ },
425
+ });
426
+ export { renderStatsCard };