@stats-organization/github-readme-stats-core 2.1.3 → 2.1.5
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.
- package/build/api/gist.d.ts.map +1 -1
- package/build/api/gist.js +34 -1
- package/build/api/index.d.ts.map +1 -1
- package/build/api/index.js +18 -1
- package/build/api/pin.d.ts.map +1 -1
- package/build/api/pin.js +17 -1
- package/build/api/top-langs.d.ts.map +1 -1
- package/build/api/top-langs.js +36 -5
- package/build/api/wakatime.d.ts.map +1 -1
- package/build/api/wakatime.js +34 -1
- package/build/calculateRank.d.ts +12 -11
- package/build/calculateRank.d.ts.map +1 -1
- package/build/calculateRank.js +18 -18
- package/build/cards/gist.d.ts.map +1 -1
- package/build/cards/gist.js +0 -1
- package/build/cards/repo.d.ts.map +1 -1
- package/build/cards/repo.js +29 -21
- package/build/cards/stats.d.ts +4 -2
- package/build/cards/stats.d.ts.map +1 -1
- package/build/cards/stats.js +21 -9
- package/build/cards/top-languages.d.ts.map +1 -1
- package/build/cards/top-languages.js +39 -12
- package/build/cards/wakatime.d.ts.map +1 -1
- package/build/cards/wakatime.js +29 -9
- package/build/common/Card.d.ts +54 -60
- package/build/common/Card.d.ts.map +1 -1
- package/build/common/Card.js +88 -61
- package/build/common/I18n.d.ts +13 -14
- package/build/common/I18n.d.ts.map +1 -1
- package/build/common/I18n.js +13 -12
- package/build/common/color.d.ts +56 -32
- package/build/common/color.d.ts.map +1 -1
- package/build/common/color.js +87 -50
- package/build/common/config.d.ts +18 -2
- package/build/common/config.d.ts.map +1 -1
- package/build/common/config.js +17 -26
- package/build/common/constants.d.ts +2 -2
- package/build/common/constants.d.ts.map +1 -1
- package/build/common/constants.js +5 -3
- package/build/common/error.d.ts +36 -29
- package/build/common/error.d.ts.map +1 -1
- package/build/common/error.js +14 -10
- package/build/common/fmt.d.ts +14 -13
- package/build/common/fmt.d.ts.map +1 -1
- package/build/common/fmt.js +19 -15
- package/build/common/html.d.ts +2 -4
- package/build/common/html.d.ts.map +1 -1
- package/build/common/html.js +2 -6
- package/build/common/http.d.ts +13 -4
- package/build/common/http.d.ts.map +1 -1
- package/build/common/http.js +3 -4
- package/build/common/icons.d.ts +22 -21
- package/build/common/icons.d.ts.map +1 -1
- package/build/common/icons.js +4 -5
- package/build/common/languageColors.json +9 -1
- package/build/common/ops.d.ts +30 -29
- package/build/common/ops.d.ts.map +1 -1
- package/build/common/ops.js +32 -38
- package/build/common/render.d.ts +115 -111
- package/build/common/render.d.ts.map +1 -1
- package/build/common/render.js +138 -88
- package/build/common/retryer.d.ts +24 -13
- package/build/common/retryer.d.ts.map +1 -1
- package/build/common/retryer.js +22 -30
- package/build/fetchers/gist.d.ts +6 -21
- package/build/fetchers/gist.d.ts.map +1 -1
- package/build/fetchers/gist.js +30 -36
- package/build/fetchers/repo.d.ts.map +1 -1
- package/build/fetchers/repo.js +4 -7
- package/build/fetchers/stats.d.ts.map +1 -1
- package/build/fetchers/stats.js +3 -7
- package/build/fetchers/top-languages.d.ts.map +1 -1
- package/build/fetchers/top-languages.js +0 -1
- package/build/fetchers/types.d.ts +126 -0
- package/build/fetchers/types.d.ts.map +1 -0
- package/build/fetchers/types.js +1 -0
- package/build/fetchers/wakatime.d.ts.map +1 -1
- package/build/fetchers/wakatime.js +0 -1
- package/build/translations.d.ts +627 -24
- package/build/translations.d.ts.map +1 -1
- package/build/translations.js +7 -8
- package/package.json +4 -4
- package/src/_emoji-name-map.d.ts +10 -0
- package/src/api/gist.js +36 -2
- package/src/api/index.js +19 -2
- package/src/api/pin.js +18 -2
- package/src/api/top-langs.js +38 -6
- package/src/api/wakatime.js +36 -2
- package/src/{calculateRank.js → calculateRank.ts} +29 -19
- package/src/cards/gist.js +0 -2
- package/src/cards/repo.js +30 -22
- package/src/cards/stats.js +22 -10
- package/src/cards/top-languages.js +49 -13
- package/src/cards/wakatime.js +33 -10
- package/src/common/Card.ts +338 -0
- package/src/common/I18n.ts +49 -0
- package/src/common/color.ts +212 -0
- package/src/common/config.ts +83 -0
- package/src/common/constants.ts +10 -0
- package/src/common/{error.js → error.ts} +20 -14
- package/src/common/{fmt.js → fmt.ts} +27 -19
- package/src/common/{html.js → html.ts} +3 -8
- package/src/common/http.ts +31 -0
- package/src/common/{icons.js → icons.ts} +9 -7
- package/src/common/languageColors.json +9 -1
- package/src/common/{ops.js → ops.ts} +48 -52
- package/src/common/{render.js → render.ts} +208 -98
- package/src/common/retryer.ts +125 -0
- package/src/fetchers/gist.ts +136 -0
- package/src/fetchers/repo.js +4 -8
- package/src/fetchers/stats.js +3 -8
- package/src/fetchers/top-languages.js +0 -2
- package/src/fetchers/{types.d.ts → types.ts} +1 -1
- package/src/fetchers/wakatime.js +0 -2
- package/src/{translations.js → translations.ts} +14 -10
- package/src/common/Card.js +0 -275
- package/src/common/I18n.js +0 -42
- package/src/common/color.js +0 -145
- package/src/common/config.js +0 -78
- package/src/common/constants.js +0 -9
- package/src/common/http.js +0 -21
- package/src/common/retryer.js +0 -104
- package/src/fetchers/gist.js +0 -114
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
|
|
3
1
|
import { Card } from "../common/Card.js";
|
|
4
2
|
import { I18n } from "../common/I18n.js";
|
|
5
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
fallbackColor,
|
|
5
|
+
getCardColors,
|
|
6
|
+
isPrefixedHexColor,
|
|
7
|
+
} from "../common/color.js";
|
|
6
8
|
import { formatBytes } from "../common/fmt.js";
|
|
9
|
+
import { encodeHTML } from "../common/html.js";
|
|
7
10
|
import { chunkArray, clampValue, lowercaseTrim } from "../common/ops.js";
|
|
8
11
|
import {
|
|
9
12
|
createProgressNode,
|
|
@@ -246,8 +249,8 @@ const createProgressTextNode = ({
|
|
|
246
249
|
|
|
247
250
|
return `
|
|
248
251
|
<g class="stagger" style="animation-delay: ${staggerDelay}ms">
|
|
249
|
-
<text data-testid="lang-name" x="2" y="15" class="lang-name">${name}</text>
|
|
250
|
-
${hideValues ? "" : `<text x="${progressTextX}" y="34" class="lang-name">${displayValue}</text>`}
|
|
252
|
+
<text data-testid="lang-name" x="2" y="15" class="lang-name">${encodeHTML(name)}</text>
|
|
253
|
+
${hideValues ? "" : `<text x="${progressTextX}" y="34" class="lang-name">${encodeHTML(displayValue)}</text>`}
|
|
251
254
|
${createProgressNode({
|
|
252
255
|
x: 0,
|
|
253
256
|
y: 25,
|
|
@@ -287,11 +290,15 @@ const createCompactLangNode = ({
|
|
|
287
290
|
const staggerDelay = (index + 3) * 150;
|
|
288
291
|
const color = lang.color || "#858585";
|
|
289
292
|
|
|
293
|
+
if (!isPrefixedHexColor(color)) {
|
|
294
|
+
throw new Error(`Invalid language color: "${color}"`);
|
|
295
|
+
}
|
|
296
|
+
|
|
290
297
|
return `
|
|
291
298
|
<g class="stagger" style="animation-delay: ${staggerDelay}ms">
|
|
292
299
|
<circle cx="5" cy="6" r="5" fill="${color}" />
|
|
293
300
|
<text data-testid="lang-name" x="15" y="10" class='lang-name'>
|
|
294
|
-
${lang.name} ${hideProgress || hideValues ? "" : displayValue}
|
|
301
|
+
${encodeHTML(lang.name)} ${hideProgress || hideValues ? "" : encodeHTML(displayValue)}
|
|
295
302
|
</text>
|
|
296
303
|
</g>
|
|
297
304
|
`;
|
|
@@ -441,6 +448,11 @@ const renderCompactLayout = (
|
|
|
441
448
|
let progressOffset = 0;
|
|
442
449
|
const compactProgressBar = langs
|
|
443
450
|
.map((lang) => {
|
|
451
|
+
const langColor = lang.color || DEFAULT_LANG_COLOR;
|
|
452
|
+
if (!isPrefixedHexColor(langColor)) {
|
|
453
|
+
throw new Error(`Invalid language color: "${langColor}"`);
|
|
454
|
+
}
|
|
455
|
+
|
|
444
456
|
const percentage = parseFloat(
|
|
445
457
|
((lang.size / totalLanguageSize) * offsetWidth).toFixed(2),
|
|
446
458
|
);
|
|
@@ -455,7 +467,7 @@ const renderCompactLayout = (
|
|
|
455
467
|
y="0"
|
|
456
468
|
width="${progress}"
|
|
457
469
|
height="8"
|
|
458
|
-
fill="${
|
|
470
|
+
fill="${langColor}"
|
|
459
471
|
/>
|
|
460
472
|
`;
|
|
461
473
|
progressOffset += percentage;
|
|
@@ -516,18 +528,23 @@ const renderDonutVerticalLayout = (
|
|
|
516
528
|
|
|
517
529
|
// Generate each donut vertical chart part
|
|
518
530
|
for (const lang of langs) {
|
|
531
|
+
const langColor = lang.color || DEFAULT_LANG_COLOR;
|
|
532
|
+
if (!isPrefixedHexColor(langColor)) {
|
|
533
|
+
throw new Error(`Invalid language color: "${langColor}"`);
|
|
534
|
+
}
|
|
535
|
+
|
|
519
536
|
const percentage = (lang.size / totalLanguageSize) * 100;
|
|
520
537
|
const circleLength = totalCircleLength * (percentage / 100);
|
|
521
538
|
const delay = startDelayCoefficient * 100;
|
|
522
539
|
|
|
523
540
|
circles.push(`
|
|
524
541
|
<g class="stagger" style="animation-delay: ${delay}ms">
|
|
525
|
-
<circle
|
|
542
|
+
<circle
|
|
526
543
|
cx="150"
|
|
527
544
|
cy="100"
|
|
528
545
|
r="${radius}"
|
|
529
546
|
fill="transparent"
|
|
530
|
-
stroke="${
|
|
547
|
+
stroke="${langColor}"
|
|
531
548
|
stroke-width="25"
|
|
532
549
|
stroke-dasharray="${totalCircleLength}"
|
|
533
550
|
stroke-dashoffset="${indent}"
|
|
@@ -591,6 +608,11 @@ const renderPieLayout = (langs, totalLanguageSize, statsFormat, hideValues) => {
|
|
|
591
608
|
|
|
592
609
|
// Generate each pie chart part
|
|
593
610
|
for (const lang of langs) {
|
|
611
|
+
const langColor = lang.color || DEFAULT_LANG_COLOR;
|
|
612
|
+
if (!isPrefixedHexColor(langColor)) {
|
|
613
|
+
throw new Error(`Invalid language color: "${langColor}"`);
|
|
614
|
+
}
|
|
615
|
+
|
|
594
616
|
if (langs.length === 1) {
|
|
595
617
|
paths.push(`
|
|
596
618
|
<circle
|
|
@@ -598,7 +620,7 @@ const renderPieLayout = (langs, totalLanguageSize, statsFormat, hideValues) => {
|
|
|
598
620
|
cy="${centerY}"
|
|
599
621
|
r="${radius}"
|
|
600
622
|
stroke="none"
|
|
601
|
-
fill="${
|
|
623
|
+
fill="${langColor}"
|
|
602
624
|
data-testid="lang-pie"
|
|
603
625
|
size="100"
|
|
604
626
|
/>
|
|
@@ -631,7 +653,7 @@ const renderPieLayout = (langs, totalLanguageSize, statsFormat, hideValues) => {
|
|
|
631
653
|
data-testid="lang-pie"
|
|
632
654
|
size="${percentage}"
|
|
633
655
|
d="M ${centerX} ${centerY} L ${startPoint.x} ${startPoint.y} A ${radius} ${radius} 0 ${largeArcFlag} 1 ${endPoint.x} ${endPoint.y} Z"
|
|
634
|
-
fill="${
|
|
656
|
+
fill="${langColor}"
|
|
635
657
|
/>
|
|
636
658
|
</g>
|
|
637
659
|
`);
|
|
@@ -718,12 +740,22 @@ const renderDonutLayout = (
|
|
|
718
740
|
statsFormat,
|
|
719
741
|
hideValues,
|
|
720
742
|
) => {
|
|
743
|
+
if (!Number.isFinite(width)) {
|
|
744
|
+
throw new Error(`Invalid width: "${width}"`);
|
|
745
|
+
}
|
|
746
|
+
|
|
721
747
|
const centerX = width / 3;
|
|
722
748
|
const centerY = width / 3;
|
|
723
749
|
const radius = centerX - 60;
|
|
724
750
|
const strokeWidth = 12;
|
|
725
751
|
|
|
726
|
-
const colors = langs.map((lang) =>
|
|
752
|
+
const colors = langs.map((lang) => {
|
|
753
|
+
const langColor = lang.color || DEFAULT_LANG_COLOR;
|
|
754
|
+
if (!isPrefixedHexColor(langColor)) {
|
|
755
|
+
throw new Error(`Invalid language color: "${langColor}"`);
|
|
756
|
+
}
|
|
757
|
+
return langColor;
|
|
758
|
+
});
|
|
727
759
|
const langsPercents = langs.map((lang) =>
|
|
728
760
|
parseFloat(((lang.size / totalLanguageSize) * 100).toFixed(2)),
|
|
729
761
|
);
|
|
@@ -785,10 +817,14 @@ const renderDonutLayout = (
|
|
|
785
817
|
* @returns {string} No languages data SVG node string.
|
|
786
818
|
*/
|
|
787
819
|
const noLanguagesDataNode = ({ color, text, layout }) => {
|
|
820
|
+
if (!isPrefixedHexColor(color)) {
|
|
821
|
+
throw new Error(`Invalid text color: "${color}"`);
|
|
822
|
+
}
|
|
823
|
+
|
|
788
824
|
return `
|
|
789
825
|
<text x="${
|
|
790
826
|
layout === "pie" || layout === "donut-vertical" ? CARD_PADDING : 0
|
|
791
|
-
}" y="11" class="stat bold" fill="${color}">${text}</text>
|
|
827
|
+
}" y="11" class="stat bold" fill="${color}">${encodeHTML(text)}</text>
|
|
792
828
|
`;
|
|
793
829
|
};
|
|
794
830
|
|
package/src/cards/wakatime.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
|
|
3
1
|
import { Card } from "../common/Card.js";
|
|
4
2
|
import { I18n } from "../common/I18n.js";
|
|
5
|
-
import { getCardColors } from "../common/color.js";
|
|
3
|
+
import { getCardColors, isPrefixedHexColor } from "../common/color.js";
|
|
4
|
+
import { encodeHTML } from "../common/html.js";
|
|
6
5
|
import languageColors from "../common/languageColors.json" with { type: "json" };
|
|
7
6
|
import { clampValue, lowercaseTrim } from "../common/ops.js";
|
|
8
7
|
import { createProgressNode, flexLayout } from "../common/render.js";
|
|
@@ -26,8 +25,12 @@ const TOTAL_TEXT_WIDTH = 275;
|
|
|
26
25
|
* @returns {string} No coding activity SVG node string.
|
|
27
26
|
*/
|
|
28
27
|
const noCodingActivityNode = ({ color, text }) => {
|
|
28
|
+
if (!isPrefixedHexColor(color)) {
|
|
29
|
+
throw new Error(`Invalid text color: "${color}"`);
|
|
30
|
+
}
|
|
31
|
+
|
|
29
32
|
return `
|
|
30
|
-
<text x="25" y="11" class="stat bold" fill="${color}">${text}</text>
|
|
33
|
+
<text x="25" y="11" class="stat bold" fill="${color}">${encodeHTML(text)}</text>
|
|
31
34
|
`;
|
|
32
35
|
};
|
|
33
36
|
|
|
@@ -60,6 +63,13 @@ const formatLanguageValue = ({ display_format, lang }) => {
|
|
|
60
63
|
* @returns {string} The compact layout language SVG node.
|
|
61
64
|
*/
|
|
62
65
|
const createCompactLangNode = ({ lang, x, y, display_format }) => {
|
|
66
|
+
if (!Number.isFinite(x)) {
|
|
67
|
+
throw new Error(`Invalid x: "${x}"`);
|
|
68
|
+
}
|
|
69
|
+
if (!Number.isFinite(y)) {
|
|
70
|
+
throw new Error(`Invalid y: "${y}"`);
|
|
71
|
+
}
|
|
72
|
+
|
|
63
73
|
// @ts-ignore
|
|
64
74
|
const color = languageColors[lang.name] || "#858585";
|
|
65
75
|
const value = formatLanguageValue({ display_format, lang });
|
|
@@ -68,7 +78,7 @@ const createCompactLangNode = ({ lang, x, y, display_format }) => {
|
|
|
68
78
|
<g transform="translate(${x}, ${y})">
|
|
69
79
|
<circle cx="5" cy="6" r="5" fill="${color}" />
|
|
70
80
|
<text data-testid="lang-name" x="15" y="10" class='lang-name'>
|
|
71
|
-
${lang.name} - ${value}
|
|
81
|
+
${encodeHTML(lang.name)} - ${encodeHTML(value)}
|
|
72
82
|
</text>
|
|
73
83
|
</g>
|
|
74
84
|
`;
|
|
@@ -127,6 +137,13 @@ const createTextNode = ({
|
|
|
127
137
|
progressBarBackgroundColor,
|
|
128
138
|
progressBarWidth,
|
|
129
139
|
}) => {
|
|
140
|
+
if (!Number.isFinite(index)) {
|
|
141
|
+
throw new Error(`Invalid index: "${index}"`);
|
|
142
|
+
}
|
|
143
|
+
if (!Number.isFinite(progressBarWidth)) {
|
|
144
|
+
throw new Error(`Invalid progressBarWidth: "${progressBarWidth}"`);
|
|
145
|
+
}
|
|
146
|
+
|
|
130
147
|
const staggerDelay = (index + 3) * 150;
|
|
131
148
|
const cardProgress = hideProgress
|
|
132
149
|
? null
|
|
@@ -144,12 +161,12 @@ const createTextNode = ({
|
|
|
144
161
|
|
|
145
162
|
return `
|
|
146
163
|
<g class="stagger" style="animation-delay: ${staggerDelay}ms" transform="translate(25, 0)">
|
|
147
|
-
<text class="stat bold" y="12.5" data-testid="${id}">${label}:</text>
|
|
164
|
+
<text class="stat bold" y="12.5" data-testid="${encodeHTML(id)}">${encodeHTML(label)}:</text>
|
|
148
165
|
<text
|
|
149
166
|
class="stat"
|
|
150
167
|
x="${hideProgress ? HIDDEN_PROGRESSBAR_PADDING : PROGRESSBAR_PADDING + progressBarWidth}"
|
|
151
168
|
y="12.5"
|
|
152
|
-
>${value}</text>
|
|
169
|
+
>${encodeHTML(value)}</text>
|
|
153
170
|
${cardProgress}
|
|
154
171
|
</g>
|
|
155
172
|
`;
|
|
@@ -181,11 +198,15 @@ const recalculatePercentages = (languages) => {
|
|
|
181
198
|
* @param {string} colors.textColor The text color.
|
|
182
199
|
* @returns {string} Card CSS styles.
|
|
183
200
|
*/
|
|
184
|
-
const getStyles = ({
|
|
201
|
+
const getStyles = function ({
|
|
185
202
|
// eslint-disable-next-line no-unused-vars
|
|
186
203
|
titleColor,
|
|
187
204
|
textColor,
|
|
188
|
-
})
|
|
205
|
+
}) {
|
|
206
|
+
if (!isPrefixedHexColor(textColor)) {
|
|
207
|
+
throw new Error(`Invalid text color: "${textColor}"`);
|
|
208
|
+
}
|
|
209
|
+
|
|
189
210
|
return `
|
|
190
211
|
.stat {
|
|
191
212
|
font: 600 14px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif; fill: ${textColor};
|
|
@@ -379,7 +400,9 @@ const renderWakatimeCard = (stats = {}, options = { hide: [] }) => {
|
|
|
379
400
|
progressBarColor: titleColor,
|
|
380
401
|
// @ts-ignore
|
|
381
402
|
progressBarBackgroundColor:
|
|
382
|
-
textColor === titleColor
|
|
403
|
+
textColor === titleColor
|
|
404
|
+
? "#fff0" // transparent
|
|
405
|
+
: textColor,
|
|
383
406
|
hideProgress: hide_progress,
|
|
384
407
|
progressBarWidth: normalizedWidth - TOTAL_TEXT_WIDTH,
|
|
385
408
|
});
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { isPrefixedHexColor, isValidGradient } from "./color.js";
|
|
2
|
+
import { encodeHTML } from "./html.js";
|
|
3
|
+
import { flexLayout } from "./render.js";
|
|
4
|
+
|
|
5
|
+
interface CardColors {
|
|
6
|
+
/** Card title color. */
|
|
7
|
+
titleColor?: string;
|
|
8
|
+
/** Card text color. */
|
|
9
|
+
textColor?: string;
|
|
10
|
+
/** Card icon color. */
|
|
11
|
+
iconColor?: string;
|
|
12
|
+
/** Card background color. */
|
|
13
|
+
bgColor?: string | Array<string>;
|
|
14
|
+
/** Card border color. */
|
|
15
|
+
borderColor?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
class Card {
|
|
19
|
+
width: number;
|
|
20
|
+
height: number;
|
|
21
|
+
hideBorder: boolean;
|
|
22
|
+
hideTitle: boolean;
|
|
23
|
+
border_radius: number;
|
|
24
|
+
colors: CardColors;
|
|
25
|
+
title: string;
|
|
26
|
+
css: string;
|
|
27
|
+
paddingX: number;
|
|
28
|
+
paddingY: number;
|
|
29
|
+
titlePrefixIcon: string | undefined;
|
|
30
|
+
animations: boolean;
|
|
31
|
+
a11yTitle: string;
|
|
32
|
+
a11yDesc: string;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Creates a new card instance.
|
|
36
|
+
*
|
|
37
|
+
* The caller must ensure that the passed `titlePrefixIcon` is properly sanitized!
|
|
38
|
+
*
|
|
39
|
+
* @param props Card arguments.
|
|
40
|
+
* @param props.width Card width.
|
|
41
|
+
* @param props.height Card height.
|
|
42
|
+
* @param props.border_radius Card border radius.
|
|
43
|
+
* @param props.colors Card colors arguments.
|
|
44
|
+
* @param props.customTitle Card custom title.
|
|
45
|
+
* @param props.defaultTitle Card default title.
|
|
46
|
+
* @param props.titlePrefixIcon Sanitized card title prefix icon.
|
|
47
|
+
*/
|
|
48
|
+
constructor({
|
|
49
|
+
width = 100,
|
|
50
|
+
height = 100,
|
|
51
|
+
border_radius = 4.5,
|
|
52
|
+
colors = {},
|
|
53
|
+
customTitle,
|
|
54
|
+
defaultTitle = "",
|
|
55
|
+
titlePrefixIcon,
|
|
56
|
+
}: {
|
|
57
|
+
width?: number;
|
|
58
|
+
height?: number;
|
|
59
|
+
border_radius?: number;
|
|
60
|
+
colors?: CardColors;
|
|
61
|
+
customTitle?: string;
|
|
62
|
+
defaultTitle?: string;
|
|
63
|
+
titlePrefixIcon?: string;
|
|
64
|
+
}) {
|
|
65
|
+
this.width = width;
|
|
66
|
+
this.height = height;
|
|
67
|
+
|
|
68
|
+
this.hideBorder = false;
|
|
69
|
+
this.hideTitle = false;
|
|
70
|
+
|
|
71
|
+
this.border_radius = parseFloat(String(border_radius));
|
|
72
|
+
|
|
73
|
+
// returns theme based colors with proper overrides and defaults
|
|
74
|
+
this.colors = colors;
|
|
75
|
+
this.title = customTitle === undefined ? defaultTitle : customTitle;
|
|
76
|
+
|
|
77
|
+
this.css = "";
|
|
78
|
+
|
|
79
|
+
this.paddingX = 25;
|
|
80
|
+
this.paddingY = 35;
|
|
81
|
+
this.titlePrefixIcon = titlePrefixIcon;
|
|
82
|
+
this.animations = true;
|
|
83
|
+
this.a11yTitle = "";
|
|
84
|
+
this.a11yDesc = "";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
disableAnimations(): void {
|
|
88
|
+
this.animations = false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @param props The props object.
|
|
93
|
+
* @param props.title Accessibility title.
|
|
94
|
+
* @param props.desc Accessibility description.
|
|
95
|
+
*/
|
|
96
|
+
setAccessibilityLabel({
|
|
97
|
+
title,
|
|
98
|
+
desc,
|
|
99
|
+
}: {
|
|
100
|
+
title: string;
|
|
101
|
+
desc: string;
|
|
102
|
+
}): void {
|
|
103
|
+
this.a11yTitle = title;
|
|
104
|
+
this.a11yDesc = desc;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The caller must ensure that the passed `css` string is properly sanitized!
|
|
109
|
+
*
|
|
110
|
+
* @param value The sanitized CSS to add to the card.
|
|
111
|
+
*/
|
|
112
|
+
setCSS(value: string): void {
|
|
113
|
+
this.css = value;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* @param value Whether to hide the border or not.
|
|
118
|
+
*/
|
|
119
|
+
setHideBorder(value: boolean): void {
|
|
120
|
+
this.hideBorder = value;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @param value Whether to hide the title or not.
|
|
125
|
+
*/
|
|
126
|
+
setHideTitle(value: boolean): void {
|
|
127
|
+
if (value && !this.hideTitle) {
|
|
128
|
+
this.height -= 30;
|
|
129
|
+
}
|
|
130
|
+
if (!value && this.hideTitle) {
|
|
131
|
+
this.height += 30;
|
|
132
|
+
}
|
|
133
|
+
this.hideTitle = value;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* @param text The title to set.
|
|
138
|
+
*/
|
|
139
|
+
setTitle(text: string): void {
|
|
140
|
+
this.title = text;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* @returns The rendered card title.
|
|
145
|
+
*/
|
|
146
|
+
renderTitle(): string {
|
|
147
|
+
const titleText = `
|
|
148
|
+
<text
|
|
149
|
+
x="0"
|
|
150
|
+
y="0"
|
|
151
|
+
class="header"
|
|
152
|
+
data-testid="header"
|
|
153
|
+
>${encodeHTML(this.title)}</text>
|
|
154
|
+
`;
|
|
155
|
+
|
|
156
|
+
const prefixIcon = `
|
|
157
|
+
<svg
|
|
158
|
+
class="icon"
|
|
159
|
+
x="0"
|
|
160
|
+
y="-13"
|
|
161
|
+
viewBox="0 0 16 16"
|
|
162
|
+
version="1.1"
|
|
163
|
+
width="16"
|
|
164
|
+
height="16"
|
|
165
|
+
>
|
|
166
|
+
${String(this.titlePrefixIcon)}
|
|
167
|
+
</svg>
|
|
168
|
+
`;
|
|
169
|
+
return `
|
|
170
|
+
<g
|
|
171
|
+
data-testid="card-title"
|
|
172
|
+
transform="translate(${this.paddingX}, ${this.paddingY})"
|
|
173
|
+
>
|
|
174
|
+
${flexLayout({
|
|
175
|
+
items: [this.titlePrefixIcon ? prefixIcon : "", titleText],
|
|
176
|
+
gap: 25,
|
|
177
|
+
}).join("")}
|
|
178
|
+
</g>
|
|
179
|
+
`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* @returns The rendered card gradient.
|
|
184
|
+
*/
|
|
185
|
+
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>
|
|
196
|
+
<linearGradient
|
|
197
|
+
id="gradient"
|
|
198
|
+
gradientTransform="rotate(${String(this.colors.bgColor[0])})"
|
|
199
|
+
gradientUnits="userSpaceOnUse"
|
|
200
|
+
>
|
|
201
|
+
${gradients
|
|
202
|
+
.map((grad, index) => {
|
|
203
|
+
const offset = (index * 100) / (gradients.length - 1);
|
|
204
|
+
return `<stop offset="${offset}%" stop-color="#${grad}" />`;
|
|
205
|
+
})
|
|
206
|
+
.join(",")}
|
|
207
|
+
</linearGradient>
|
|
208
|
+
</defs>
|
|
209
|
+
`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Retrieves css animations for a card.
|
|
214
|
+
*
|
|
215
|
+
* @returns Animation css.
|
|
216
|
+
*/
|
|
217
|
+
getAnimations = (): string => {
|
|
218
|
+
return `
|
|
219
|
+
/* Animations */
|
|
220
|
+
@keyframes scaleInAnimation {
|
|
221
|
+
from {
|
|
222
|
+
transform: translate(-5px, 5px) scale(0);
|
|
223
|
+
}
|
|
224
|
+
to {
|
|
225
|
+
transform: translate(-5px, 5px) scale(1);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
@keyframes fadeInAnimation {
|
|
229
|
+
from {
|
|
230
|
+
opacity: 0;
|
|
231
|
+
}
|
|
232
|
+
to {
|
|
233
|
+
opacity: 1;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
`;
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* The caller must ensure that the passed `body` string is properly sanitized!
|
|
241
|
+
*
|
|
242
|
+
* @param body The sanitized inner body of the card.
|
|
243
|
+
* @returns The rendered card.
|
|
244
|
+
*/
|
|
245
|
+
render(body: string): string {
|
|
246
|
+
if (!Number.isFinite(this.border_radius)) {
|
|
247
|
+
throw new Error(`Invalid border radius: "${this.border_radius}"`);
|
|
248
|
+
}
|
|
249
|
+
if (
|
|
250
|
+
this.colors.titleColor !== undefined &&
|
|
251
|
+
!isPrefixedHexColor(this.colors.titleColor)
|
|
252
|
+
) {
|
|
253
|
+
throw new Error(`Invalid title color: "${this.colors.titleColor}"`);
|
|
254
|
+
}
|
|
255
|
+
if (
|
|
256
|
+
this.colors.borderColor !== undefined &&
|
|
257
|
+
!isPrefixedHexColor(this.colors.borderColor)
|
|
258
|
+
) {
|
|
259
|
+
throw new Error(`Invalid border color: "${this.colors.borderColor}"`);
|
|
260
|
+
}
|
|
261
|
+
if (
|
|
262
|
+
this.colors.bgColor !== undefined &&
|
|
263
|
+
!(typeof this.colors.bgColor === "object"
|
|
264
|
+
? isValidGradient(this.colors.bgColor)
|
|
265
|
+
: isPrefixedHexColor(this.colors.bgColor))
|
|
266
|
+
) {
|
|
267
|
+
throw new Error(
|
|
268
|
+
`Invalid background color: ${String(this.colors.bgColor)}`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return `
|
|
273
|
+
<svg
|
|
274
|
+
width="${this.width}"
|
|
275
|
+
height="${this.height}"
|
|
276
|
+
viewBox="0 0 ${this.width} ${this.height}"
|
|
277
|
+
fill="none"
|
|
278
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
279
|
+
role="img"
|
|
280
|
+
aria-labelledby="descId"
|
|
281
|
+
>
|
|
282
|
+
<title id="titleId">${encodeHTML(this.a11yTitle)}</title>
|
|
283
|
+
<desc id="descId">${encodeHTML(this.a11yDesc)}</desc>
|
|
284
|
+
<style>
|
|
285
|
+
.header {
|
|
286
|
+
font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif;
|
|
287
|
+
fill: ${String(this.colors.titleColor)};
|
|
288
|
+
animation: fadeInAnimation 0.8s ease-in-out forwards;
|
|
289
|
+
}
|
|
290
|
+
@supports(-moz-appearance: auto) {
|
|
291
|
+
/* Selector detects Firefox */
|
|
292
|
+
.header { font-size: 15.5px; }
|
|
293
|
+
}
|
|
294
|
+
${this.css}
|
|
295
|
+
|
|
296
|
+
${this.getAnimations()}
|
|
297
|
+
${
|
|
298
|
+
this.animations
|
|
299
|
+
? ""
|
|
300
|
+
: `* { animation-duration: 0s !important; animation-delay: 0s !important; }`
|
|
301
|
+
}
|
|
302
|
+
</style>
|
|
303
|
+
|
|
304
|
+
${this.renderGradient()}
|
|
305
|
+
|
|
306
|
+
<rect
|
|
307
|
+
data-testid="card-bg"
|
|
308
|
+
x="0.5"
|
|
309
|
+
y="0.5"
|
|
310
|
+
rx="${this.border_radius}"
|
|
311
|
+
height="99%"
|
|
312
|
+
stroke="${String(this.colors.borderColor)}"
|
|
313
|
+
width="${this.width - 1}"
|
|
314
|
+
fill="${
|
|
315
|
+
typeof this.colors.bgColor === "object"
|
|
316
|
+
? "url(#gradient)"
|
|
317
|
+
: String(this.colors.bgColor)
|
|
318
|
+
}"
|
|
319
|
+
stroke-opacity="${this.hideBorder ? 0 : 1}"
|
|
320
|
+
/>
|
|
321
|
+
|
|
322
|
+
${this.hideTitle ? "" : this.renderTitle()}
|
|
323
|
+
|
|
324
|
+
<g
|
|
325
|
+
data-testid="main-card-body"
|
|
326
|
+
transform="translate(0, ${
|
|
327
|
+
this.hideTitle ? this.paddingX : this.paddingY + 20
|
|
328
|
+
})"
|
|
329
|
+
>
|
|
330
|
+
${body}
|
|
331
|
+
</g>
|
|
332
|
+
</svg>
|
|
333
|
+
`;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export { Card };
|
|
338
|
+
export default Card;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const FALLBACK_LOCALE = "en";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* I18n translation class.
|
|
5
|
+
*/
|
|
6
|
+
class I18n<Translations extends Record<string, Record<string, string>>> {
|
|
7
|
+
locale: string;
|
|
8
|
+
translations: Translations;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param props Constructor arguments.
|
|
12
|
+
* @param props.locale Locale.
|
|
13
|
+
* @param props.translations Translations.
|
|
14
|
+
*/
|
|
15
|
+
constructor({
|
|
16
|
+
locale,
|
|
17
|
+
translations,
|
|
18
|
+
}: {
|
|
19
|
+
locale?: string;
|
|
20
|
+
translations: Translations;
|
|
21
|
+
}) {
|
|
22
|
+
this.locale = locale || FALLBACK_LOCALE;
|
|
23
|
+
this.translations = translations;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get translation.
|
|
28
|
+
*
|
|
29
|
+
* @param str String to translate.
|
|
30
|
+
* @returns Translated string.
|
|
31
|
+
*/
|
|
32
|
+
t(str: keyof Translations & string): string {
|
|
33
|
+
const translation = this.translations[str];
|
|
34
|
+
if (!translation) {
|
|
35
|
+
throw new Error(`${str} Translation string not found`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const localized = translation[this.locale];
|
|
39
|
+
if (!localized) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`'${str}' translation not found for locale '${this.locale}'`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return localized;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export { I18n };
|