@stats-organization/github-readme-stats-core 2.1.5 → 2.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 (169) hide show
  1. package/build/api/gist.d.ts +2 -7
  2. package/build/api/gist.d.ts.map +1 -1
  3. package/build/api/gist.js +9 -41
  4. package/build/api/index.d.ts +3 -8
  5. package/build/api/index.d.ts.map +1 -1
  6. package/build/api/index.js +23 -45
  7. package/build/api/pin.d.ts +2 -7
  8. package/build/api/pin.d.ts.map +1 -1
  9. package/build/api/pin.js +9 -41
  10. package/build/api/top-langs.d.ts +2 -7
  11. package/build/api/top-langs.d.ts.map +1 -1
  12. package/build/api/top-langs.js +11 -55
  13. package/build/api/wakatime.d.ts +2 -7
  14. package/build/api/wakatime.d.ts.map +1 -1
  15. package/build/api/wakatime.js +9 -41
  16. package/build/cards/common-options.d.ts +13 -0
  17. package/build/cards/common-options.d.ts.map +1 -0
  18. package/build/cards/common-options.js +1 -0
  19. package/build/cards/gist.d.ts +11 -16
  20. package/build/cards/gist.d.ts.map +1 -1
  21. package/build/cards/gist.js +26 -31
  22. package/build/cards/repo.d.ts +19 -16
  23. package/build/cards/repo.d.ts.map +1 -1
  24. package/build/cards/repo.js +58 -60
  25. package/build/cards/stats.d.ts +29 -46
  26. package/build/cards/stats.d.ts.map +1 -1
  27. package/build/cards/stats.js +113 -181
  28. package/build/cards/top-languages.d.ts +81 -77
  29. package/build/cards/top-languages.d.ts.map +1 -1
  30. package/build/cards/top-languages.js +174 -200
  31. package/build/cards/wakatime.d.ts +21 -11
  32. package/build/cards/wakatime.d.ts.map +1 -1
  33. package/build/cards/wakatime.js +73 -111
  34. package/build/common/Card.d.ts +34 -13
  35. package/build/common/Card.d.ts.map +1 -1
  36. package/build/common/Card.js +73 -40
  37. package/build/common/I18n.d.ts +4 -2
  38. package/build/common/I18n.d.ts.map +1 -1
  39. package/build/common/color.d.ts +60 -18
  40. package/build/common/color.d.ts.map +1 -1
  41. package/build/common/color.js +102 -5
  42. package/build/common/config.d.ts +2 -1
  43. package/build/common/config.d.ts.map +1 -1
  44. package/build/common/config.js +16 -26
  45. package/build/common/constants.d.ts +2 -1
  46. package/build/common/constants.d.ts.map +1 -1
  47. package/build/common/date.d.ts +37 -0
  48. package/build/common/date.d.ts.map +1 -0
  49. package/build/common/date.js +31 -0
  50. package/build/common/http.d.ts +25 -2
  51. package/build/common/http.d.ts.map +1 -1
  52. package/build/common/http.js +26 -3
  53. package/build/common/icons.d.ts +1 -0
  54. package/build/common/icons.d.ts.map +1 -1
  55. package/build/common/icons.js +1 -0
  56. package/build/common/languageColors.d.ts +11 -0
  57. package/build/common/languageColors.d.ts.map +1 -0
  58. package/build/common/languageColors.js +15 -0
  59. package/build/common/languageColors.json +1 -0
  60. package/build/common/ops.d.ts +3 -10
  61. package/build/common/ops.d.ts.map +1 -1
  62. package/build/common/ops.js +3 -15
  63. package/build/common/render.d.ts +43 -6
  64. package/build/common/render.d.ts.map +1 -1
  65. package/build/common/render.js +77 -11
  66. package/build/common/retryer.d.ts +3 -2
  67. package/build/common/retryer.d.ts.map +1 -1
  68. package/build/common/retryer.js +1 -0
  69. package/build/fetchers/gist.d.ts.map +1 -1
  70. package/build/fetchers/gist.js +8 -39
  71. package/build/fetchers/repo.d.ts +12 -11
  72. package/build/fetchers/repo.d.ts.map +1 -1
  73. package/build/fetchers/repo.js +26 -67
  74. package/build/fetchers/stats.d.ts +24 -17
  75. package/build/fetchers/stats.d.ts.map +1 -1
  76. package/build/fetchers/stats.js +302 -183
  77. package/build/fetchers/top-languages.d.ts +10 -15
  78. package/build/fetchers/top-languages.d.ts.map +1 -1
  79. package/build/fetchers/top-languages.js +52 -103
  80. package/build/fetchers/types.d.ts +24 -20
  81. package/build/fetchers/types.d.ts.map +1 -1
  82. package/build/fetchers/wakatime.d.ts +9 -5
  83. package/build/fetchers/wakatime.d.ts.map +1 -1
  84. package/build/fetchers/wakatime.js +8 -4
  85. package/build/graphql/contributionsDocument.d.ts +18 -0
  86. package/build/graphql/contributionsDocument.d.ts.map +1 -0
  87. package/build/graphql/contributionsDocument.js +30 -0
  88. package/build/graphql/generated/common.d.ts +42 -0
  89. package/build/graphql/generated/common.d.ts.map +1 -0
  90. package/build/graphql/generated/common.js +2 -0
  91. package/build/graphql/generated/gist.d.ts +44 -0
  92. package/build/graphql/generated/gist.d.ts.map +1 -0
  93. package/build/graphql/generated/gist.js +27 -0
  94. package/build/graphql/generated/repo.d.ts +67 -0
  95. package/build/graphql/generated/repo.d.ts.map +1 -0
  96. package/build/graphql/generated/repo.js +30 -0
  97. package/build/graphql/generated/stats.d.ts +150 -0
  98. package/build/graphql/generated/stats.d.ts.map +1 -0
  99. package/build/graphql/generated/stats.js +95 -0
  100. package/build/graphql/generated/top-languages.d.ts +54 -0
  101. package/build/graphql/generated/top-languages.d.ts.map +1 -0
  102. package/build/graphql/generated/top-languages.js +27 -0
  103. package/build/graphql/graphqlDocument.d.ts +22 -0
  104. package/build/graphql/graphqlDocument.d.ts.map +1 -0
  105. package/build/graphql/graphqlDocument.js +8 -0
  106. package/build/graphql/reposContributedToDocument.d.ts +27 -0
  107. package/build/graphql/reposContributedToDocument.d.ts.map +1 -0
  108. package/build/graphql/reposContributedToDocument.js +59 -0
  109. package/build/index.d.ts +3 -1
  110. package/build/index.d.ts.map +1 -1
  111. package/build/index.js +2 -1
  112. package/build/themes/index.d.ts +32 -0
  113. package/build/themes/index.d.ts.map +1 -1
  114. package/build/themes/index.js +28 -0
  115. package/build/translations.d.ts +17 -0
  116. package/build/translations.d.ts.map +1 -1
  117. package/build/translations.js +17 -0
  118. package/package.json +13 -7
  119. package/src/_github-username-regex.d.ts +4 -0
  120. package/src/api/gist.js +10 -46
  121. package/src/api/index.js +30 -51
  122. package/src/api/pin.js +10 -46
  123. package/src/api/top-langs.js +13 -60
  124. package/src/api/wakatime.js +10 -46
  125. package/src/cards/common-options.ts +13 -0
  126. package/src/cards/{gist.js → gist.ts} +37 -37
  127. package/src/cards/{repo.js → repo.ts} +86 -68
  128. package/src/cards/{stats.js → stats.ts} +168 -219
  129. package/src/cards/{top-languages.js → top-languages.ts} +309 -265
  130. package/src/cards/{wakatime.js → wakatime.ts} +136 -130
  131. package/src/common/Card.ts +94 -49
  132. package/src/common/I18n.ts +6 -2
  133. package/src/common/color.ts +163 -14
  134. package/src/common/config.ts +22 -29
  135. package/src/common/constants.ts +3 -1
  136. package/src/common/date.ts +45 -0
  137. package/src/common/http.ts +43 -3
  138. package/src/common/icons.ts +1 -0
  139. package/src/common/languageColors.json +1 -0
  140. package/src/common/languageColors.ts +19 -0
  141. package/src/common/ops.ts +8 -18
  142. package/src/common/render.ts +114 -14
  143. package/src/common/retryer.ts +6 -5
  144. package/src/fetchers/gist.ts +15 -72
  145. package/src/fetchers/repo.ts +114 -0
  146. package/src/fetchers/stats.ts +743 -0
  147. package/src/fetchers/top-languages.ts +135 -0
  148. package/src/fetchers/types.ts +28 -20
  149. package/src/fetchers/{wakatime.js → wakatime.ts} +19 -5
  150. package/src/graphql/contributionsDocument.ts +44 -0
  151. package/src/graphql/generated/common.ts +26 -0
  152. package/src/graphql/generated/gist.ts +59 -0
  153. package/src/graphql/generated/repo.ts +90 -0
  154. package/src/graphql/generated/stats.ts +194 -0
  155. package/src/graphql/generated/top-languages.ts +75 -0
  156. package/src/graphql/graphqlDocument.ts +25 -0
  157. package/src/graphql/queries/gist.graphql +25 -0
  158. package/src/graphql/queries/repo.graphql +28 -0
  159. package/src/graphql/queries/stats.graphql +120 -0
  160. package/src/graphql/queries/top-languages.graphql +34 -0
  161. package/src/graphql/reposContributedToDocument.ts +86 -0
  162. package/src/index.ts +3 -1
  163. package/src/themes/index.ts +33 -0
  164. package/src/translations.ts +17 -0
  165. package/src/cards/types.d.ts +0 -78
  166. package/src/fetchers/repo.js +0 -161
  167. package/src/fetchers/stats.js +0 -473
  168. package/src/fetchers/top-languages.js +0 -175
  169. package/src/themes/README.md +0 -229
@@ -1,18 +1,18 @@
1
- import { isPrefixedHexColor, isValidGradient } from "./color.js";
1
+ import { getCardColors, isPrefixedHexColor, isValidGradient } from "./color.js";
2
2
  import { encodeHTML } from "./html.js";
3
3
  import { flexLayout } from "./render.js";
4
4
 
5
5
  interface CardColors {
6
6
  /** Card title color. */
7
- titleColor?: string;
7
+ titleColor: string;
8
8
  /** Card text color. */
9
- textColor?: string;
9
+ textColor: string;
10
10
  /** Card icon color. */
11
- iconColor?: string;
11
+ iconColor: string;
12
12
  /** Card background color. */
13
- bgColor?: string | Array<string>;
13
+ bgColor: string | Array<string>;
14
14
  /** Card border color. */
15
- borderColor?: string;
15
+ borderColor: string;
16
16
  }
17
17
 
18
18
  class Card {
@@ -21,9 +21,10 @@ class Card {
21
21
  hideBorder: boolean;
22
22
  hideTitle: boolean;
23
23
  border_radius: number;
24
- colors: CardColors;
24
+ colors: { light: CardColors; dark: CardColors | null };
25
25
  title: string;
26
26
  css: string;
27
+ darkCss: string;
27
28
  paddingX: number;
28
29
  paddingY: number;
29
30
  titlePrefixIcon: string | undefined;
@@ -40,7 +41,9 @@ class Card {
40
41
  * @param props.width Card width.
41
42
  * @param props.height Card height.
42
43
  * @param props.border_radius Card border radius.
43
- * @param props.colors Card colors arguments.
44
+ * @param props.colors Card colors for light and dark mode.
45
+ * @param props.colors.light Card colors for light mode.
46
+ * @param props.colors.dark Card colors for dark mode, or `null` when no dark-mode override is needed.
44
47
  * @param props.customTitle Card custom title.
45
48
  * @param props.defaultTitle Card default title.
46
49
  * @param props.titlePrefixIcon Sanitized card title prefix icon.
@@ -49,16 +52,17 @@ class Card {
49
52
  width = 100,
50
53
  height = 100,
51
54
  border_radius = 4.5,
52
- colors = {},
55
+ colors = { light: getCardColors({}), dark: null },
53
56
  customTitle,
54
57
  defaultTitle = "",
55
58
  titlePrefixIcon,
56
59
  }: {
57
60
  width?: number;
58
61
  height?: number;
59
- border_radius?: number;
60
- colors?: CardColors;
61
- customTitle?: string;
62
+ // `| undefined`: card callers forward possibly-undefined query options
63
+ border_radius?: number | undefined;
64
+ colors?: { light: CardColors; dark: CardColors | null };
65
+ customTitle?: string | undefined;
62
66
  defaultTitle?: string;
63
67
  titlePrefixIcon?: string;
64
68
  }) {
@@ -70,11 +74,11 @@ class Card {
70
74
 
71
75
  this.border_radius = parseFloat(String(border_radius));
72
76
 
73
- // returns theme based colors with proper overrides and defaults
74
77
  this.colors = colors;
75
78
  this.title = customTitle === undefined ? defaultTitle : customTitle;
76
79
 
77
80
  this.css = "";
81
+ this.darkCss = "";
78
82
 
79
83
  this.paddingX = 25;
80
84
  this.paddingY = 35;
@@ -105,12 +109,17 @@ class Card {
105
109
  }
106
110
 
107
111
  /**
108
- * The caller must ensure that the passed `css` string is properly sanitized!
112
+ * Sets the card CSS for light and dark mode.
109
113
  *
110
- * @param value The sanitized CSS to add to the card.
114
+ * The caller must ensure that the passed CSS strings are properly sanitized!
115
+ *
116
+ * @param props The props object.
117
+ * @param props.light CSS applied unconditionally (light/default mode).
118
+ * @param props.dark CSS placed inside a `@media (prefers-color-scheme: dark)` block. Pass `null` when not needed.
111
119
  */
112
- setCSS(value: string): void {
113
- this.css = value;
120
+ setCSS({ light, dark }: { light: string; dark?: string | null }): void {
121
+ this.css = light;
122
+ this.darkCss = dark ?? "";
114
123
  }
115
124
 
116
125
  /**
@@ -183,19 +192,12 @@ class Card {
183
192
  * @returns The rendered card gradient.
184
193
  */
185
194
  renderGradient(): string {
186
- if (typeof this.colors.bgColor !== "object") {
187
- return "";
188
- }
189
- if (!isValidGradient(this.colors.bgColor)) {
190
- throw new Error(`Invalid gradient: ${this.colors.bgColor.join(",")}`);
191
- }
192
-
193
- const gradients = this.colors.bgColor.slice(1);
194
- return `
195
- <defs>
195
+ const buildGradientDef = (id: string, bgColor: Array<string>): string => {
196
+ const gradients = bgColor.slice(1);
197
+ return `
196
198
  <linearGradient
197
- id="gradient"
198
- gradientTransform="rotate(${String(this.colors.bgColor[0])})"
199
+ id="${id}"
200
+ gradientTransform="rotate(${String(bgColor[0])})"
199
201
  gradientUnits="userSpaceOnUse"
200
202
  >
201
203
  ${gradients
@@ -204,7 +206,31 @@ class Card {
204
206
  return `<stop offset="${offset}%" stop-color="#${grad}" />`;
205
207
  })
206
208
  .join(",")}
207
- </linearGradient>
209
+ </linearGradient>`;
210
+ };
211
+
212
+ if (
213
+ typeof this.colors.light.bgColor === "object" &&
214
+ !isValidGradient(this.colors.light.bgColor)
215
+ ) {
216
+ throw new Error(
217
+ `Invalid gradient: ${this.colors.light.bgColor.join(",")}`,
218
+ );
219
+ }
220
+ if (
221
+ this.colors.dark &&
222
+ typeof this.colors.dark.bgColor === "object" &&
223
+ !isValidGradient(this.colors.dark.bgColor)
224
+ ) {
225
+ throw new Error(
226
+ `Invalid dark gradient: ${this.colors.dark.bgColor.join(",")}`,
227
+ );
228
+ }
229
+
230
+ return `
231
+ <defs>
232
+ ${typeof this.colors.light.bgColor === "object" ? buildGradientDef("gradient", this.colors.light.bgColor) : ""}
233
+ ${this.colors.dark && typeof this.colors.dark.bgColor === "object" ? buildGradientDef("gradient-dark", this.colors.dark.bgColor) : ""}
208
234
  </defs>
209
235
  `;
210
236
  }
@@ -236,6 +262,28 @@ class Card {
236
262
  `;
237
263
  };
238
264
 
265
+ /**
266
+ * Builds the @media (prefers-color-scheme: dark) CSS block for the card.
267
+ * Returns an empty string when no dark colors are set.
268
+ */
269
+ private renderDarkMediaBlock(): string {
270
+ if (!this.colors.dark) {
271
+ return "";
272
+ }
273
+
274
+ const bgFill =
275
+ typeof this.colors.dark.bgColor === "object"
276
+ ? "url(#gradient-dark)"
277
+ : this.colors.dark.bgColor;
278
+
279
+ return `
280
+ @media (prefers-color-scheme: dark) {
281
+ .header { fill: ${this.colors.dark.titleColor}; }
282
+ .card-bg { fill: ${bgFill}; stroke: ${this.colors.dark.borderColor}; }
283
+ ${this.darkCss}
284
+ }`;
285
+ }
286
+
239
287
  /**
240
288
  * The caller must ensure that the passed `body` string is properly sanitized!
241
289
  *
@@ -246,26 +294,21 @@ class Card {
246
294
  if (!Number.isFinite(this.border_radius)) {
247
295
  throw new Error(`Invalid border radius: "${this.border_radius}"`);
248
296
  }
249
- if (
250
- this.colors.titleColor !== undefined &&
251
- !isPrefixedHexColor(this.colors.titleColor)
252
- ) {
253
- throw new Error(`Invalid title color: "${this.colors.titleColor}"`);
297
+ if (!isPrefixedHexColor(this.colors.light.titleColor)) {
298
+ throw new Error(`Invalid title color: "${this.colors.light.titleColor}"`);
254
299
  }
255
- if (
256
- this.colors.borderColor !== undefined &&
257
- !isPrefixedHexColor(this.colors.borderColor)
258
- ) {
259
- throw new Error(`Invalid border color: "${this.colors.borderColor}"`);
300
+ if (!isPrefixedHexColor(this.colors.light.borderColor)) {
301
+ throw new Error(
302
+ `Invalid border color: "${this.colors.light.borderColor}"`,
303
+ );
260
304
  }
261
305
  if (
262
- this.colors.bgColor !== undefined &&
263
- !(typeof this.colors.bgColor === "object"
264
- ? isValidGradient(this.colors.bgColor)
265
- : isPrefixedHexColor(this.colors.bgColor))
306
+ !(typeof this.colors.light.bgColor === "object"
307
+ ? isValidGradient(this.colors.light.bgColor)
308
+ : isPrefixedHexColor(this.colors.light.bgColor))
266
309
  ) {
267
310
  throw new Error(
268
- `Invalid background color: ${String(this.colors.bgColor)}`,
311
+ `Invalid background color: ${String(this.colors.light.bgColor)}`,
269
312
  );
270
313
  }
271
314
 
@@ -284,7 +327,7 @@ class Card {
284
327
  <style>
285
328
  .header {
286
329
  font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif;
287
- fill: ${String(this.colors.titleColor)};
330
+ fill: ${this.colors.light.titleColor};
288
331
  animation: fadeInAnimation 0.8s ease-in-out forwards;
289
332
  }
290
333
  @supports(-moz-appearance: auto) {
@@ -292,6 +335,7 @@ class Card {
292
335
  .header { font-size: 15.5px; }
293
336
  }
294
337
  ${this.css}
338
+ ${this.renderDarkMediaBlock()}
295
339
 
296
340
  ${this.getAnimations()}
297
341
  ${
@@ -305,16 +349,17 @@ class Card {
305
349
 
306
350
  <rect
307
351
  data-testid="card-bg"
352
+ class="card-bg"
308
353
  x="0.5"
309
354
  y="0.5"
310
355
  rx="${this.border_radius}"
311
356
  height="99%"
312
- stroke="${String(this.colors.borderColor)}"
357
+ stroke="${this.colors.light.borderColor}"
313
358
  width="${this.width - 1}"
314
359
  fill="${
315
- typeof this.colors.bgColor === "object"
360
+ typeof this.colors.light.bgColor === "object"
316
361
  ? "url(#gradient)"
317
- : String(this.colors.bgColor)
362
+ : this.colors.light.bgColor
318
363
  }"
319
364
  stroke-opacity="${this.hideBorder ? 0 : 1}"
320
365
  />
@@ -1,9 +1,12 @@
1
1
  const FALLBACK_LOCALE = "en";
2
2
 
3
+ /** A map of translation keys to per-locale strings. */
4
+ type TranslationsMap = Record<string, Record<string, string>>;
5
+
3
6
  /**
4
7
  * I18n translation class.
5
8
  */
6
- class I18n<Translations extends Record<string, Record<string, string>>> {
9
+ class I18n<Translations extends TranslationsMap = TranslationsMap> {
7
10
  locale: string;
8
11
  translations: Translations;
9
12
 
@@ -16,7 +19,8 @@ class I18n<Translations extends Record<string, Record<string, string>>> {
16
19
  locale,
17
20
  translations,
18
21
  }: {
19
- locale?: string;
22
+ // `| undefined`: card callers forward possibly-undefined query options
23
+ locale?: string | undefined;
20
24
  translations: Translations;
21
25
  }) {
22
26
  this.locale = locale || FALLBACK_LOCALE;
@@ -1,4 +1,5 @@
1
1
  import { themes } from "../themes/index.js";
2
+ import type { ThemeName } from "../themes/index.js";
2
3
 
3
4
  /** Matches a 3-, 4-, 6-, or 8-digit hex color with no leading `#`. */
4
5
  const HEX_COLOR =
@@ -114,8 +115,38 @@ interface CardColors {
114
115
  bgColor: string | Array<string>;
115
116
  borderColor: string;
116
117
  ringColor: string;
118
+ progBarBgColor: string;
117
119
  }
118
120
 
121
+ /**
122
+ * Every color param a card accepts, before any `_light` / `_dark` suffix.
123
+ *
124
+ * Single source of truth: the param types and {@link COLOR_PARAM_KEYS} are all
125
+ * derived from this, so adding a param here is enough.
126
+ */
127
+ const BASE_COLOR_KEYS = [
128
+ "title_color",
129
+ "icon_color",
130
+ "text_color",
131
+ "bg_color",
132
+ "border_color",
133
+ "ring_color",
134
+ "prog_bar_bg_color",
135
+ "theme",
136
+ ] as const;
137
+
138
+ const THEME_VARIANTS = ["light", "dark"] as const;
139
+
140
+ type BaseColorKey = (typeof BASE_COLOR_KEYS)[number];
141
+ type ThemeVariant = (typeof THEME_VARIANTS)[number];
142
+
143
+ /**
144
+ * Object with all input color params. Not every field is consumed by every card
145
+ * (e.g. `prog_bar_bg_color` is only used by the top-languages card's `normal`
146
+ * layout).
147
+ */
148
+ type ColorInput = Partial<Record<BaseColorKey, string | undefined>>;
149
+
119
150
  /**
120
151
  * Returns theme based colors with proper overrides and defaults.
121
152
  *
@@ -126,6 +157,7 @@ interface CardColors {
126
157
  * @param props.bg_color Card background color.
127
158
  * @param props.border_color Card border color.
128
159
  * @param props.ring_color Card ring color.
160
+ * @param props.prog_bar_bg_color Progress bar background color.
129
161
  * @param props.theme Card theme.
130
162
  * @returns Card colors.
131
163
  */
@@ -136,21 +168,14 @@ const getCardColors = ({
136
168
  bg_color,
137
169
  border_color,
138
170
  ring_color,
171
+ prog_bar_bg_color,
139
172
  theme,
140
- }: {
141
- title_color?: string | undefined;
142
- text_color?: string | undefined;
143
- icon_color?: string | undefined;
144
- bg_color?: string | undefined;
145
- border_color?: string | undefined;
146
- ring_color?: string | undefined;
147
- theme?: string | undefined;
148
- }): CardColors => {
173
+ }: ColorInput): CardColors => {
149
174
  const defaultTheme = themes.default;
150
175
  const isThemeProvided = theme !== undefined && theme in themes;
151
176
 
152
177
  const selectedTheme = isThemeProvided
153
- ? themes[theme as keyof typeof themes]
178
+ ? themes[theme as ThemeName]
154
179
  : defaultTheme;
155
180
 
156
181
  const defaultBorderColor =
@@ -167,8 +192,6 @@ const getCardColors = ({
167
192
 
168
193
  // get the color provided by the user else the theme color
169
194
  // finally if both colors are invalid we use the titleColor
170
- // NOTE: no built-in theme defines `ring_color`, so it falls back to the title color.
171
- const ringColor = fallbackColor(ring_color, titleColor);
172
195
  const iconColor = fallbackColor(
173
196
  icon_color || selectedTheme.icon_color,
174
197
  "#" + defaultTheme.icon_color,
@@ -186,11 +209,16 @@ const getCardColors = ({
186
209
  border_color || defaultBorderColor,
187
210
  "#" + defaultBorderColor,
188
211
  );
212
+ // No theme defines `ring_color`, so it falls back to the title color.
213
+ const ringColor = fallbackColor(ring_color, titleColor);
214
+ // No theme defines `prog_bar_bg_color`, so it falls back to "#ddd".
215
+ const progBarBgColor = fallbackColor(prog_bar_bg_color, "#ddd");
189
216
 
190
217
  if (
191
218
  typeof titleColor !== "string" ||
192
219
  typeof textColor !== "string" ||
193
220
  typeof ringColor !== "string" ||
221
+ typeof progBarBgColor !== "string" ||
194
222
  typeof iconColor !== "string" ||
195
223
  typeof borderColor !== "string"
196
224
  ) {
@@ -199,14 +227,135 @@ const getCardColors = ({
199
227
  );
200
228
  }
201
229
 
202
- return { titleColor, iconColor, textColor, bgColor, borderColor, ringColor };
230
+ return {
231
+ titleColor,
232
+ iconColor,
233
+ textColor,
234
+ bgColor,
235
+ borderColor,
236
+ ringColor,
237
+ progBarBgColor,
238
+ };
203
239
  };
204
240
 
241
+ type LightDarkColorParams = Partial<
242
+ Record<`${BaseColorKey}_${ThemeVariant}`, string | undefined>
243
+ >;
244
+
245
+ /**
246
+ * Returns the light- or dark-mode-specific color params, given a set of
247
+ * raw query params. Also removes the "_light" or "_dark" suffixes.
248
+ *
249
+ * @param params Raw query params with optional `_light` / `_dark` suffixes.
250
+ * @param suffix `"_light"` or `"_dark"`.
251
+ * @returns ColorInput with the suffix stripped, ready for `getCardColors`.
252
+ */
253
+ const extractLightDarkColors = (
254
+ params: LightDarkColorParams,
255
+ suffix: `_${ThemeVariant}`,
256
+ ): ColorInput =>
257
+ Object.fromEntries(
258
+ BASE_COLOR_KEYS.map((key) => [key, params[`${key}${suffix}`]]),
259
+ );
260
+
261
+ /**
262
+ * Returns resolved colors for both light and dark mode given all input params.
263
+ *
264
+ * Each mode resolves independently, then runs the normal `getCardColors` precedence
265
+ * (explicit color -> theme color -> default theme):
266
+ * light: `theme_light ?? theme`, with `*_light` params overriding general ones
267
+ * dark: `theme_dark ?? theme`, with `*_dark` params overriding general ones
268
+ *
269
+ * Anything a mode does not override falls back to the general params,
270
+ * so a partial override such as `bg_color_dark` alone keeps every other color from the base theme.
271
+ *
272
+ * When no `_light` / `_dark` param is provided at all, `darkColors` is `null` and the caller emits no dark-mode block.
273
+ *
274
+ * @param params Raw query params, containing both general and `_light`/`_dark` suffixed colors and themes.
275
+ * @returns `{ lightColors, darkColors }`, resolved colors for both light and dark mode
276
+ */
277
+ const getLightDarkColors = (
278
+ params: ColorInput & LightDarkColorParams,
279
+ ): { lightColors: CardColors; darkColors: CardColors | null } => {
280
+ const lightOverrides = extractLightDarkColors(params, "_light");
281
+ const darkOverrides = extractLightDarkColors(params, "_dark");
282
+
283
+ const hasModeOverrides =
284
+ Object.values(lightOverrides).some((v) => v !== undefined) ||
285
+ Object.values(darkOverrides).some((v) => v !== undefined);
286
+
287
+ if (!hasModeOverrides) {
288
+ return { lightColors: getCardColors(params), darkColors: null };
289
+ }
290
+
291
+ const defined = (obj: ColorInput): ColorInput =>
292
+ Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined));
293
+
294
+ return {
295
+ lightColors: getCardColors({ ...params, ...defined(lightOverrides) }),
296
+ darkColors: getCardColors({ ...params, ...defined(darkOverrides) }),
297
+ };
298
+ };
299
+
300
+ type ColorParams = ColorInput & LightDarkColorParams;
301
+
302
+ const COLOR_PARAM_KEYS: ReadonlyArray<keyof ColorParams> = [
303
+ ...BASE_COLOR_KEYS,
304
+ ...THEME_VARIANTS.flatMap((variant) =>
305
+ BASE_COLOR_KEYS.map((key) => `${key}_${variant}` as const),
306
+ ),
307
+ ];
308
+
309
+ /**
310
+ * Picks all color-related parameters from a query object.
311
+ *
312
+ * @param query Raw query parameters.
313
+ * @returns All color-related parameters.
314
+ */
315
+ const pickColorParams = (
316
+ query: Record<string, string | undefined>,
317
+ ): ColorParams =>
318
+ Object.fromEntries(
319
+ COLOR_PARAM_KEYS.filter((k) => k in query).map((k) => [k, query[k]]),
320
+ );
321
+
322
+ /** Params naming a theme rather than holding a color value. */
323
+ const THEME_PARAM_KEYS: ReadonlyArray<keyof ColorParams> = [
324
+ "theme",
325
+ ...THEME_VARIANTS.map((variant) => `theme_${variant}` as const),
326
+ ];
327
+
328
+ /**
329
+ * Finds the first color param holding an invalid color.
330
+ *
331
+ * Theme params are skipped: they name a theme, and an unknown name falls back
332
+ * to the default rather than being an error.
333
+ *
334
+ * @param params Color params, as returned by {@link pickColorParams}.
335
+ * @returns The first invalid param name, or null if all are valid.
336
+ */
337
+ const findInvalidColorParam = (params: ColorParams): string | null =>
338
+ findInvalidColor(
339
+ Object.fromEntries(
340
+ Object.entries(params).filter(
341
+ ([key]) => !THEME_PARAM_KEYS.includes(key as keyof ColorParams),
342
+ ),
343
+ ),
344
+ );
345
+
205
346
  export {
206
- fallbackColor,
207
347
  getCardColors,
348
+ getLightDarkColors,
208
349
  findInvalidColor,
350
+ findInvalidColorParam,
351
+ pickColorParams,
209
352
  isValidGradient,
210
353
  isBareHexColor,
211
354
  isPrefixedHexColor,
355
+
356
+ // Not re-exported from the package index: internal,
357
+ // exposed so tests can pin the accepted param list.
358
+ BASE_COLOR_KEYS,
359
+ THEME_VARIANTS,
360
+ COLOR_PARAM_KEYS,
212
361
  };
@@ -9,7 +9,8 @@ interface Config {
9
9
  whitelist: Array<string> | undefined;
10
10
  gistWhitelist: Array<string> | undefined;
11
11
  excludeRepositories: Array<string>;
12
- fetchMultiPageStars: string | undefined;
12
+ /** Max pages of starred repos to fetch; `Infinity` means every page, `1` only the first. */
13
+ fetchMultiPageStars: number;
13
14
  pats: Array<PersonalAccessToken>;
14
15
  }
15
16
 
@@ -17,11 +18,19 @@ interface Config {
17
18
  * @param value Comma-separated string.
18
19
  * @returns Parsed string values.
19
20
  */
20
- const parseCsv = (value: string | undefined): Array<string> | undefined => {
21
- if (!value) {
22
- return undefined;
21
+ const parseCsv = (value: string | undefined): Array<string> | undefined =>
22
+ value ? value.split(",") : undefined;
23
+
24
+ /**
25
+ * @param value Raw `FETCH_MULTI_PAGE_STARS` value.
26
+ * @returns Page limit: `"true"` means every page, a positive number caps the pages, anything else means one.
27
+ */
28
+ const parseFetchMultiPageStars = (value: string | undefined): number => {
29
+ if (value === "true") {
30
+ return Infinity;
23
31
  }
24
- return value.split(",");
32
+ const limit = Number(value);
33
+ return limit > 0 ? limit : 1;
25
34
  };
26
35
 
27
36
  /**
@@ -45,37 +54,21 @@ const getDefaultEnv = (): Env => {
45
54
  return processEnv ?? {};
46
55
  };
47
56
 
48
- /**
49
- * @param config (Partial) config values to normalize.
50
- * @returns Normalized config object with defaults applied.
51
- */
52
- const normalizeConfig = (config: Partial<Config> = {}): Config => {
53
- return {
54
- whitelist: config.whitelist,
55
- gistWhitelist: config.gistWhitelist,
56
- excludeRepositories: config.excludeRepositories ?? [],
57
- fetchMultiPageStars: config.fetchMultiPageStars,
58
- pats: config.pats ?? [],
59
- };
60
- };
61
-
62
57
  let currentConfig: Config;
63
58
 
64
59
  /**
65
60
  * @param env Environment variables used to build the runtime config.
66
61
  */
67
62
  export const loadConfigFromEnv = (env: Env = getDefaultEnv()): void => {
68
- const whitelist = parseCsv(env["WHITELIST"]);
69
- const gistWhitelist = parseCsv(env["GIST_WHITELIST"]);
70
- const excludeRepositories = parseCsv(env["EXCLUDE_REPO"]) ?? [];
71
-
72
- currentConfig = normalizeConfig({
73
- whitelist,
74
- gistWhitelist,
75
- excludeRepositories,
76
- fetchMultiPageStars: env["FETCH_MULTI_PAGE_STARS"],
63
+ currentConfig = {
64
+ whitelist: parseCsv(env["WHITELIST"]),
65
+ gistWhitelist: parseCsv(env["GIST_WHITELIST"]),
66
+ excludeRepositories: parseCsv(env["EXCLUDE_REPO"]) ?? [],
67
+ fetchMultiPageStars: parseFetchMultiPageStars(
68
+ env["FETCH_MULTI_PAGE_STARS"],
69
+ ),
77
70
  pats: parsePATsFromEnv(env),
78
- });
71
+ };
79
72
  };
80
73
 
81
74
  loadConfigFromEnv();
@@ -1,7 +1,9 @@
1
+ import type { RepositoryAffiliation } from "../graphql/generated/common.js";
2
+
1
3
  /**
2
4
  * Valid owner affiliations for GitHub API queries.
3
5
  */
4
- const OWNER_AFFILIATIONS: Array<string> = [
6
+ const OWNER_AFFILIATIONS: Array<RepositoryAffiliation> = [
5
7
  "OWNER",
6
8
  "COLLABORATOR",
7
9
  "ORGANIZATION_MEMBER",
@@ -0,0 +1,45 @@
1
+ /** A span between two dates, as GitHub's range arguments take it. */
2
+ interface GitHubDateRange {
3
+ /** Start of the range, inclusive. */
4
+ from: Date;
5
+ /** End of the range, inclusive. */
6
+ to: Date;
7
+ }
8
+
9
+ /**
10
+ * Format a date as a GitHub `DateTime` scalar.
11
+ * Seconds precision, no milliseconds.
12
+ *
13
+ * @param date Date to format.
14
+ * @returns e.g. `2024-01-01T00:00:00Z`.
15
+ */
16
+ const toGitHubDateTime = (date: Date): string =>
17
+ `${date.toISOString().slice(0, 19)}Z`;
18
+
19
+ /**
20
+ * The full UTC span of a calendar year, both ends inclusive.
21
+ *
22
+ * The end matters to callers that would otherwise leave a range open:
23
+ * GitHub's `contributionsCollection` defaults `to` to one year after `from`,
24
+ * which pulls the next 1st of January into the year.
25
+ *
26
+ * @param year Four-digit year.
27
+ * @returns The range covering it.
28
+ */
29
+ const getGitHubYearRange = (year: number): GitHubDateRange => ({
30
+ from: new Date(Date.UTC(year, 0, 1)),
31
+ to: new Date(Date.UTC(year, 11, 31, 23, 59, 59)),
32
+ });
33
+
34
+ /**
35
+ * Get diff in minutes between two dates.
36
+ *
37
+ * @param d1 First date.
38
+ * @param d2 Second date.
39
+ * @returns Number of minutes between the two dates.
40
+ */
41
+ const dateDiff = (d1: Date, d2: Date): number =>
42
+ Math.round((d1.getTime() - d2.getTime()) / (1000 * 60));
43
+
44
+ export { dateDiff, getGitHubYearRange, toGitHubDateTime };
45
+ export type { GitHubDateRange };