@stats-forge/github-stats-forge-core 0.1.0 → 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 (142) hide show
  1. package/build/api/contributed-to.d.ts +23 -0
  2. package/build/api/contributed-to.d.ts.map +1 -0
  3. package/build/api/contributed-to.js +43 -0
  4. package/build/api/gist.d.ts +8 -15
  5. package/build/api/gist.d.ts.map +1 -1
  6. package/build/api/gist.js +14 -29
  7. package/build/api/handler.d.ts +16 -0
  8. package/build/api/handler.d.ts.map +1 -0
  9. package/build/api/handler.js +29 -0
  10. package/build/api/index.d.ts +1 -0
  11. package/build/api/index.d.ts.map +1 -1
  12. package/build/api/index.js +1 -0
  13. package/build/api/params.d.ts +11 -5
  14. package/build/api/params.d.ts.map +1 -1
  15. package/build/api/params.js +26 -6
  16. package/build/api/pin.d.ts +11 -15
  17. package/build/api/pin.d.ts.map +1 -1
  18. package/build/api/pin.js +34 -46
  19. package/build/api/stats.d.ts +15 -22
  20. package/build/api/stats.d.ts.map +1 -1
  21. package/build/api/stats.js +59 -72
  22. package/build/api/top-langs.d.ts +11 -22
  23. package/build/api/top-langs.d.ts.map +1 -1
  24. package/build/api/top-langs.js +34 -51
  25. package/build/api/wakatime.d.ts +10 -24
  26. package/build/api/wakatime.d.ts.map +1 -1
  27. package/build/api/wakatime.js +27 -47
  28. package/build/cards/contributed-to.d.ts +19 -0
  29. package/build/cards/contributed-to.d.ts.map +1 -0
  30. package/build/cards/contributed-to.js +242 -0
  31. package/build/cards/gist.d.ts +1 -0
  32. package/build/cards/gist.d.ts.map +1 -1
  33. package/build/cards/gist.js +13 -9
  34. package/build/cards/repo.d.ts +12 -1
  35. package/build/cards/repo.d.ts.map +1 -1
  36. package/build/cards/repo.js +40 -18
  37. package/build/cards/stats.d.ts +15 -3
  38. package/build/cards/stats.d.ts.map +1 -1
  39. package/build/cards/stats.js +91 -76
  40. package/build/cards/top-languages.d.ts +13 -2
  41. package/build/cards/top-languages.d.ts.map +1 -1
  42. package/build/cards/top-languages.js +26 -9
  43. package/build/cards/wakatime.d.ts +13 -2
  44. package/build/cards/wakatime.d.ts.map +1 -1
  45. package/build/cards/wakatime.js +22 -9
  46. package/build/common/Card.d.ts +9 -0
  47. package/build/common/Card.d.ts.map +1 -1
  48. package/build/common/Card.js +44 -9
  49. package/build/common/I18n.d.ts +1 -1
  50. package/build/common/I18n.d.ts.map +1 -1
  51. package/build/common/I18n.js +4 -2
  52. package/build/common/brand.d.ts +76 -0
  53. package/build/common/brand.d.ts.map +1 -0
  54. package/build/common/brand.js +73 -0
  55. package/build/common/constants.d.ts +3 -1
  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 +50 -3
  59. package/build/common/date.d.ts.map +1 -1
  60. package/build/common/date.js +104 -2
  61. package/build/common/icons.d.ts +4 -2
  62. package/build/common/icons.d.ts.map +1 -1
  63. package/build/common/icons.js +10 -5
  64. package/build/common/ops.d.ts +8 -3
  65. package/build/common/ops.d.ts.map +1 -1
  66. package/build/common/ops.js +8 -14
  67. package/build/common/render.d.ts +7 -1
  68. package/build/common/render.d.ts.map +1 -1
  69. package/build/common/render.js +60 -58
  70. package/build/fetchers/contributed-to.d.ts +62 -0
  71. package/build/fetchers/contributed-to.d.ts.map +1 -0
  72. package/build/fetchers/contributed-to.js +184 -0
  73. package/build/fetchers/graphql-error.d.ts +13 -0
  74. package/build/fetchers/graphql-error.d.ts.map +1 -0
  75. package/build/fetchers/graphql-error.js +29 -0
  76. package/build/fetchers/index.d.ts +2 -1
  77. package/build/fetchers/index.d.ts.map +1 -1
  78. package/build/fetchers/index.js +1 -0
  79. package/build/fetchers/stats.d.ts +14 -8
  80. package/build/fetchers/stats.d.ts.map +1 -1
  81. package/build/fetchers/stats.js +72 -195
  82. package/build/fetchers/top-languages.d.ts.map +1 -1
  83. package/build/fetchers/top-languages.js +3 -18
  84. package/build/fetchers/types.d.ts +24 -0
  85. package/build/fetchers/types.d.ts.map +1 -1
  86. package/build/graphql/contributionsCollection.d.ts +9 -0
  87. package/build/graphql/contributionsCollection.d.ts.map +1 -0
  88. package/build/graphql/contributionsCollection.js +12 -0
  89. package/build/graphql/contributionsDocument.d.ts +5 -5
  90. package/build/graphql/contributionsDocument.d.ts.map +1 -1
  91. package/build/graphql/contributionsDocument.js +8 -15
  92. package/build/graphql/generated/contributed-to.d.ts +22 -0
  93. package/build/graphql/generated/contributed-to.d.ts.map +1 -0
  94. package/build/graphql/generated/contributed-to.js +11 -0
  95. package/build/graphql/generated/stats.d.ts +13 -1
  96. package/build/graphql/generated/stats.d.ts.map +1 -1
  97. package/build/graphql/generated/stats.js +2 -2
  98. package/build/graphql/graphqlDocument.d.ts +1 -1
  99. package/build/graphql/reposContributedToDocument.d.ts.map +1 -1
  100. package/build/graphql/reposContributedToDocument.js +11 -5
  101. package/build/translations.d.ts +64 -1
  102. package/build/translations.d.ts.map +1 -1
  103. package/build/translations.js +65 -1
  104. package/package.json +1 -1
  105. package/src/api/contributed-to.ts +84 -0
  106. package/src/api/gist.ts +20 -37
  107. package/src/api/handler.ts +42 -0
  108. package/src/api/index.ts +1 -0
  109. package/src/api/params.ts +48 -9
  110. package/src/api/pin.ts +33 -45
  111. package/src/api/stats.ts +78 -89
  112. package/src/api/top-langs.ts +34 -56
  113. package/src/api/wakatime.ts +33 -54
  114. package/src/cards/contributed-to.ts +373 -0
  115. package/src/cards/gist.ts +17 -8
  116. package/src/cards/repo.ts +44 -20
  117. package/src/cards/stats.ts +119 -90
  118. package/src/cards/top-languages.ts +27 -18
  119. package/src/cards/wakatime.ts +23 -13
  120. package/src/common/Card.ts +56 -11
  121. package/src/common/I18n.ts +4 -2
  122. package/src/common/brand.ts +109 -0
  123. package/src/common/constants.ts +4 -1
  124. package/src/common/date.ts +135 -3
  125. package/src/common/icons.ts +10 -5
  126. package/src/common/ops.ts +10 -19
  127. package/src/common/render.ts +65 -62
  128. package/src/fetchers/contributed-to.ts +271 -0
  129. package/src/fetchers/graphql-error.ts +36 -0
  130. package/src/fetchers/index.ts +3 -0
  131. package/src/fetchers/stats.ts +123 -274
  132. package/src/fetchers/top-languages.ts +5 -19
  133. package/src/fetchers/types.ts +26 -0
  134. package/src/graphql/contributionsCollection.ts +18 -0
  135. package/src/graphql/contributionsDocument.ts +11 -18
  136. package/src/graphql/generated/contributed-to.ts +25 -0
  137. package/src/graphql/generated/stats.ts +19 -6
  138. package/src/graphql/graphqlDocument.ts +1 -1
  139. package/src/graphql/queries/contributed-to.graphql +8 -0
  140. package/src/graphql/queries/stats.graphql +14 -2
  141. package/src/graphql/reposContributedToDocument.ts +11 -9
  142. package/src/translations.ts +77 -0
package/src/cards/repo.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { CARD_ICON, CARD_WIDTH, FONT_SIZE, FONT_WEIGHT, font } from '../common/brand.ts';
1
2
  import { Card } from '../common/Card.ts';
2
3
  import { getLightDarkColors } from '../common/color.ts';
3
4
  import { kFormatter } from '../common/fmt.ts';
@@ -5,6 +6,7 @@ import { I18n } from '../common/I18n.ts';
5
6
  import { icons } from '../common/icons.ts';
6
7
  import { buildSearchFilter, clampValue, parseEmojis } from '../common/ops.ts';
7
8
  import {
9
+ NUMBER_FORMATS,
8
10
  countWrappedLines,
9
11
  wrapTextMultiline,
10
12
  createLanguageNode,
@@ -23,12 +25,22 @@ import { repoCardLocales } from '../translations.ts';
23
25
  import type { CardOptions, CommonCardOptions } from './options.ts';
24
26
 
25
27
  const ICON_SIZE = 16;
26
- const CARD_DEFAULT_WIDTH = 400;
28
+ const CARD_DEFAULT_WIDTH = CARD_WIDTH.standard;
27
29
  const X_OFFSET = 25;
28
- const DESCRIPTION_FONT_SIZE = 13;
30
+ const DESCRIPTION_FONT_SIZE = FONT_SIZE.meta;
29
31
  const DESCRIPTION_LINE_HEIGHT_PX = 16;
30
32
  const DESCRIPTION_MAX_LINES = 3;
31
33
 
34
+ /** Stats the card draws only when `show` names them. */
35
+ const REPO_SHOW_STATS = [
36
+ 'prs_authored',
37
+ 'prs_commented',
38
+ 'prs_reviewed',
39
+ 'issues_authored',
40
+ 'issues_commented',
41
+ ] as const;
42
+ type RepoShowStat = (typeof REPO_SHOW_STATS)[number];
43
+
32
44
  interface RepoCardOptions extends CommonCardOptions {
33
45
  locale: string;
34
46
  show_owner: boolean;
@@ -85,10 +97,7 @@ const getBadgeSVG = (label: string, xOffset = 0): MarkupElement => {
85
97
  *
86
98
  * @returns Repository card SVG object.
87
99
  */
88
- const renderRepoCard = (
89
- repo: RepositoryData,
90
- options: CardOptions<RepoCardOptions> = {},
91
- ): string => {
100
+ const renderCard = (repo: RepositoryData, options: CardOptions<RepoCardOptions> = {}): string => {
92
101
  const {
93
102
  name,
94
103
  nameWithOwner,
@@ -133,10 +142,13 @@ const renderRepoCard = (
133
142
  translations: repoCardLocales,
134
143
  });
135
144
 
145
+ // Typed against the exported list, so a stat drawn here cannot be missing from it.
146
+ const shows = (stat: RepoShowStat): boolean => show.includes(stat);
147
+
136
148
  const repoFilter = encodeURIComponent(buildSearchFilter([nameWithOwner], []));
137
149
  const encodedUsername = encodeURIComponent(username ?? '');
138
150
  const STATS: Record<string, RepoStatItem> = {};
139
- if (show.includes('prs_authored')) {
151
+ if (shows('prs_authored')) {
140
152
  STATS['prs_authored'] = {
141
153
  icon: icons.prs,
142
154
  label: i18n.t('repocard.prs-authored'),
@@ -145,7 +157,7 @@ const renderRepoCard = (
145
157
  link: `https://github.com/search?q=${repoFilter}author%3A${encodedUsername}&amp;type=pullrequests`,
146
158
  };
147
159
  }
148
- if (show.includes('prs_commented')) {
160
+ if (shows('prs_commented')) {
149
161
  STATS['prs_commented'] = {
150
162
  icon: icons.comments,
151
163
  label: i18n.t('repocard.prs-commented'),
@@ -154,7 +166,7 @@ const renderRepoCard = (
154
166
  link: `https://github.com/search?q=${repoFilter}commenter%3A${encodedUsername}+-author%3A${encodedUsername}&amp;type=pullrequests`,
155
167
  };
156
168
  }
157
- if (show.includes('prs_reviewed')) {
169
+ if (shows('prs_reviewed')) {
158
170
  STATS['prs_reviewed'] = {
159
171
  icon: icons.reviews,
160
172
  label: i18n.t('repocard.prs-reviewed'),
@@ -163,7 +175,7 @@ const renderRepoCard = (
163
175
  link: `https://github.com/search?q=${repoFilter}reviewed-by%3A${encodedUsername}+-author%3A${encodedUsername}&amp;type=pullrequests`,
164
176
  };
165
177
  }
166
- if (show.includes('issues_authored')) {
178
+ if (shows('issues_authored')) {
167
179
  STATS['issues_authored'] = {
168
180
  icon: icons.issues,
169
181
  label: i18n.t('repocard.issues-authored'),
@@ -172,7 +184,7 @@ const renderRepoCard = (
172
184
  link: `https://github.com/search?q=${repoFilter}author%3A${encodedUsername}&amp;type=issues`,
173
185
  };
174
186
  }
175
- if (show.includes('issues_commented')) {
187
+ if (shows('issues_commented')) {
176
188
  STATS['issues_commented'] = {
177
189
  icon: icons.discussions_started,
178
190
  label: i18n.t('repocard.issues-commented'),
@@ -203,9 +215,9 @@ const renderRepoCard = (
203
215
  const extraLHeight = Number.parseInt(String(line_height), 10);
204
216
  const lineHeight = 10;
205
217
  const header = show_owner ? nameWithOwner : name;
206
- const langName = primaryLanguage?.name || 'Unspecified';
218
+ const langName = primaryLanguage?.name || i18n.t('repocard.unspecified-language');
207
219
  const langColor = primaryLanguage?.color || '#333';
208
- const desc = parseEmojis(description || 'No description provided');
220
+ const desc = parseEmojis(description || i18n.t('repocard.no-description'));
209
221
  const descriptionBoxWidth = card_width - 2 * X_OFFSET;
210
222
 
211
223
  let descriptionLinesCount: number;
@@ -294,7 +306,7 @@ const renderRepoCard = (
294
306
 
295
307
  const card = new Card({
296
308
  defaultTitle: header.length > 35 ? `${header.slice(0, 35)}...` : header,
297
- titlePrefixIcon: icons.contribs,
309
+ titlePrefixIcon: CARD_ICON.repo,
298
310
  width: card_width,
299
311
  height,
300
312
  border_radius,
@@ -307,21 +319,21 @@ const renderRepoCard = (
307
319
  card.setCSS({
308
320
  light: ({ textColor, iconColor }) => [
309
321
  rule('.description', {
310
- font: `400 ${DESCRIPTION_FONT_SIZE}px 'Segoe UI', Ubuntu, Sans-Serif`,
322
+ font: font('regular', 'meta'),
311
323
  fill: textColor,
312
324
  ...(browser_rendering ? wrappedTextStyles(textColor) : {}),
313
325
  }),
314
- rule('.gray', { font: "400 12px 'Segoe UI', Ubuntu, Sans-Serif", fill: textColor }),
315
- rule('.badge', { font: "600 11px 'Segoe UI', Ubuntu, Sans-Serif" }),
326
+ rule('.gray', { font: font('regular', 'small'), fill: textColor }),
327
+ rule('.badge', { font: font('semibold', 'micro') }),
316
328
  rule('.badge rect', { opacity: 0.2, stroke: textColor }),
317
329
  rule('.badge text', { fill: textColor }),
318
- rule('.stat', { font: "400 12px 'Segoe UI', Ubuntu, Sans-Serif", fill: textColor }),
330
+ rule('.stat', { font: font('regular', 'small'), fill: textColor }),
319
331
  rule('.stagger', {
320
332
  opacity: 0,
321
333
  animation: 'fadeInAnimation 0.3s ease-in-out forwards',
322
334
  }),
323
- rule('.not_bold', { 'font-weight': 400 }),
324
- rule('.bold', { 'font-weight': 700 }),
335
+ rule('.not_bold', { 'font-weight': FONT_WEIGHT.regular }),
336
+ rule('.bold', { 'font-weight': FONT_WEIGHT.semibold }),
325
337
  rule('.icon', { fill: iconColor, display: 'block' }),
326
338
  ],
327
339
  dark: ({ textColor, iconColor }) => [
@@ -363,4 +375,16 @@ const renderRepoCard = (
363
375
  ]);
364
376
  };
365
377
 
378
+ /**
379
+ * The card, and the values each of its options accepts, keyed by the option's own name.
380
+ * They ride on the renderer so a list cannot be found without what draws it;
381
+ * the api handler forwards them onto its own export, which is what a UI reads.
382
+ */
383
+ const renderRepoCard = Object.assign(renderCard, {
384
+ OPTIONS: {
385
+ show: REPO_SHOW_STATS,
386
+ number_format: NUMBER_FORMATS,
387
+ },
388
+ });
389
+
366
390
  export { renderRepoCard };
@@ -1,11 +1,21 @@
1
+ import {
2
+ CARD_ICON,
3
+ CARD_WIDTH,
4
+ FONT_SIZE,
5
+ FONT_WEIGHT,
6
+ firefoxFontSize,
7
+ font,
8
+ } from '../common/brand.ts';
1
9
  import { Card } from '../common/Card.ts';
2
10
  import { getLightDarkColors } from '../common/color.ts';
3
11
  import type { CardColors } from '../common/color.ts';
12
+ import type { GitHubDateRange } from '../common/date.ts';
13
+ import { formatRange } from '../common/date.ts';
4
14
  import { CardError } from '../common/error.ts';
5
15
  import { I18n } from '../common/I18n.ts';
6
16
  import { icons, rankIcon } from '../common/icons.ts';
7
17
  import { buildSearchFilter, clampValue } from '../common/ops.ts';
8
- import { createTextNode, flexLayout, measureText } from '../common/render.ts';
18
+ import { NUMBER_FORMATS, createTextNode, flexLayout, measureText } from '../common/render.ts';
9
19
  import type { StatsData } from '../fetchers/types.ts';
10
20
  import type { Child, CssChild } from '../markup/index.ts';
11
21
  import { atRule, cssComment, el, rule } from '../markup/index.ts';
@@ -14,19 +24,19 @@ import { statCardLocales, wakatimeCardLocales } from '../translations.ts';
14
24
  import type { CardOptions, CommonCardOptions } from './options.ts';
15
25
 
16
26
  const CARD_MIN_WIDTH = 287;
17
- const CARD_DEFAULT_WIDTH = 287;
18
- const RANK_CARD_MIN_WIDTH = 420;
19
- const RANK_CARD_DEFAULT_WIDTH = 450;
20
- const RANK_ONLY_CARD_MIN_WIDTH = 290;
21
- const RANK_ONLY_CARD_DEFAULT_WIDTH = 290;
27
+ const CARD_DEFAULT_WIDTH = CARD_WIDTH.compact;
28
+ const RANK_CARD_DEFAULT_WIDTH = CARD_WIDTH.wide;
29
+ const RANK_ONLY_CARD_DEFAULT_WIDTH = CARD_WIDTH.compact;
22
30
 
23
- const STAT_FONT_SIZE = 14;
31
+ const STAT_FONT_SIZE = FONT_SIZE.body;
24
32
  /** Padding the card keeps at its edges; matches `Card`'s own `paddingX`. */
25
33
  const CARD_PADDING_X = 25;
26
34
  /** How far a stat row is translated into the card; see `createTextNode`. */
27
35
  const STAT_ROW_X = 25;
28
36
  /** Room the rank ring needs at the right edge, so a value never runs into it. */
29
37
  const RANK_GUTTER = 120;
38
+ /** The ring's own `cx`, which its group's translation has to make up for. */
39
+ const RANK_CIRCLE_CX_OFFSET = 10;
30
40
  /** `createTextNode`'s own label offset, which it applies only when icons are shown. */
31
41
  const LABEL_X_OFFSET = 25;
32
42
  /** Smallest gap kept between the longest label and its value. */
@@ -36,6 +46,30 @@ const LABEL_VALUE_GAP = 16;
36
46
  const RANK_ICONS = ['default', 'github', 'percentile'] as const;
37
47
  type RankIcon = (typeof RANK_ICONS)[number];
38
48
 
49
+ /** Stats the card draws only when `show` names them. */
50
+ const SHOW_STATS = [
51
+ 'contributions',
52
+ 'prs_merged',
53
+ 'prs_merged_percentage',
54
+ 'reviews',
55
+ 'discussions_started',
56
+ 'discussions_answered',
57
+ 'prs_authored',
58
+ 'prs_commented',
59
+ 'prs_reviewed',
60
+ 'issues_authored',
61
+ 'issues_commented',
62
+ 'all_time_contribs',
63
+ ] as const;
64
+ type ShowStat = (typeof SHOW_STATS)[number];
65
+
66
+ /** Stats the card always draws, and so the ones `hide` has anything to remove. */
67
+ const HIDE_STATS = ['stars', 'commits', 'prs', 'issues', 'contribs'] as const;
68
+ type HideStat = (typeof HIDE_STATS)[number];
69
+
70
+ /** Every stat the card can draw: the ones it always draws, plus the ones `show` adds. */
71
+ type StatId = HideStat | ShowStat;
72
+
39
73
  interface StatCardOptions extends CommonCardOptions {
40
74
  locale: string;
41
75
  hide: Array<string>;
@@ -44,7 +78,6 @@ interface StatCardOptions extends CommonCardOptions {
44
78
  card_width: number;
45
79
  hide_rank: boolean;
46
80
  include_all_commits: boolean;
47
- commits_year: number;
48
81
  line_height: number | string;
49
82
  custom_title: string;
50
83
  disable_animations: boolean;
@@ -141,26 +174,22 @@ const getStyles = ({
141
174
  progress: number;
142
175
  }): Array<CssChild> => [
143
176
  rule('.stat', {
144
- font: `400 ${STAT_FONT_SIZE}px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif`,
177
+ font: font('regular', 'body'),
145
178
  fill: textColor,
146
179
  'font-variant-numeric': 'tabular-nums',
147
180
  }),
148
- atRule(
149
- '@supports(-moz-appearance: auto)',
150
- cssComment('Selector detects Firefox'),
151
- rule('.stat', { 'font-size': '12px' }),
152
- ),
181
+ firefoxFontSize(['.stat'], 'small'),
153
182
  rule('.stagger', { opacity: 0, animation: 'fadeInAnimation 0.3s ease-in-out forwards' }),
154
183
  rule('.rank-text', {
155
- font: "700 22px 'Segoe UI', Ubuntu, Sans-Serif",
184
+ font: font('bold', 'display'),
156
185
  fill: textColor,
157
186
  animation: 'scaleInAnimation 0.3s ease-in-out forwards',
158
187
  }),
159
- rule('.rank-percentile-header', { 'font-size': '14px' }),
160
- rule('.rank-percentile-text', { 'font-size': '16px' }),
188
+ rule('.rank-percentile-header', { 'font-size': `${String(FONT_SIZE.body)}px` }),
189
+ rule('.rank-percentile-text', { 'font-size': `${String(FONT_SIZE.lead)}px` }),
161
190
  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 }),
191
+ rule('.not_bold', { 'font-weight': FONT_WEIGHT.regular, opacity: 0.75 }),
192
+ rule('.bold', { 'font-weight': FONT_WEIGHT.semibold }),
164
193
  rule('.icon', {
165
194
  fill: iconColor,
166
195
  opacity: 0.75,
@@ -187,19 +216,17 @@ const getStyles = ({
187
216
  ];
188
217
 
189
218
  /**
190
- * Return the label for commits according to the selected options
191
- *
192
- * @returns The label corresponding to the options.
219
+ * @returns What the commit count covers, for the stat's label.
193
220
  */
194
- const getTotalCommitsYearLabel = (
221
+ const getTotalCommitsRangeLabel = (
195
222
  include_all_commits: boolean,
196
- commits_year: number | undefined,
223
+ commitsRange: GitHubDateRange | undefined,
197
224
  i18n: I18n,
198
225
  ): string =>
199
226
  include_all_commits
200
227
  ? ''
201
- : commits_year
202
- ? ` (${commits_year})`
228
+ : commitsRange
229
+ ? ` (${formatRange(commitsRange)})`
203
230
  : ` (${i18n.t('wakatimecard.lastyear')})`;
204
231
 
205
232
  /**
@@ -207,7 +234,7 @@ const getTotalCommitsYearLabel = (
207
234
  *
208
235
  * @returns The stats card SVG object.
209
236
  */
210
- const renderStatsCard = (
237
+ const renderCard = (
211
238
  stats: StatsData,
212
239
  options: CardOptions<StatCardOptions> = {},
213
240
  username?: string,
@@ -218,6 +245,7 @@ const renderStatsCard = (
218
245
  name,
219
246
  totalStars,
220
247
  totalCommits,
248
+ commitsRange,
221
249
  totalIssues,
222
250
  totalPRs,
223
251
  totalPRsMerged,
@@ -243,7 +271,6 @@ const renderStatsCard = (
243
271
  card_width,
244
272
  hide_rank = false,
245
273
  include_all_commits = false,
246
- commits_year,
247
274
  line_height = 25,
248
275
  text_bold = true,
249
276
  custom_title,
@@ -260,6 +287,9 @@ const renderStatsCard = (
260
287
 
261
288
  const { lightColors, darkColors } = getLightDarkColors(options);
262
289
 
290
+ // Typed against the exported list, so a stat drawn here cannot be missing from it.
291
+ const shows = (stat: ShowStat): boolean => show.includes(stat);
292
+
263
293
  const apostrophe = /s$/i.test(name.trim()) ? '' : 's';
264
294
  const i18n = new I18n({
265
295
  locale,
@@ -270,7 +300,7 @@ const renderStatsCard = (
270
300
  });
271
301
 
272
302
  // Meta data for creating text nodes with createTextNode function
273
- const STATS: Record<string, StatItem> = {
303
+ const STATS: Partial<Record<StatId, StatItem>> = {
274
304
  stars: {
275
305
  icon: icons.star,
276
306
  label: i18n.t('statcard.totalstars'),
@@ -279,7 +309,7 @@ const renderStatsCard = (
279
309
  },
280
310
  };
281
311
 
282
- if (show.includes('contributions')) {
312
+ if (shows('contributions')) {
283
313
  STATS['contributions'] = {
284
314
  icon: icons.contributions,
285
315
  label: i18n.t('statcard.contributions'),
@@ -290,9 +320,9 @@ const renderStatsCard = (
290
320
 
291
321
  STATS['commits'] = {
292
322
  icon: icons.commits,
293
- label: `${i18n.t('statcard.commits')}${getTotalCommitsYearLabel(
323
+ label: `${i18n.t('statcard.commits')}${getTotalCommitsRangeLabel(
294
324
  include_all_commits,
295
- commits_year,
325
+ commitsRange,
296
326
  i18n,
297
327
  )}`,
298
328
  value: totalCommits,
@@ -305,7 +335,7 @@ const renderStatsCard = (
305
335
  id: 'prs',
306
336
  };
307
337
 
308
- if (show.includes('prs_merged')) {
338
+ if (shows('prs_merged')) {
309
339
  STATS['prs_merged'] = {
310
340
  icon: icons.prs_merged,
311
341
  label: i18n.t('statcard.prs-merged'),
@@ -314,7 +344,7 @@ const renderStatsCard = (
314
344
  };
315
345
  }
316
346
 
317
- if (show.includes('prs_merged_percentage')) {
347
+ if (shows('prs_merged_percentage')) {
318
348
  STATS['prs_merged_percentage'] = {
319
349
  icon: icons.prs_merged_percentage,
320
350
  label: i18n.t('statcard.prs-merged-percentage'),
@@ -328,7 +358,7 @@ const renderStatsCard = (
328
358
  };
329
359
  }
330
360
 
331
- if (show.includes('reviews')) {
361
+ if (shows('reviews')) {
332
362
  STATS['reviews'] = {
333
363
  icon: icons.reviews,
334
364
  label: i18n.t('statcard.reviews'),
@@ -344,7 +374,7 @@ const renderStatsCard = (
344
374
  id: 'issues',
345
375
  };
346
376
 
347
- if (show.includes('discussions_started')) {
377
+ if (shows('discussions_started')) {
348
378
  STATS['discussions_started'] = {
349
379
  icon: icons.discussions_started,
350
380
  label: i18n.t('statcard.discussions-started'),
@@ -352,7 +382,7 @@ const renderStatsCard = (
352
382
  id: 'discussions_started',
353
383
  };
354
384
  }
355
- if (show.includes('discussions_answered')) {
385
+ if (shows('discussions_answered')) {
356
386
  STATS['discussions_answered'] = {
357
387
  icon: icons.discussions_answered,
358
388
  label: i18n.t('statcard.discussions-answered'),
@@ -363,7 +393,7 @@ const renderStatsCard = (
363
393
 
364
394
  const repoFilter = encodeURIComponent(buildSearchFilter(repo, owner));
365
395
  const encodedUsername = encodeURIComponent(username ?? '');
366
- if (show.includes('prs_authored')) {
396
+ if (shows('prs_authored')) {
367
397
  STATS['prs_authored'] = {
368
398
  icon: icons.prs,
369
399
  label: i18n.t('statcard.prs-authored'),
@@ -372,7 +402,7 @@ const renderStatsCard = (
372
402
  link: `https://github.com/search?q=${repoFilter}author%3A${encodedUsername}&amp;type=pullrequests`,
373
403
  };
374
404
  }
375
- if (show.includes('prs_commented')) {
405
+ if (shows('prs_commented')) {
376
406
  STATS['prs_commented'] = {
377
407
  icon: icons.comments,
378
408
  label: i18n.t('statcard.prs-commented'),
@@ -381,7 +411,7 @@ const renderStatsCard = (
381
411
  link: `https://github.com/search?q=${repoFilter}commenter%3A${encodedUsername}+-author%3A${encodedUsername}&amp;type=pullrequests`,
382
412
  };
383
413
  }
384
- if (show.includes('prs_reviewed')) {
414
+ if (shows('prs_reviewed')) {
385
415
  STATS['prs_reviewed'] = {
386
416
  icon: icons.reviews,
387
417
  label: i18n.t('statcard.prs-reviewed'),
@@ -390,7 +420,7 @@ const renderStatsCard = (
390
420
  link: `https://github.com/search?q=${repoFilter}reviewed-by%3A${encodedUsername}+-author%3A${encodedUsername}&amp;type=pullrequests`,
391
421
  };
392
422
  }
393
- if (show.includes('issues_authored')) {
423
+ if (shows('issues_authored')) {
394
424
  STATS['issues_authored'] = {
395
425
  icon: icons.issues,
396
426
  label: i18n.t('statcard.issues-authored'),
@@ -399,7 +429,7 @@ const renderStatsCard = (
399
429
  link: `https://github.com/search?q=${repoFilter}author%3A${encodedUsername}&amp;type=issues`,
400
430
  };
401
431
  }
402
- if (show.includes('issues_commented')) {
432
+ if (shows('issues_commented')) {
403
433
  STATS['issues_commented'] = {
404
434
  icon: icons.discussions_started,
405
435
  label: i18n.t('statcard.issues-commented'),
@@ -410,15 +440,15 @@ const renderStatsCard = (
410
440
  }
411
441
 
412
442
  STATS['contribs'] = {
413
- icon: icons.contribs,
443
+ icon: icons.repo,
414
444
  label: i18n.t('statcard.contribs'),
415
445
  value: contributedTo,
416
446
  id: 'contribs',
417
447
  };
418
448
 
419
- if (show.includes('all_time_contribs')) {
449
+ if (shows('all_time_contribs')) {
420
450
  STATS['all_time_contribs'] = {
421
- icon: icons.contribs,
451
+ icon: icons.repo,
422
452
  label: i18n.t('statcard.all-time-contribs'),
423
453
  value: allTimeContributedTo,
424
454
  id: 'all_time_contribs',
@@ -455,29 +485,24 @@ const renderStatsCard = (
455
485
  (visibleStats.length > 0 ? i18n.t('statcard.title') : i18n.t('statcard.ranktitle')),
456
486
  );
457
487
 
458
- /*
459
- When hide_rank=true, the minimum card width is 270 px + the title length and padding.
460
- When hide_rank=false, the minimum card_width is 340 px + the icon width (if show_icons=true).
461
- Numbers are picked by looking at existing dimensions on production.
462
- */
463
488
  const iconWidth = show_icons && visibleStats.length > 0 ? 16 + /* padding */ 1 : 0;
464
- const minCardWidth =
465
- (hide_rank
466
- ? clampValue(50 /* padding */ + calculateTextWidth() * 2, CARD_MIN_WIDTH, Infinity)
467
- : visibleStats.length > 0
468
- ? RANK_CARD_MIN_WIDTH
469
- : RANK_ONLY_CARD_MIN_WIDTH) + iconWidth;
470
- const defaultCardWidth =
471
- (hide_rank
472
- ? CARD_DEFAULT_WIDTH
473
- : visibleStats.length > 0
474
- ? RANK_CARD_DEFAULT_WIDTH
475
- : RANK_ONLY_CARD_DEFAULT_WIDTH) + iconWidth;
476
- const width = card_width
477
- ? Number.isNaN(card_width)
478
- ? Math.max(defaultCardWidth, minCardWidth)
479
- : card_width
480
- : Math.max(defaultCardWidth, minCardWidth);
489
+ // The icons ride inside the step's own slack, so a default card lands on the width grid;
490
+ // only the minimum, which has no slack, still has to make room for them.
491
+ const defaultCardWidth = hide_rank
492
+ ? CARD_DEFAULT_WIDTH
493
+ : visibleStats.length > 0
494
+ ? RANK_CARD_DEFAULT_WIDTH
495
+ : RANK_ONLY_CARD_DEFAULT_WIDTH;
496
+ // Only a card sized by its own title can outgrow its step: the ranked layouts are fixed,
497
+ // so their step is also their floor.
498
+ const fittedWidth = hide_rank
499
+ ? Math.max(
500
+ defaultCardWidth,
501
+ clampValue(50 /* padding */ + calculateTextWidth() * 2, CARD_MIN_WIDTH, Infinity) +
502
+ iconWidth,
503
+ )
504
+ : defaultCardWidth;
505
+ const width = card_width ? (Number.isNaN(card_width) ? fittedWidth : card_width) : fittedWidth;
481
506
 
482
507
  // A value ends at the card's inner edge, clear of the rank ring — or right after
483
508
  // the longest label, on a card too narrow for that.
@@ -515,6 +540,7 @@ const renderStatsCard = (
515
540
  const card = new Card({
516
541
  customTitle: custom_title,
517
542
  defaultTitle: visibleStats.length > 0 ? i18n.t('statcard.title') : i18n.t('statcard.ranktitle'),
543
+ titlePrefixIcon: CARD_ICON.stats,
518
544
  width,
519
545
  height,
520
546
  border_radius,
@@ -533,26 +559,15 @@ const renderStatsCard = (
533
559
  }
534
560
 
535
561
  /**
536
- * Calculates the right rank circle translation values such that the rank circle
537
- * keeps respecting the following padding:
538
- *
539
- * width > RANK_CARD_DEFAULT_WIDTH: The default right padding of 70 px will be used.
540
- * width < RANK_CARD_DEFAULT_WIDTH: The left and right padding will be enlarged
541
- * equally from a certain minimum at RANK_CARD_MIN_WIDTH.
562
+ * The ring sits centred in the same gutter the values are kept clear of, so the gap
563
+ * between them holds at every width; a rank-only card has no values, so it centres.
542
564
  *
543
565
  * @returns Rank circle translation value.
544
566
  */
545
- const calculateRankXTranslation = (): number => {
546
- if (visibleStats.length > 0) {
547
- const minXTranslation = RANK_CARD_MIN_WIDTH + iconWidth - 70;
548
- if (width > RANK_CARD_DEFAULT_WIDTH) {
549
- const xMaxExpansion = minXTranslation + (450 - minCardWidth) / 2;
550
- return xMaxExpansion + width - RANK_CARD_DEFAULT_WIDTH;
551
- }
552
- return minXTranslation + (width - minCardWidth) / 2;
553
- }
554
- return width / 2 + 20 - 10;
555
- };
567
+ const calculateRankXTranslation = (): number =>
568
+ visibleStats.length > 0
569
+ ? width - CARD_PADDING_X - RANK_GUTTER / 2 + RANK_CIRCLE_CX_OFFSET
570
+ : width / 2 + 20 - 10;
556
571
 
557
572
  // Conditionally rendered elements
558
573
  const rankCircle =
@@ -563,8 +578,8 @@ const renderStatsCard = (
563
578
  'data-testid': 'rank-circle',
564
579
  transform: `translate(${calculateRankXTranslation()}, ${height / 2 - 50})`,
565
580
  },
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 }),
581
+ el('circle', { class: 'rank-circle-rim', cx: -RANK_CIRCLE_CX_OFFSET, cy: 8, r: 40 }),
582
+ el('circle', { class: 'rank-circle', cx: -RANK_CIRCLE_CX_OFFSET, cy: 8, r: 40 }),
568
583
  el('g', { class: 'rank-text' }, rankIcon(rank_icon, rank.level, rank.percentile)),
569
584
  );
570
585
 
@@ -572,9 +587,9 @@ const renderStatsCard = (
572
587
  const labels = visibleStats
573
588
  .map(([key, stat]) => {
574
589
  if (key === 'commits') {
575
- return `${i18n.t('statcard.commits')} ${getTotalCommitsYearLabel(
590
+ return `${i18n.t('statcard.commits')} ${getTotalCommitsRangeLabel(
576
591
  include_all_commits,
577
- commits_year,
592
+ commitsRange,
578
593
  i18n,
579
594
  )} : ${stat.value}`;
580
595
  }
@@ -583,7 +598,7 @@ const renderStatsCard = (
583
598
  .join(', ');
584
599
 
585
600
  card.setAccessibilityLabel({
586
- title: `${card.title}, Rank: ${rank.level}`,
601
+ title: `${card.title}, ${i18n.t('statcard.rank')}: ${rank.level}`,
587
602
  desc: labels,
588
603
  });
589
604
 
@@ -593,4 +608,18 @@ const renderStatsCard = (
593
608
  ]);
594
609
  };
595
610
 
596
- export { RANK_ICONS, renderStatsCard };
611
+ /**
612
+ * The card, and the values each of its options accepts, keyed by the option's own name.
613
+ * They ride on the renderer so a list cannot be found without what draws it;
614
+ * the api handler forwards them onto its own export, which is what a UI reads.
615
+ */
616
+ const renderStatsCard = Object.assign(renderCard, {
617
+ OPTIONS: {
618
+ rank_icon: RANK_ICONS,
619
+ show: SHOW_STATS,
620
+ hide: HIDE_STATS,
621
+ number_format: NUMBER_FORMATS,
622
+ },
623
+ });
624
+
625
+ export { renderStatsCard };