@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
@@ -0,0 +1,242 @@
1
+ import { CARD_ICON, CARD_WIDTH, FONT_SIZE, firefoxFontSize, font } from '../common/brand.js';
2
+ import { Card } from '../common/Card.js';
3
+ import { getLightDarkColors } from '../common/color.js';
4
+ import { formatYears } from '../common/date.js';
5
+ import { kFormatter } from '../common/fmt.js';
6
+ import { I18n } from '../common/I18n.js';
7
+ import { clampValue } from '../common/ops.js';
8
+ import { createProgressNode, measureText } from '../common/render.js';
9
+ import { atRule, el, rule } from '../markup/index.js';
10
+ import { contributedToCardLocales } from '../translations.js';
11
+ const CARD_DEFAULT_WIDTH = CARD_WIDTH.wide;
12
+ const MIN_CARD_WIDTH = 340;
13
+ /** Padding the card keeps at its edges; matches `Card`'s own `paddingX`. */
14
+ const CARD_PADDING = 25;
15
+ const TITLE_FONT_SIZE = FONT_SIZE.title;
16
+ /** What `Card`'s title layout reserves for the prefix icon, so the title text starts past it. */
17
+ const TITLE_ICON_COLUMN = 25;
18
+ const NAME_FONT_SIZE = FONT_SIZE.meta;
19
+ /** Room kept at the right edge for the contribution count. */
20
+ const COUNT_WIDTH = 46;
21
+ const BAR_WIDTH = 130;
22
+ const COLUMN_GAP = 12;
23
+ /** Baseline of the first row, measured from the top of the card body. */
24
+ const FIRST_ROW_Y = 10;
25
+ const ROW_HEIGHT = 38;
26
+ /** A row with no year marks under it needs only its own line. */
27
+ const ROW_HEIGHT_NO_YEARS = 24;
28
+ /** How far the year marks sit below their row's baseline. */
29
+ const YEAR_STRIP_Y = 8;
30
+ const YEAR_MARK_SIZE = 7;
31
+ const YEAR_MARK_GAP = 3;
32
+ /** Gap between the last row and the footer's own baseline. */
33
+ const FOOTER_GAP = 12;
34
+ /** Where `Card` puts the body while the title is shown; `setHideTitle` adjusts the height by the same 30. */
35
+ const BODY_OFFSET_Y = 55;
36
+ const BOTTOM_PADDING = 18;
37
+ /**
38
+ * The title, naming the account unless that makes it too wide for the card.
39
+ *
40
+ * A login is up to 39 characters, which at the title's size overruns the default width
41
+ * by half again — so the plain wording is the fallback rather than a truncated login.
42
+ *
43
+ * @returns The default title, which `custom_title` still overrides.
44
+ */
45
+ const defaultTitleFor = (i18n, contentWidth) => {
46
+ const named = i18n.t('contributedtocard.title');
47
+ return measureText(named, TITLE_FONT_SIZE) <= contentWidth
48
+ ? named
49
+ : i18n.t('contributedtocard.title-unnamed');
50
+ };
51
+ /**
52
+ * Shortens a name to fit its column, ending it with an ellipsis when it does not.
53
+ *
54
+ * @returns The name as it is drawn.
55
+ */
56
+ const truncateName = (name, maxWidth) => {
57
+ if (measureText(name, NAME_FONT_SIZE) <= maxWidth) {
58
+ return name;
59
+ }
60
+ let trimmed = name;
61
+ while (trimmed.length > 1 && measureText(`${trimmed}…`, NAME_FONT_SIZE) > maxWidth) {
62
+ trimmed = trimmed.slice(0, -1);
63
+ }
64
+ return `${trimmed}…`;
65
+ };
66
+ /**
67
+ * The marks under a repository's name, one per contribution year of the account,
68
+ * filled for the years that repository got a contribution.
69
+ *
70
+ * The step shrinks to fit, so a long-lived account keeps the strip inside its column
71
+ * rather than running it under the bar.
72
+ *
73
+ * @returns The strip, positioned relative to its row's baseline.
74
+ */
75
+ const createYearStrip = (repoYears, years, columnWidth) => {
76
+ const contributed = new Set(repoYears);
77
+ const step = Math.min(YEAR_MARK_SIZE + YEAR_MARK_GAP, Math.floor(columnWidth / years.length));
78
+ const size = Math.max(step - YEAR_MARK_GAP, 3);
79
+ return el('g', { 'data-testid': 'year-strip', transform: `translate(0, ${YEAR_STRIP_Y})` }, years.map((year, index) => el('rect', {
80
+ 'data-testid': contributed.has(year) ? 'year-on' : 'year-off',
81
+ class: contributed.has(year) ? 'year-on' : 'year-off',
82
+ x: index * step,
83
+ y: 0,
84
+ width: size,
85
+ height: size,
86
+ rx: 1,
87
+ ry: 1,
88
+ })));
89
+ };
90
+ /**
91
+ * One repository: its name, a bar for its share of the top repository's contributions,
92
+ * and the count itself.
93
+ *
94
+ * @returns The row, positioned by its own transform.
95
+ */
96
+ const createRepoRow = ({ repo, index, maxContributions, years, nameWidth, contentWidth, rowHeight, }) => el('g', {
97
+ class: 'stagger',
98
+ style: `animation-delay: ${450 + index * 150}ms`,
99
+ transform: `translate(0, ${FIRST_ROW_Y + index * rowHeight})`,
100
+ }, el('text', { class: 'repo-name', 'data-testid': 'repo-name', x: 0, y: 0 }, truncateName(repo.nameWithOwner, nameWidth)), createProgressNode({
101
+ x: nameWidth + COLUMN_GAP,
102
+ y: -9,
103
+ width: BAR_WIDTH,
104
+ progress: (repo.contributions / maxContributions) * 100,
105
+ delay: 450 + index * 150,
106
+ }), el('text', {
107
+ class: 'count',
108
+ 'data-testid': 'repo-contributions',
109
+ x: contentWidth,
110
+ y: 0,
111
+ 'text-anchor': 'end',
112
+ }, kFormatter(repo.contributions)), years.length > 0 && createYearStrip(repo.years, years, nameWidth));
113
+ /**
114
+ * The line under the rows: how much of the walk the card is showing, and over what years.
115
+ *
116
+ * Never dropped, because the card shows a slice of the repositories
117
+ * and a rank means nothing without the total it was taken from.
118
+ *
119
+ * @returns The footer text.
120
+ */
121
+ const footerText = ({ i18n, shown, totalRepos, years, }) => {
122
+ const parts = [
123
+ i18n.t(shown < totalRepos ? 'contributedtocard.footer-top' : 'contributedtocard.footer-all'),
124
+ ];
125
+ const [firstYear] = years;
126
+ const lastYear = years.at(-1);
127
+ if (firstYear !== undefined && lastYear !== undefined) {
128
+ parts.push(formatYears(firstYear, lastYear));
129
+ }
130
+ return parts.join(' · ');
131
+ };
132
+ /**
133
+ * Renders the repositories a user contributed to, ranked by contributions.
134
+ *
135
+ * @returns Contributed-to card SVG string.
136
+ */
137
+ const renderContributedToCard = (data, options = {}) => {
138
+ const { locale, hide_title = false, hide_border = false, card_width, custom_title, disable_animations = false, hide_years = false, border_radius, theme = 'default', } = options;
139
+ const { login, repos, totalRepos } = data;
140
+ const years = hide_years ? [] : data.years;
141
+ const width = card_width && !Number.isNaN(card_width)
142
+ ? clampValue(card_width, MIN_CARD_WIDTH, Number.MAX_SAFE_INTEGER)
143
+ : CARD_DEFAULT_WIDTH;
144
+ const contentWidth = width - 2 * CARD_PADDING;
145
+ const nameWidth = contentWidth - COUNT_WIDTH - BAR_WIDTH - 2 * COLUMN_GAP;
146
+ const rowHeight = years.length > 0 ? ROW_HEIGHT : ROW_HEIGHT_NO_YEARS;
147
+ // the count is a share of the busiest repository, so the first bar is always full
148
+ const maxContributions = Math.max(...repos.map((repo) => repo.contributions), 1);
149
+ const rows = repos.map((repo, index) => createRepoRow({
150
+ repo,
151
+ index,
152
+ maxContributions,
153
+ years,
154
+ nameWidth,
155
+ contentWidth,
156
+ rowHeight,
157
+ }));
158
+ const i18n = new I18n({
159
+ locale,
160
+ translations: contributedToCardLocales({ login, shown: repos.length, totalRepos }),
161
+ });
162
+ const footer = footerText({ i18n, shown: repos.length, totalRepos, years });
163
+ const footerY = FIRST_ROW_Y + Math.max(repos.length, 1) * rowHeight + FOOTER_GAP;
164
+ const height = BODY_OFFSET_Y + footerY + BOTTOM_PADDING;
165
+ const { lightColors, darkColors } = getLightDarkColors({ ...options, theme });
166
+ const card = new Card({
167
+ customTitle: custom_title,
168
+ defaultTitle: defaultTitleFor(i18n, contentWidth - TITLE_ICON_COLUMN),
169
+ titlePrefixIcon: CARD_ICON.contributedTo,
170
+ width,
171
+ height,
172
+ border_radius,
173
+ colors: { light: lightColors, dark: darkColors },
174
+ });
175
+ if (disable_animations) {
176
+ card.disableAnimations();
177
+ }
178
+ card.setHideBorder(hide_border);
179
+ card.setHideTitle(hide_title);
180
+ card.setCSS({
181
+ light: ({ textColor, titleColor, iconColor, progBarBgColor }) => [
182
+ atRule('@keyframes growWidthAnimation', rule('from', { width: 0 }), rule('to', { width: '100%' })),
183
+ rule('.repo-name', {
184
+ font: font('regular', 'meta'),
185
+ fill: textColor,
186
+ }),
187
+ rule('.count', {
188
+ font: font('semibold', 'meta'),
189
+ fill: textColor,
190
+ 'font-variant-numeric': 'tabular-nums',
191
+ }),
192
+ rule('.footer', {
193
+ font: font('regular', 'micro'),
194
+ fill: textColor,
195
+ opacity: 0.7,
196
+ }),
197
+ firefoxFontSize(['.repo-name', '.count'], 'small'),
198
+ rule('.year-on', { fill: titleColor }),
199
+ // a year with no contribution reads off the text color, not `progBarBgColor`:
200
+ // on a dark theme that color is a light grey, and the empty marks came out looking filled
201
+ rule('.year-off', { fill: textColor, opacity: 0.25 }),
202
+ rule('.progress-background', { fill: progBarBgColor }),
203
+ rule('.lang-progress', {
204
+ fill: titleColor,
205
+ animation: 'growWidthAnimation 0.6s ease-in-out forwards',
206
+ }),
207
+ rule('.stagger', { opacity: 0, animation: 'fadeInAnimation 0.3s ease-in-out forwards' }),
208
+ rule('.icon', { fill: iconColor, display: 'block' }),
209
+ ],
210
+ dark: ({ textColor, titleColor, iconColor, progBarBgColor }) => [
211
+ rule('.repo-name', { fill: textColor }),
212
+ rule('.count', { fill: textColor }),
213
+ rule('.footer', { fill: textColor }),
214
+ rule('.year-on', { fill: titleColor }),
215
+ // a year with no contribution reads off the text color, not `progBarBgColor`:
216
+ // on a dark theme that color is a light grey, and the empty marks came out looking filled
217
+ rule('.year-off', { fill: textColor, opacity: 0.25 }),
218
+ rule('.progress-background', { fill: progBarBgColor }),
219
+ rule('.lang-progress', { fill: titleColor }),
220
+ rule('.icon', { fill: iconColor }),
221
+ ],
222
+ });
223
+ // `role="img"` hides the inner text from assistive tech, so everything the card
224
+ // shows has to be repeated here — the year marks included, since they carry
225
+ // information no other element on the card does.
226
+ card.setAccessibilityLabel({
227
+ title: card.title,
228
+ desc: [
229
+ ...repos.map((repo) => {
230
+ const inYears = hide_years
231
+ ? ''
232
+ : `, ${i18n.t('contributedtocard.years')}: ${repo.years.join(', ')}`;
233
+ return `${repo.nameWithOwner}: ${repo.contributions} ${i18n.t('contributedtocard.contributions')}${inYears}`;
234
+ }),
235
+ footer,
236
+ ].join('; '),
237
+ });
238
+ return card.render(el('g', { 'data-testid': 'contributed-to-body', transform: `translate(${CARD_PADDING}, 0)` }, repos.length === 0
239
+ ? el('text', { class: 'repo-name', 'data-testid': 'no-repos', x: 0, y: FIRST_ROW_Y }, i18n.t('contributedtocard.no-contributions'))
240
+ : rows, el('text', { class: 'footer', 'data-testid': 'footer', x: 0, y: footerY }, footer)));
241
+ };
242
+ export { renderContributedToCard, MIN_CARD_WIDTH };
@@ -1,6 +1,7 @@
1
1
  import type { GistData } from '../fetchers/types.ts';
2
2
  import type { CardOptions, CommonCardOptions } from './options.ts';
3
3
  interface GistCardOptions extends CommonCardOptions {
4
+ locale: string;
4
5
  show_owner: boolean;
5
6
  browser_rendering: boolean;
6
7
  }
@@ -1 +1 @@
1
- {"version":3,"file":"gist.d.ts","sourceRoot":"","sources":["../../src/cards/gist.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAIrD,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAWnE,UAAU,eAAgB,SAAQ,iBAAiB;IACjD,UAAU,EAAE,OAAO,CAAC;IACpB,iBAAiB,EAAE,OAAO,CAAC;CAC5B;AAED;;;;GAIG;AACH,QAAA,MAAM,cAAc,aAAc,QAAQ,YAAW,WAAW,CAAC,eAAe,CAAC,KAAQ,MA0HxF,CAAC;AAEF,OAAO,EAAE,cAAc,EAAE,CAAC"}
1
+ {"version":3,"file":"gist.d.ts","sourceRoot":"","sources":["../../src/cards/gist.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAKrD,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAWnE,UAAU,eAAgB,SAAQ,iBAAiB;IACjD,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,OAAO,CAAC;IACpB,iBAAiB,EAAE,OAAO,CAAC;CAC5B;AAED;;;;GAIG;AACH,QAAA,MAAM,cAAc,aAAc,QAAQ,YAAW,WAAW,CAAC,eAAe,CAAC,KAAQ,MA+HxF,CAAC;AAEF,OAAO,EAAE,cAAc,EAAE,CAAC"}
@@ -1,17 +1,20 @@
1
+ import { CARD_ICON, CARD_WIDTH, FONT_SIZE, font } from '../common/brand.js';
1
2
  import { Card } from '../common/Card.js';
2
3
  import { getLightDarkColors } from '../common/color.js';
3
4
  import { kFormatter } from '../common/fmt.js';
5
+ import { I18n } from '../common/I18n.js';
4
6
  import { icons } from '../common/icons.js';
5
7
  import { getLanguageColor } from '../common/languageColors.js';
6
8
  import { parseEmojis } from '../common/ops.js';
7
9
  import { countWrappedLines, wrapTextMultiline, createLanguageNode, flexLayout, iconWithLabel, measureText, wrappedTextNode, wrappedTextStyles, } from '../common/render.js';
8
10
  import { el, rule } from '../markup/index.js';
11
+ import { gistCardLocales } from '../translations.js';
9
12
  const ICON_SIZE = 16;
10
- const CARD_DEFAULT_WIDTH = 400;
13
+ const CARD_DEFAULT_WIDTH = CARD_WIDTH.standard;
11
14
  const X_OFFSET = 25;
12
15
  const HEADER_MAX_LENGTH = 35;
13
16
  const DESCRIPTION_BOX_WIDTH = CARD_DEFAULT_WIDTH - 2 * X_OFFSET;
14
- const DESCRIPTION_FONT_SIZE = 13;
17
+ const DESCRIPTION_FONT_SIZE = FONT_SIZE.meta;
15
18
  const DESCRIPTION_LINE_HEIGHT_PX = 16;
16
19
  const DESCRIPTION_MAX_LINES = 10;
17
20
  /**
@@ -21,9 +24,10 @@ const DESCRIPTION_MAX_LINES = 10;
21
24
  */
22
25
  const renderGistCard = (gistData, options = {}) => {
23
26
  const { name, nameWithOwner, description, language, starsCount, forksCount } = gistData;
24
- const { theme = 'default_repocard', border_radius, show_owner = false, browser_rendering = false, hide_border = false, } = options;
27
+ const { locale, theme = 'default_repocard', border_radius, show_owner = false, browser_rendering = false, hide_border = false, } = options;
25
28
  const { lightColors, darkColors } = getLightDarkColors({ ...options, theme });
26
- const desc = parseEmojis(description || 'No description provided');
29
+ const i18n = new I18n({ locale, translations: gistCardLocales });
30
+ const desc = parseEmojis(description || i18n.t('gistcard.no-description'));
27
31
  let descriptionLines;
28
32
  let descriptionSvg;
29
33
  if (browser_rendering) {
@@ -55,7 +59,7 @@ const renderGistCard = (gistData, options = {}) => {
55
59
  const totalForks = kFormatter(forksCount);
56
60
  const svgStars = iconWithLabel(icons.star, totalStars, 'starsCount', ICON_SIZE);
57
61
  const svgForks = iconWithLabel(icons.fork, totalForks, 'forksCount', ICON_SIZE);
58
- const languageName = language || 'Unspecified';
62
+ const languageName = language || i18n.t('gistcard.unspecified-language');
59
63
  const languageColor = getLanguageColor(languageName);
60
64
  const svgLanguage = createLanguageNode(languageName, languageColor);
61
65
  const starAndForkCount = flexLayout({
@@ -70,7 +74,7 @@ const renderGistCard = (gistData, options = {}) => {
70
74
  const header = show_owner ? nameWithOwner : name;
71
75
  const card = new Card({
72
76
  defaultTitle: header.length > HEADER_MAX_LENGTH ? `${header.slice(0, HEADER_MAX_LENGTH)}...` : header,
73
- titlePrefixIcon: icons.gist,
77
+ titlePrefixIcon: CARD_ICON.gist,
74
78
  width: CARD_DEFAULT_WIDTH,
75
79
  height,
76
80
  border_radius,
@@ -79,11 +83,11 @@ const renderGistCard = (gistData, options = {}) => {
79
83
  card.setCSS({
80
84
  light: ({ textColor, iconColor }) => [
81
85
  rule('.description', {
82
- font: `400 ${DESCRIPTION_FONT_SIZE}px 'Segoe UI', Ubuntu, Sans-Serif`,
86
+ font: font('regular', 'meta'),
83
87
  fill: textColor,
84
88
  ...(browser_rendering ? wrappedTextStyles(textColor) : {}),
85
89
  }),
86
- rule('.gray', { font: "400 12px 'Segoe UI', Ubuntu, Sans-Serif", fill: textColor }),
90
+ rule('.gray', { font: font('regular', 'small'), fill: textColor }),
87
91
  rule('.icon', { fill: iconColor }),
88
92
  ],
89
93
  dark: ({ textColor, iconColor }) => [
@@ -100,7 +104,7 @@ const renderGistCard = (gistData, options = {}) => {
100
104
  // shows has to be repeated here.
101
105
  card.setAccessibilityLabel({
102
106
  title: card.title,
103
- desc: `${desc}. Language: ${languageName}, Stars: ${totalStars}, Forks: ${totalForks}`,
107
+ desc: `${desc}. ${i18n.t('gistcard.language')}: ${languageName}, ${i18n.t('gistcard.stars')}: ${totalStars}, ${i18n.t('gistcard.forks')}: ${totalForks}`,
104
108
  });
105
109
  return card.render([
106
110
  descriptionSvg,
@@ -18,6 +18,17 @@ interface RepoCardOptions extends CommonCardOptions {
18
18
  *
19
19
  * @returns Repository card SVG object.
20
20
  */
21
- declare const renderRepoCard: (repo: RepositoryData, options?: CardOptions<RepoCardOptions>) => string;
21
+ declare const renderCard: (repo: RepositoryData, options?: CardOptions<RepoCardOptions>) => string;
22
+ /**
23
+ * The card, and the values each of its options accepts, keyed by the option's own name.
24
+ * They ride on the renderer so a list cannot be found without what draws it;
25
+ * the api handler forwards them onto its own export, which is what a UI reads.
26
+ */
27
+ declare const renderRepoCard: typeof renderCard & {
28
+ OPTIONS: {
29
+ show: readonly ["prs_authored", "prs_commented", "prs_reviewed", "issues_authored", "issues_commented"];
30
+ number_format: readonly ["short", "long"];
31
+ };
32
+ };
22
33
  export { renderRepoCard };
23
34
  //# sourceMappingURL=repo.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"repo.d.ts","sourceRoot":"","sources":["../../src/cards/repo.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAK3D,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AASnE,UAAU,eAAgB,SAAQ,iBAAiB;IACjD,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,OAAO,CAAC;IACpB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,uBAAuB,EAAE,MAAM,CAAC;IAChC,gBAAgB,EAAE,MAAM,CAAC;IACzB,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,OAAO,CAAC;IACnB,WAAW,EAAE,MAAM,GAAG,MAAM,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAuCD;;;;GAIG;AACH,QAAA,MAAM,cAAc,SACZ,cAAc,YACX,WAAW,CAAC,eAAe,CAAC,KACpC,MAiRF,CAAC;AAEF,OAAO,EAAE,cAAc,EAAE,CAAC"}
1
+ {"version":3,"file":"repo.d.ts","sourceRoot":"","sources":["../../src/cards/repo.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAK3D,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAmBnE,UAAU,eAAgB,SAAQ,iBAAiB;IACjD,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,OAAO,CAAC;IACpB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,uBAAuB,EAAE,MAAM,CAAC;IAChC,gBAAgB,EAAE,MAAM,CAAC;IACzB,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,OAAO,CAAC;IACnB,WAAW,EAAE,MAAM,GAAG,MAAM,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAuCD;;;;GAIG;AACH,QAAA,MAAM,UAAU,SAAU,cAAc,YAAW,WAAW,CAAC,eAAe,CAAC,KAAQ,MAoRtF,CAAC;AAEF;;;;GAIG;AACH,QAAA,MAAM,cAAc;;QAEhB,IAAI;QACJ,aAAa;;CAEf,CAAC;AAEH,OAAO,EAAE,cAAc,EAAE,CAAC"}
@@ -1,18 +1,27 @@
1
+ import { CARD_ICON, CARD_WIDTH, FONT_SIZE, FONT_WEIGHT, font } from '../common/brand.js';
1
2
  import { Card } from '../common/Card.js';
2
3
  import { getLightDarkColors } from '../common/color.js';
3
4
  import { kFormatter } from '../common/fmt.js';
4
5
  import { I18n } from '../common/I18n.js';
5
6
  import { icons } from '../common/icons.js';
6
7
  import { buildSearchFilter, clampValue, parseEmojis } from '../common/ops.js';
7
- import { countWrappedLines, wrapTextMultiline, createLanguageNode, createTextNode, flexLayout, iconWithLabel, measureText, wrappedTextNode, wrappedTextStyles, } from '../common/render.js';
8
+ import { NUMBER_FORMATS, countWrappedLines, wrapTextMultiline, createLanguageNode, createTextNode, flexLayout, iconWithLabel, measureText, wrappedTextNode, wrappedTextStyles, } from '../common/render.js';
8
9
  import { el, rule } from '../markup/index.js';
9
10
  import { repoCardLocales } from '../translations.js';
10
11
  const ICON_SIZE = 16;
11
- const CARD_DEFAULT_WIDTH = 400;
12
+ const CARD_DEFAULT_WIDTH = CARD_WIDTH.standard;
12
13
  const X_OFFSET = 25;
13
- const DESCRIPTION_FONT_SIZE = 13;
14
+ const DESCRIPTION_FONT_SIZE = FONT_SIZE.meta;
14
15
  const DESCRIPTION_LINE_HEIGHT_PX = 16;
15
16
  const DESCRIPTION_MAX_LINES = 3;
17
+ /** Stats the card draws only when `show` names them. */
18
+ const REPO_SHOW_STATS = [
19
+ 'prs_authored',
20
+ 'prs_commented',
21
+ 'prs_reviewed',
22
+ 'issues_authored',
23
+ 'issues_commented',
24
+ ];
16
25
  /**
17
26
  * Retrieves the repository description and wraps it to fit the card width.
18
27
  *
@@ -35,7 +44,7 @@ const getBadgeSVG = (label, xOffset = 0) => {
35
44
  *
36
45
  * @returns Repository card SVG object.
37
46
  */
38
- const renderRepoCard = (repo, options = {}) => {
47
+ const renderCard = (repo, options = {}) => {
39
48
  const { name, nameWithOwner, description, primaryLanguage, isArchived, isTemplate, stargazerCount, forkCount, totalPRsAuthored, totalPRsCommented, totalPRsReviewed, totalIssuesAuthored, totalIssuesCommented, } = repo;
40
49
  const { hide_border = false, card_width_input, show_owner = false, browser_rendering = false, show = [], show_icons = true, number_format = 'short', text_bold = false, line_height = 22, username, theme = 'default_repocard', border_radius, locale, description_lines_count, } = options;
41
50
  const card_width = card_width_input && !Number.isNaN(card_width_input)
@@ -47,10 +56,12 @@ const renderRepoCard = (repo, options = {}) => {
47
56
  locale,
48
57
  translations: repoCardLocales,
49
58
  });
59
+ // Typed against the exported list, so a stat drawn here cannot be missing from it.
60
+ const shows = (stat) => show.includes(stat);
50
61
  const repoFilter = encodeURIComponent(buildSearchFilter([nameWithOwner], []));
51
62
  const encodedUsername = encodeURIComponent(username ?? '');
52
63
  const STATS = {};
53
- if (show.includes('prs_authored')) {
64
+ if (shows('prs_authored')) {
54
65
  STATS['prs_authored'] = {
55
66
  icon: icons.prs,
56
67
  label: i18n.t('repocard.prs-authored'),
@@ -59,7 +70,7 @@ const renderRepoCard = (repo, options = {}) => {
59
70
  link: `https://github.com/search?q=${repoFilter}author%3A${encodedUsername}&amp;type=pullrequests`,
60
71
  };
61
72
  }
62
- if (show.includes('prs_commented')) {
73
+ if (shows('prs_commented')) {
63
74
  STATS['prs_commented'] = {
64
75
  icon: icons.comments,
65
76
  label: i18n.t('repocard.prs-commented'),
@@ -68,7 +79,7 @@ const renderRepoCard = (repo, options = {}) => {
68
79
  link: `https://github.com/search?q=${repoFilter}commenter%3A${encodedUsername}+-author%3A${encodedUsername}&amp;type=pullrequests`,
69
80
  };
70
81
  }
71
- if (show.includes('prs_reviewed')) {
82
+ if (shows('prs_reviewed')) {
72
83
  STATS['prs_reviewed'] = {
73
84
  icon: icons.reviews,
74
85
  label: i18n.t('repocard.prs-reviewed'),
@@ -77,7 +88,7 @@ const renderRepoCard = (repo, options = {}) => {
77
88
  link: `https://github.com/search?q=${repoFilter}reviewed-by%3A${encodedUsername}+-author%3A${encodedUsername}&amp;type=pullrequests`,
78
89
  };
79
90
  }
80
- if (show.includes('issues_authored')) {
91
+ if (shows('issues_authored')) {
81
92
  STATS['issues_authored'] = {
82
93
  icon: icons.issues,
83
94
  label: i18n.t('repocard.issues-authored'),
@@ -86,7 +97,7 @@ const renderRepoCard = (repo, options = {}) => {
86
97
  link: `https://github.com/search?q=${repoFilter}author%3A${encodedUsername}&amp;type=issues`,
87
98
  };
88
99
  }
89
- if (show.includes('issues_commented')) {
100
+ if (shows('issues_commented')) {
90
101
  STATS['issues_commented'] = {
91
102
  icon: icons.discussions_started,
92
103
  label: i18n.t('repocard.issues-commented'),
@@ -114,9 +125,9 @@ const renderRepoCard = (repo, options = {}) => {
114
125
  const extraLHeight = Number.parseInt(String(line_height), 10);
115
126
  const lineHeight = 10;
116
127
  const header = show_owner ? nameWithOwner : name;
117
- const langName = primaryLanguage?.name || 'Unspecified';
128
+ const langName = primaryLanguage?.name || i18n.t('repocard.unspecified-language');
118
129
  const langColor = primaryLanguage?.color || '#333';
119
- const desc = parseEmojis(description || 'No description provided');
130
+ const desc = parseEmojis(description || i18n.t('repocard.no-description'));
120
131
  const descriptionBoxWidth = card_width - 2 * X_OFFSET;
121
132
  let descriptionLinesCount;
122
133
  let descriptionSvg;
@@ -177,7 +188,7 @@ const renderRepoCard = (repo, options = {}) => {
177
188
  const extraItems = el('svg', { x: 0, y: 0 }, el('g', { transform: `translate(-3, ${height - 52 - extraHeight})` }, flexLayout({ items: extraRows, gap: extraLHeight, direction: 'column' })));
178
189
  const card = new Card({
179
190
  defaultTitle: header.length > 35 ? `${header.slice(0, 35)}...` : header,
180
- titlePrefixIcon: icons.contribs,
191
+ titlePrefixIcon: CARD_ICON.repo,
181
192
  width: card_width,
182
193
  height,
183
194
  border_radius,
@@ -189,21 +200,21 @@ const renderRepoCard = (repo, options = {}) => {
189
200
  card.setCSS({
190
201
  light: ({ textColor, iconColor }) => [
191
202
  rule('.description', {
192
- font: `400 ${DESCRIPTION_FONT_SIZE}px 'Segoe UI', Ubuntu, Sans-Serif`,
203
+ font: font('regular', 'meta'),
193
204
  fill: textColor,
194
205
  ...(browser_rendering ? wrappedTextStyles(textColor) : {}),
195
206
  }),
196
- rule('.gray', { font: "400 12px 'Segoe UI', Ubuntu, Sans-Serif", fill: textColor }),
197
- rule('.badge', { font: "600 11px 'Segoe UI', Ubuntu, Sans-Serif" }),
207
+ rule('.gray', { font: font('regular', 'small'), fill: textColor }),
208
+ rule('.badge', { font: font('semibold', 'micro') }),
198
209
  rule('.badge rect', { opacity: 0.2, stroke: textColor }),
199
210
  rule('.badge text', { fill: textColor }),
200
- rule('.stat', { font: "400 12px 'Segoe UI', Ubuntu, Sans-Serif", fill: textColor }),
211
+ rule('.stat', { font: font('regular', 'small'), fill: textColor }),
201
212
  rule('.stagger', {
202
213
  opacity: 0,
203
214
  animation: 'fadeInAnimation 0.3s ease-in-out forwards',
204
215
  }),
205
- rule('.not_bold', { 'font-weight': 400 }),
206
- rule('.bold', { 'font-weight': 700 }),
216
+ rule('.not_bold', { 'font-weight': FONT_WEIGHT.regular }),
217
+ rule('.bold', { 'font-weight': FONT_WEIGHT.semibold }),
207
218
  rule('.icon', { fill: iconColor, display: 'block' }),
208
219
  ],
209
220
  dark: ({ textColor, iconColor }) => [
@@ -241,4 +252,15 @@ const renderRepoCard = (repo, options = {}) => {
241
252
  extraItems,
242
253
  ]);
243
254
  };
255
+ /**
256
+ * The card, and the values each of its options accepts, keyed by the option's own name.
257
+ * They ride on the renderer so a list cannot be found without what draws it;
258
+ * the api handler forwards them onto its own export, which is what a UI reads.
259
+ */
260
+ const renderRepoCard = Object.assign(renderCard, {
261
+ OPTIONS: {
262
+ show: REPO_SHOW_STATS,
263
+ number_format: NUMBER_FORMATS,
264
+ },
265
+ });
244
266
  export { renderRepoCard };
@@ -11,7 +11,6 @@ interface StatCardOptions extends CommonCardOptions {
11
11
  card_width: number;
12
12
  hide_rank: boolean;
13
13
  include_all_commits: boolean;
14
- commits_year: number;
15
14
  line_height: number | string;
16
15
  custom_title: string;
17
16
  disable_animations: boolean;
@@ -27,6 +26,19 @@ interface StatCardOptions extends CommonCardOptions {
27
26
  *
28
27
  * @returns The stats card SVG object.
29
28
  */
30
- declare const renderStatsCard: (stats: StatsData, options?: CardOptions<StatCardOptions>, username?: string, repo?: Array<string>, owner?: Array<string>) => string;
31
- export { RANK_ICONS, renderStatsCard };
29
+ declare const renderCard: (stats: StatsData, options?: CardOptions<StatCardOptions>, username?: string, repo?: Array<string>, owner?: Array<string>) => string;
30
+ /**
31
+ * The card, and the values each of its options accepts, keyed by the option's own name.
32
+ * They ride on the renderer so a list cannot be found without what draws it;
33
+ * the api handler forwards them onto its own export, which is what a UI reads.
34
+ */
35
+ declare const renderStatsCard: typeof renderCard & {
36
+ OPTIONS: {
37
+ rank_icon: readonly ["default", "github", "percentile"];
38
+ show: readonly ["contributions", "prs_merged", "prs_merged_percentage", "reviews", "discussions_started", "discussions_answered", "prs_authored", "prs_commented", "prs_reviewed", "issues_authored", "issues_commented", "all_time_contribs"];
39
+ hide: readonly ["stars", "commits", "prs", "issues", "contribs"];
40
+ number_format: readonly ["short", "long"];
41
+ };
42
+ };
43
+ export { renderStatsCard };
32
44
  //# sourceMappingURL=stats.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"stats.d.ts","sourceRoot":"","sources":["../../src/cards/stats.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAKtD,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAqBnE,qFAAqF;AACrF,QAAA,MAAM,UAAU,YAAI,SAAS,EAAE,QAAQ,EAAE,YAAY,CAAU,CAAC;AAChE,KAAK,QAAQ,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;AAE5C,UAAU,eAAgB,SAAQ,iBAAiB;IACjD,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,mBAAmB,EAAE,OAAO,CAAC;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,GAAG,MAAM,CAAC;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,kBAAkB,EAAE,OAAO,CAAC;IAC5B,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,MAAM,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,QAAQ,CAAC;IACpB,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CACrB;AAoJD;;;;GAIG;AACH,QAAA,MAAM,eAAe,UACZ,SAAS,YACP,WAAW,CAAC,eAAe,CAAC,aAC1B,MAAM,SACX,KAAK,CAAC,MAAM,CAAC,UACZ,KAAK,CAAC,MAAM,CAAC,KACnB,MA0XF,CAAC;AAEF,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,CAAC"}
1
+ {"version":3,"file":"stats.d.ts","sourceRoot":"","sources":["../../src/cards/stats.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAKtD,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAqBnE,qFAAqF;AACrF,QAAA,MAAM,UAAU,YAAI,SAAS,EAAE,QAAQ,EAAE,YAAY,CAAU,CAAC;AAChE,KAAK,QAAQ,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;AA0B5C,UAAU,eAAgB,SAAQ,iBAAiB;IACjD,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,mBAAmB,EAAE,OAAO,CAAC;IAC7B,WAAW,EAAE,MAAM,GAAG,MAAM,CAAC;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,kBAAkB,EAAE,OAAO,CAAC;IAC5B,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,MAAM,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,QAAQ,CAAC;IACpB,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CACrB;AA8ID;;;;GAIG;AACH,QAAA,MAAM,UAAU,UACP,SAAS,YACP,WAAW,CAAC,eAAe,CAAC,aAC1B,MAAM,SACX,KAAK,CAAC,MAAM,CAAC,UACZ,KAAK,CAAC,MAAM,CAAC,KACnB,MA8WF,CAAC;AAEF;;;;GAIG;AACH,QAAA,MAAM,eAAe;;QAEjB,SAAS;QACT,IAAI;QACJ,IAAI;QACJ,aAAa;;CAEf,CAAC;AAEH,OAAO,EAAE,eAAe,EAAE,CAAC"}