@stats-organization/github-readme-stats-core 2.1.4 → 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 (182) 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 +28 -26
  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 +32 -36
  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 +18 -33
  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 +32 -44
  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 +28 -26
  16. package/build/calculateRank.d.ts +12 -11
  17. package/build/calculateRank.d.ts.map +1 -1
  18. package/build/calculateRank.js +18 -18
  19. package/build/cards/common-options.d.ts +13 -0
  20. package/build/cards/common-options.d.ts.map +1 -0
  21. package/build/cards/common-options.js +1 -0
  22. package/build/cards/gist.d.ts +11 -16
  23. package/build/cards/gist.d.ts.map +1 -1
  24. package/build/cards/gist.js +26 -31
  25. package/build/cards/repo.d.ts +19 -16
  26. package/build/cards/repo.d.ts.map +1 -1
  27. package/build/cards/repo.js +79 -72
  28. package/build/cards/stats.d.ts +29 -44
  29. package/build/cards/stats.d.ts.map +1 -1
  30. package/build/cards/stats.js +118 -173
  31. package/build/cards/top-languages.d.ts +81 -77
  32. package/build/cards/top-languages.d.ts.map +1 -1
  33. package/build/cards/top-languages.js +186 -184
  34. package/build/cards/wakatime.d.ts +21 -11
  35. package/build/cards/wakatime.d.ts.map +1 -1
  36. package/build/cards/wakatime.js +92 -109
  37. package/build/common/Card.d.ts +41 -14
  38. package/build/common/Card.d.ts.map +1 -1
  39. package/build/common/Card.js +92 -31
  40. package/build/common/I18n.d.ts +4 -2
  41. package/build/common/I18n.d.ts.map +1 -1
  42. package/build/common/color.d.ts +91 -15
  43. package/build/common/color.d.ts.map +1 -1
  44. package/build/common/color.js +163 -17
  45. package/build/common/config.d.ts +2 -1
  46. package/build/common/config.d.ts.map +1 -1
  47. package/build/common/config.js +16 -26
  48. package/build/common/constants.d.ts +2 -1
  49. package/build/common/constants.d.ts.map +1 -1
  50. package/build/common/date.d.ts +37 -0
  51. package/build/common/date.d.ts.map +1 -0
  52. package/build/common/date.js +31 -0
  53. package/build/common/html.js +1 -1
  54. package/build/common/http.d.ts +37 -5
  55. package/build/common/http.d.ts.map +1 -1
  56. package/build/common/http.js +29 -6
  57. package/build/common/icons.d.ts +1 -0
  58. package/build/common/icons.d.ts.map +1 -1
  59. package/build/common/icons.js +1 -0
  60. package/build/common/languageColors.d.ts +11 -0
  61. package/build/common/languageColors.d.ts.map +1 -0
  62. package/build/common/languageColors.js +15 -0
  63. package/build/common/languageColors.json +10 -1
  64. package/build/common/ops.d.ts +27 -33
  65. package/build/common/ops.d.ts.map +1 -1
  66. package/build/common/ops.js +31 -48
  67. package/build/common/render.d.ts +154 -113
  68. package/build/common/render.d.ts.map +1 -1
  69. package/build/common/render.js +209 -92
  70. package/build/common/retryer.d.ts +15 -6
  71. package/build/common/retryer.d.ts.map +1 -1
  72. package/build/common/retryer.js +2 -0
  73. package/build/fetchers/gist.d.ts +6 -21
  74. package/build/fetchers/gist.d.ts.map +1 -1
  75. package/build/fetchers/gist.js +28 -64
  76. package/build/fetchers/repo.d.ts +12 -11
  77. package/build/fetchers/repo.d.ts.map +1 -1
  78. package/build/fetchers/repo.js +27 -68
  79. package/build/fetchers/stats.d.ts +24 -17
  80. package/build/fetchers/stats.d.ts.map +1 -1
  81. package/build/fetchers/stats.js +302 -184
  82. package/build/fetchers/top-languages.d.ts +10 -15
  83. package/build/fetchers/top-languages.d.ts.map +1 -1
  84. package/build/fetchers/top-languages.js +52 -103
  85. package/build/fetchers/types.d.ts +130 -0
  86. package/build/fetchers/types.d.ts.map +1 -0
  87. package/build/fetchers/types.js +1 -0
  88. package/build/fetchers/wakatime.d.ts +9 -5
  89. package/build/fetchers/wakatime.d.ts.map +1 -1
  90. package/build/fetchers/wakatime.js +8 -4
  91. package/build/graphql/contributionsDocument.d.ts +18 -0
  92. package/build/graphql/contributionsDocument.d.ts.map +1 -0
  93. package/build/graphql/contributionsDocument.js +30 -0
  94. package/build/graphql/generated/common.d.ts +42 -0
  95. package/build/graphql/generated/common.d.ts.map +1 -0
  96. package/build/graphql/generated/common.js +2 -0
  97. package/build/graphql/generated/gist.d.ts +44 -0
  98. package/build/graphql/generated/gist.d.ts.map +1 -0
  99. package/build/graphql/generated/gist.js +27 -0
  100. package/build/graphql/generated/repo.d.ts +67 -0
  101. package/build/graphql/generated/repo.d.ts.map +1 -0
  102. package/build/graphql/generated/repo.js +30 -0
  103. package/build/graphql/generated/stats.d.ts +150 -0
  104. package/build/graphql/generated/stats.d.ts.map +1 -0
  105. package/build/graphql/generated/stats.js +95 -0
  106. package/build/graphql/generated/top-languages.d.ts +54 -0
  107. package/build/graphql/generated/top-languages.d.ts.map +1 -0
  108. package/build/graphql/generated/top-languages.js +27 -0
  109. package/build/graphql/graphqlDocument.d.ts +22 -0
  110. package/build/graphql/graphqlDocument.d.ts.map +1 -0
  111. package/build/graphql/graphqlDocument.js +8 -0
  112. package/build/graphql/reposContributedToDocument.d.ts +27 -0
  113. package/build/graphql/reposContributedToDocument.d.ts.map +1 -0
  114. package/build/graphql/reposContributedToDocument.js +59 -0
  115. package/build/index.d.ts +3 -1
  116. package/build/index.d.ts.map +1 -1
  117. package/build/index.js +2 -1
  118. package/build/themes/index.d.ts +32 -0
  119. package/build/themes/index.d.ts.map +1 -1
  120. package/build/themes/index.js +28 -0
  121. package/build/translations.d.ts +17 -0
  122. package/build/translations.d.ts.map +1 -1
  123. package/build/translations.js +17 -0
  124. package/package.json +14 -8
  125. package/src/_emoji-name-map.d.ts +10 -0
  126. package/src/_github-username-regex.d.ts +4 -0
  127. package/src/api/gist.js +31 -31
  128. package/src/api/index.js +40 -42
  129. package/src/api/pin.js +20 -38
  130. package/src/api/top-langs.js +36 -49
  131. package/src/api/wakatime.js +31 -31
  132. package/src/{calculateRank.js → calculateRank.ts} +29 -19
  133. package/src/cards/common-options.ts +13 -0
  134. package/src/cards/{gist.js → gist.ts} +37 -37
  135. package/src/cards/{repo.js → repo.ts} +107 -79
  136. package/src/cards/{stats.js → stats.ts} +173 -210
  137. package/src/cards/{top-languages.js → top-languages.ts} +325 -243
  138. package/src/cards/{wakatime.js → wakatime.ts} +158 -127
  139. package/src/common/Card.ts +123 -43
  140. package/src/common/I18n.ts +6 -2
  141. package/src/common/color.ts +240 -28
  142. package/src/common/config.ts +22 -29
  143. package/src/common/constants.ts +3 -1
  144. package/src/common/date.ts +45 -0
  145. package/src/common/html.ts +1 -1
  146. package/src/common/http.ts +71 -0
  147. package/src/common/icons.ts +1 -0
  148. package/src/common/languageColors.json +10 -1
  149. package/src/common/languageColors.ts +19 -0
  150. package/src/common/ops.ts +158 -0
  151. package/src/common/render.ts +652 -0
  152. package/src/common/retryer.ts +21 -12
  153. package/src/fetchers/gist.ts +79 -0
  154. package/src/fetchers/repo.ts +114 -0
  155. package/src/fetchers/stats.ts +743 -0
  156. package/src/fetchers/top-languages.ts +135 -0
  157. package/src/fetchers/{types.d.ts → types.ts} +28 -20
  158. package/src/fetchers/{wakatime.js → wakatime.ts} +19 -5
  159. package/src/graphql/contributionsDocument.ts +44 -0
  160. package/src/graphql/generated/common.ts +26 -0
  161. package/src/graphql/generated/gist.ts +59 -0
  162. package/src/graphql/generated/repo.ts +90 -0
  163. package/src/graphql/generated/stats.ts +194 -0
  164. package/src/graphql/generated/top-languages.ts +75 -0
  165. package/src/graphql/graphqlDocument.ts +25 -0
  166. package/src/graphql/queries/gist.graphql +25 -0
  167. package/src/graphql/queries/repo.graphql +28 -0
  168. package/src/graphql/queries/stats.graphql +120 -0
  169. package/src/graphql/queries/top-languages.graphql +34 -0
  170. package/src/graphql/reposContributedToDocument.ts +86 -0
  171. package/src/index.ts +3 -1
  172. package/src/themes/index.ts +33 -0
  173. package/src/translations.ts +17 -0
  174. package/src/cards/types.d.ts +0 -78
  175. package/src/common/http.js +0 -19
  176. package/src/common/ops.js +0 -170
  177. package/src/common/render.js +0 -440
  178. package/src/fetchers/gist.js +0 -112
  179. package/src/fetchers/repo.js +0 -161
  180. package/src/fetchers/stats.js +0 -474
  181. package/src/fetchers/top-languages.js +0 -175
  182. package/src/themes/README.md +0 -229
@@ -1,17 +1,18 @@
1
+ import { getCardColors, isPrefixedHexColor, isValidGradient } from "./color.js";
1
2
  import { encodeHTML } from "./html.js";
2
3
  import { flexLayout } from "./render.js";
3
4
 
4
5
  interface CardColors {
5
6
  /** Card title color. */
6
- titleColor?: string;
7
+ titleColor: string;
7
8
  /** Card text color. */
8
- textColor?: string;
9
+ textColor: string;
9
10
  /** Card icon color. */
10
- iconColor?: string;
11
+ iconColor: string;
11
12
  /** Card background color. */
12
- bgColor?: string | Array<string>;
13
+ bgColor: string | Array<string>;
13
14
  /** Card border color. */
14
- borderColor?: string;
15
+ borderColor: string;
15
16
  }
16
17
 
17
18
  class Card {
@@ -20,9 +21,10 @@ class Card {
20
21
  hideBorder: boolean;
21
22
  hideTitle: boolean;
22
23
  border_radius: number;
23
- colors: CardColors;
24
+ colors: { light: CardColors; dark: CardColors | null };
24
25
  title: string;
25
26
  css: string;
27
+ darkCss: string;
26
28
  paddingX: number;
27
29
  paddingY: number;
28
30
  titlePrefixIcon: string | undefined;
@@ -33,29 +35,34 @@ class Card {
33
35
  /**
34
36
  * Creates a new card instance.
35
37
  *
38
+ * The caller must ensure that the passed `titlePrefixIcon` is properly sanitized!
39
+ *
36
40
  * @param props Card arguments.
37
41
  * @param props.width Card width.
38
42
  * @param props.height Card height.
39
43
  * @param props.border_radius Card border radius.
40
- * @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.
41
47
  * @param props.customTitle Card custom title.
42
48
  * @param props.defaultTitle Card default title.
43
- * @param props.titlePrefixIcon Card title prefix icon.
49
+ * @param props.titlePrefixIcon Sanitized card title prefix icon.
44
50
  */
45
51
  constructor({
46
52
  width = 100,
47
53
  height = 100,
48
54
  border_radius = 4.5,
49
- colors = {},
55
+ colors = { light: getCardColors({}), dark: null },
50
56
  customTitle,
51
57
  defaultTitle = "",
52
58
  titlePrefixIcon,
53
59
  }: {
54
60
  width?: number;
55
61
  height?: number;
56
- border_radius?: number;
57
- colors?: CardColors;
58
- 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;
59
66
  defaultTitle?: string;
60
67
  titlePrefixIcon?: string;
61
68
  }) {
@@ -65,15 +72,13 @@ class Card {
65
72
  this.hideBorder = false;
66
73
  this.hideTitle = false;
67
74
 
68
- this.border_radius = border_radius;
75
+ this.border_radius = parseFloat(String(border_radius));
69
76
 
70
- // returns theme based colors with proper overrides and defaults
71
77
  this.colors = colors;
72
- this.title = encodeHTML(
73
- customTitle === undefined ? defaultTitle : customTitle,
74
- );
78
+ this.title = customTitle === undefined ? defaultTitle : customTitle;
75
79
 
76
80
  this.css = "";
81
+ this.darkCss = "";
77
82
 
78
83
  this.paddingX = 25;
79
84
  this.paddingY = 35;
@@ -104,10 +109,17 @@ class Card {
104
109
  }
105
110
 
106
111
  /**
107
- * @param value The CSS to add to the card.
112
+ * Sets the card CSS for light and dark mode.
113
+ *
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.
108
119
  */
109
- setCSS(value: string): void {
110
- this.css = value;
120
+ setCSS({ light, dark }: { light: string; dark?: string | null }): void {
121
+ this.css = light;
122
+ this.darkCss = dark ?? "";
111
123
  }
112
124
 
113
125
  /**
@@ -121,10 +133,13 @@ class Card {
121
133
  * @param value Whether to hide the title or not.
122
134
  */
123
135
  setHideTitle(value: boolean): void {
124
- this.hideTitle = value;
125
- if (value) {
136
+ if (value && !this.hideTitle) {
126
137
  this.height -= 30;
127
138
  }
139
+ if (!value && this.hideTitle) {
140
+ this.height += 30;
141
+ }
142
+ this.hideTitle = value;
128
143
  }
129
144
 
130
145
  /**
@@ -144,7 +159,7 @@ class Card {
144
159
  y="0"
145
160
  class="header"
146
161
  data-testid="header"
147
- >${this.title}</text>
162
+ >${encodeHTML(this.title)}</text>
148
163
  `;
149
164
 
150
165
  const prefixIcon = `
@@ -177,17 +192,12 @@ class Card {
177
192
  * @returns The rendered card gradient.
178
193
  */
179
194
  renderGradient(): string {
180
- if (typeof this.colors.bgColor !== "object") {
181
- return "";
182
- }
183
-
184
- const gradients = this.colors.bgColor.slice(1);
185
- return typeof this.colors.bgColor === "object"
186
- ? `
187
- <defs>
195
+ const buildGradientDef = (id: string, bgColor: Array<string>): string => {
196
+ const gradients = bgColor.slice(1);
197
+ return `
188
198
  <linearGradient
189
- id="gradient"
190
- gradientTransform="rotate(${String(this.colors.bgColor[0])})"
199
+ id="${id}"
200
+ gradientTransform="rotate(${String(bgColor[0])})"
191
201
  gradientUnits="userSpaceOnUse"
192
202
  >
193
203
  ${gradients
@@ -196,10 +206,33 @@ class Card {
196
206
  return `<stop offset="${offset}%" stop-color="#${grad}" />`;
197
207
  })
198
208
  .join(",")}
199
- </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) : ""}
200
234
  </defs>
201
- `
202
- : "";
235
+ `;
203
236
  }
204
237
 
205
238
  /**
@@ -230,10 +263,55 @@ class Card {
230
263
  };
231
264
 
232
265
  /**
233
- * @param body The inner body of the card.
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
+
287
+ /**
288
+ * The caller must ensure that the passed `body` string is properly sanitized!
289
+ *
290
+ * @param body The sanitized inner body of the card.
234
291
  * @returns The rendered card.
235
292
  */
236
293
  render(body: string): string {
294
+ if (!Number.isFinite(this.border_radius)) {
295
+ throw new Error(`Invalid border radius: "${this.border_radius}"`);
296
+ }
297
+ if (!isPrefixedHexColor(this.colors.light.titleColor)) {
298
+ throw new Error(`Invalid title color: "${this.colors.light.titleColor}"`);
299
+ }
300
+ if (!isPrefixedHexColor(this.colors.light.borderColor)) {
301
+ throw new Error(
302
+ `Invalid border color: "${this.colors.light.borderColor}"`,
303
+ );
304
+ }
305
+ if (
306
+ !(typeof this.colors.light.bgColor === "object"
307
+ ? isValidGradient(this.colors.light.bgColor)
308
+ : isPrefixedHexColor(this.colors.light.bgColor))
309
+ ) {
310
+ throw new Error(
311
+ `Invalid background color: ${String(this.colors.light.bgColor)}`,
312
+ );
313
+ }
314
+
237
315
  return `
238
316
  <svg
239
317
  width="${this.width}"
@@ -244,12 +322,12 @@ class Card {
244
322
  role="img"
245
323
  aria-labelledby="descId"
246
324
  >
247
- <title id="titleId">${this.a11yTitle}</title>
248
- <desc id="descId">${this.a11yDesc}</desc>
325
+ <title id="titleId">${encodeHTML(this.a11yTitle)}</title>
326
+ <desc id="descId">${encodeHTML(this.a11yDesc)}</desc>
249
327
  <style>
250
328
  .header {
251
329
  font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif;
252
- fill: ${String(this.colors.titleColor)};
330
+ fill: ${this.colors.light.titleColor};
253
331
  animation: fadeInAnimation 0.8s ease-in-out forwards;
254
332
  }
255
333
  @supports(-moz-appearance: auto) {
@@ -257,6 +335,7 @@ class Card {
257
335
  .header { font-size: 15.5px; }
258
336
  }
259
337
  ${this.css}
338
+ ${this.renderDarkMediaBlock()}
260
339
 
261
340
  ${this.getAnimations()}
262
341
  ${
@@ -270,16 +349,17 @@ class Card {
270
349
 
271
350
  <rect
272
351
  data-testid="card-bg"
352
+ class="card-bg"
273
353
  x="0.5"
274
354
  y="0.5"
275
355
  rx="${this.border_radius}"
276
356
  height="99%"
277
- stroke="${String(this.colors.borderColor)}"
357
+ stroke="${this.colors.light.borderColor}"
278
358
  width="${this.width - 1}"
279
359
  fill="${
280
- typeof this.colors.bgColor === "object"
360
+ typeof this.colors.light.bgColor === "object"
281
361
  ? "url(#gradient)"
282
- : String(this.colors.bgColor)
362
+ : this.colors.light.bgColor
283
363
  }"
284
364
  stroke-opacity="${this.hideBorder ? 0 : 1}"
285
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,30 +1,87 @@
1
1
  import { themes } from "../themes/index.js";
2
+ import type { ThemeName } from "../themes/index.js";
3
+
4
+ /** Matches a 3-, 4-, 6-, or 8-digit hex color with no leading `#`. */
5
+ const HEX_COLOR =
6
+ /^([A-Fa-f0-9]{8}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{4}|[A-Fa-f0-9]{3})$/;
2
7
 
3
8
  /**
4
- * Checks if a string is a valid hex color.
9
+ * Checks if a value is a bare hex color, i.e. hex digits with no `#` prefix
10
+ * (`"f00"`, `"ffffff"`). This is the form user-supplied color params and
11
+ * gradient stops arrive in.
5
12
  *
6
- * @param hexColor String to check.
7
- * @returns True if the given string is a valid hex color.
13
+ * @param value Value to check.
14
+ * @returns True if the value is a bare hex color.
8
15
  */
9
- const isValidHexColor = (hexColor: string): boolean => {
10
- return new RegExp(
11
- /^([A-Fa-f0-9]{8}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}|[A-Fa-f0-9]{4})$/,
12
- ).test(hexColor);
16
+ const isBareHexColor = (value: unknown): boolean => {
17
+ return typeof value === "string" && HEX_COLOR.test(value);
13
18
  };
14
19
 
15
20
  /**
16
- * Check if the given string is a valid gradient.
21
+ * Checks if a value is a `#`-prefixed hex color (`"#f00"`, `"#ffffff"`). This
22
+ * is the form colors take once resolved by {@link getCardColors}, i.e. right
23
+ * before they are written into the SVG.
17
24
  *
18
- * @param colors Array of colors.
19
- * @returns True if the given string is a valid gradient.
25
+ * @param value Value to check.
26
+ * @returns True if the value is a `#`-prefixed hex color.
20
27
  */
21
- const isValidGradient = (colors: Array<string>): boolean => {
28
+ const isPrefixedHexColor = (value: unknown): boolean => {
22
29
  return (
23
- colors.length > 2 &&
24
- colors.slice(1).every((color) => isValidHexColor(color))
30
+ typeof value === "string" &&
31
+ value.startsWith("#") &&
32
+ HEX_COLOR.test(value.slice(1))
25
33
  );
26
34
  };
27
35
 
36
+ /**
37
+ * Checks if the given parts form a valid gradient: a finite numeric angle
38
+ * followed by at least two bare-hex color stops, e.g. `["90", "f00", "0f0"]`.
39
+ * The angle is written into the SVG `gradientTransform="rotate(...)"`.
40
+ *
41
+ * @param parts Gradient parts: `[angle, ...stops]`.
42
+ * @returns True if the parts form a valid gradient.
43
+ */
44
+ const isValidGradient = (parts: Array<string>): boolean => {
45
+ const [angle, ...stops] = parts;
46
+ return (
47
+ stops.length >= 2 &&
48
+ angle !== undefined &&
49
+ angle.trim() !== "" &&
50
+ Number.isFinite(Number(angle)) &&
51
+ stops.every(isBareHexColor)
52
+ );
53
+ };
54
+
55
+ /**
56
+ * Checks if a string is a valid input for a color or gradient.
57
+ *
58
+ * @param color String to check, may be null or undefined.
59
+ * @returns True if the given string is a valid input.
60
+ */
61
+ const isValidColorInput = (color: string | null | undefined): boolean => {
62
+ if (color === null || color === undefined) {
63
+ return true;
64
+ }
65
+ return isValidGradient(color.split(",")) || isBareHexColor(color);
66
+ };
67
+
68
+ /**
69
+ * Iterates over a collection of colors inputs and verifies that each is a valid color or gradient.
70
+ *
71
+ * @param colors Object whose values are checked as valid color inputs.
72
+ * @return The first key where the associated input value is not valid. null if all inputs are valid.
73
+ */
74
+ const findInvalidColor = (
75
+ colors: Record<string, string | null | undefined>,
76
+ ): string | null => {
77
+ for (const [key, value] of Object.entries(colors)) {
78
+ if (!isValidColorInput(value)) {
79
+ return key;
80
+ }
81
+ }
82
+ return null;
83
+ };
84
+
28
85
  /**
29
86
  * Retrieves a gradient if color has more than one valid hex codes else a single color.
30
87
  *
@@ -41,7 +98,7 @@ const fallbackColor = (
41
98
  return colors;
42
99
  }
43
100
 
44
- if (color !== undefined && isValidHexColor(color)) {
101
+ if (color !== undefined && isBareHexColor(color)) {
45
102
  return `#${color}`;
46
103
  }
47
104
 
@@ -58,8 +115,38 @@ interface CardColors {
58
115
  bgColor: string | Array<string>;
59
116
  borderColor: string;
60
117
  ringColor: string;
118
+ progBarBgColor: string;
61
119
  }
62
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
+
63
150
  /**
64
151
  * Returns theme based colors with proper overrides and defaults.
65
152
  *
@@ -70,6 +157,7 @@ interface CardColors {
70
157
  * @param props.bg_color Card background color.
71
158
  * @param props.border_color Card border color.
72
159
  * @param props.ring_color Card ring color.
160
+ * @param props.prog_bar_bg_color Progress bar background color.
73
161
  * @param props.theme Card theme.
74
162
  * @returns Card colors.
75
163
  */
@@ -80,21 +168,14 @@ const getCardColors = ({
80
168
  bg_color,
81
169
  border_color,
82
170
  ring_color,
171
+ prog_bar_bg_color,
83
172
  theme,
84
- }: {
85
- title_color?: string;
86
- text_color?: string;
87
- icon_color?: string;
88
- bg_color?: string;
89
- border_color?: string;
90
- ring_color?: string;
91
- theme?: string;
92
- }): CardColors => {
173
+ }: ColorInput): CardColors => {
93
174
  const defaultTheme = themes.default;
94
175
  const isThemeProvided = theme !== undefined && theme in themes;
95
176
 
96
177
  const selectedTheme = isThemeProvided
97
- ? themes[theme as keyof typeof themes]
178
+ ? themes[theme as ThemeName]
98
179
  : defaultTheme;
99
180
 
100
181
  const defaultBorderColor =
@@ -111,8 +192,6 @@ const getCardColors = ({
111
192
 
112
193
  // get the color provided by the user else the theme color
113
194
  // finally if both colors are invalid we use the titleColor
114
- // NOTE: no built-in theme defines `ring_color`, so it falls back to the title color.
115
- const ringColor = fallbackColor(ring_color, titleColor);
116
195
  const iconColor = fallbackColor(
117
196
  icon_color || selectedTheme.icon_color,
118
197
  "#" + defaultTheme.icon_color,
@@ -130,11 +209,16 @@ const getCardColors = ({
130
209
  border_color || defaultBorderColor,
131
210
  "#" + defaultBorderColor,
132
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");
133
216
 
134
217
  if (
135
218
  typeof titleColor !== "string" ||
136
219
  typeof textColor !== "string" ||
137
220
  typeof ringColor !== "string" ||
221
+ typeof progBarBgColor !== "string" ||
138
222
  typeof iconColor !== "string" ||
139
223
  typeof borderColor !== "string"
140
224
  ) {
@@ -143,7 +227,135 @@ const getCardColors = ({
143
227
  );
144
228
  }
145
229
 
146
- return { titleColor, iconColor, textColor, bgColor, borderColor, ringColor };
230
+ return {
231
+ titleColor,
232
+ iconColor,
233
+ textColor,
234
+ bgColor,
235
+ borderColor,
236
+ ringColor,
237
+ progBarBgColor,
238
+ };
239
+ };
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
+ };
147
298
  };
148
299
 
149
- export { fallbackColor, getCardColors };
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
+
346
+ export {
347
+ getCardColors,
348
+ getLightDarkColors,
349
+ findInvalidColor,
350
+ findInvalidColorParam,
351
+ pickColorParams,
352
+ isValidGradient,
353
+ isBareHexColor,
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,
361
+ };