@stats-forge/github-stats-forge-core 0.1.0 → 0.3.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.
- package/build/api/contributed-to.d.ts +23 -0
- package/build/api/contributed-to.d.ts.map +1 -0
- package/build/api/contributed-to.js +43 -0
- package/build/api/gist.d.ts +8 -15
- package/build/api/gist.d.ts.map +1 -1
- package/build/api/gist.js +14 -29
- package/build/api/handler.d.ts +16 -0
- package/build/api/handler.d.ts.map +1 -0
- package/build/api/handler.js +29 -0
- package/build/api/index.d.ts +1 -0
- package/build/api/index.d.ts.map +1 -1
- package/build/api/index.js +1 -0
- package/build/api/params.d.ts +11 -5
- package/build/api/params.d.ts.map +1 -1
- package/build/api/params.js +26 -6
- package/build/api/pin.d.ts +11 -15
- package/build/api/pin.d.ts.map +1 -1
- package/build/api/pin.js +34 -46
- package/build/api/stats.d.ts +15 -22
- package/build/api/stats.d.ts.map +1 -1
- package/build/api/stats.js +59 -72
- package/build/api/top-langs.d.ts +11 -22
- package/build/api/top-langs.d.ts.map +1 -1
- package/build/api/top-langs.js +34 -51
- package/build/api/wakatime.d.ts +10 -24
- package/build/api/wakatime.d.ts.map +1 -1
- package/build/api/wakatime.js +27 -47
- package/build/cards/contributed-to.d.ts +19 -0
- package/build/cards/contributed-to.d.ts.map +1 -0
- package/build/cards/contributed-to.js +242 -0
- package/build/cards/gist.d.ts +1 -0
- package/build/cards/gist.d.ts.map +1 -1
- package/build/cards/gist.js +13 -9
- package/build/cards/repo.d.ts +12 -1
- package/build/cards/repo.d.ts.map +1 -1
- package/build/cards/repo.js +40 -18
- package/build/cards/stats.d.ts +15 -3
- package/build/cards/stats.d.ts.map +1 -1
- package/build/cards/stats.js +91 -76
- package/build/cards/top-languages.d.ts +13 -2
- package/build/cards/top-languages.d.ts.map +1 -1
- package/build/cards/top-languages.js +26 -9
- package/build/cards/wakatime.d.ts +13 -2
- package/build/cards/wakatime.d.ts.map +1 -1
- package/build/cards/wakatime.js +22 -9
- package/build/common/Card.d.ts +9 -0
- package/build/common/Card.d.ts.map +1 -1
- package/build/common/Card.js +44 -9
- package/build/common/I18n.d.ts +1 -1
- package/build/common/I18n.d.ts.map +1 -1
- package/build/common/I18n.js +4 -2
- package/build/common/brand.d.ts +76 -0
- package/build/common/brand.d.ts.map +1 -0
- package/build/common/brand.js +73 -0
- package/build/common/constants.d.ts +3 -1
- package/build/common/constants.d.ts.map +1 -1
- package/build/common/constants.js +3 -1
- package/build/common/date.d.ts +50 -3
- package/build/common/date.d.ts.map +1 -1
- package/build/common/date.js +104 -2
- package/build/common/icons.d.ts +4 -2
- package/build/common/icons.d.ts.map +1 -1
- package/build/common/icons.js +10 -5
- package/build/common/ops.d.ts +8 -3
- package/build/common/ops.d.ts.map +1 -1
- package/build/common/ops.js +8 -14
- package/build/common/render.d.ts +7 -1
- package/build/common/render.d.ts.map +1 -1
- package/build/common/render.js +69 -61
- package/build/fetchers/contributed-to.d.ts +62 -0
- package/build/fetchers/contributed-to.d.ts.map +1 -0
- package/build/fetchers/contributed-to.js +184 -0
- package/build/fetchers/graphql-error.d.ts +13 -0
- package/build/fetchers/graphql-error.d.ts.map +1 -0
- package/build/fetchers/graphql-error.js +29 -0
- package/build/fetchers/index.d.ts +2 -1
- package/build/fetchers/index.d.ts.map +1 -1
- package/build/fetchers/index.js +1 -0
- package/build/fetchers/stats.d.ts +14 -8
- package/build/fetchers/stats.d.ts.map +1 -1
- package/build/fetchers/stats.js +72 -195
- package/build/fetchers/top-languages.d.ts.map +1 -1
- package/build/fetchers/top-languages.js +3 -18
- package/build/fetchers/types.d.ts +24 -0
- package/build/fetchers/types.d.ts.map +1 -1
- package/build/graphql/contributionsCollection.d.ts +9 -0
- package/build/graphql/contributionsCollection.d.ts.map +1 -0
- package/build/graphql/contributionsCollection.js +12 -0
- package/build/graphql/contributionsDocument.d.ts +5 -5
- package/build/graphql/contributionsDocument.d.ts.map +1 -1
- package/build/graphql/contributionsDocument.js +8 -15
- package/build/graphql/generated/contributed-to.d.ts +22 -0
- package/build/graphql/generated/contributed-to.d.ts.map +1 -0
- package/build/graphql/generated/contributed-to.js +11 -0
- package/build/graphql/generated/stats.d.ts +13 -1
- package/build/graphql/generated/stats.d.ts.map +1 -1
- package/build/graphql/generated/stats.js +2 -2
- package/build/graphql/graphqlDocument.d.ts +1 -1
- package/build/graphql/reposContributedToDocument.d.ts.map +1 -1
- package/build/graphql/reposContributedToDocument.js +11 -5
- package/build/translations.d.ts +64 -1
- package/build/translations.d.ts.map +1 -1
- package/build/translations.js +65 -1
- package/package.json +1 -1
- package/src/api/contributed-to.ts +84 -0
- package/src/api/gist.ts +20 -37
- package/src/api/handler.ts +42 -0
- package/src/api/index.ts +1 -0
- package/src/api/params.ts +48 -9
- package/src/api/pin.ts +33 -45
- package/src/api/stats.ts +78 -89
- package/src/api/top-langs.ts +34 -56
- package/src/api/wakatime.ts +33 -54
- package/src/cards/contributed-to.ts +373 -0
- package/src/cards/gist.ts +17 -8
- package/src/cards/repo.ts +44 -20
- package/src/cards/stats.ts +119 -90
- package/src/cards/top-languages.ts +27 -18
- package/src/cards/wakatime.ts +23 -13
- package/src/common/Card.ts +56 -11
- package/src/common/I18n.ts +4 -2
- package/src/common/brand.ts +109 -0
- package/src/common/constants.ts +4 -1
- package/src/common/date.ts +135 -3
- package/src/common/icons.ts +10 -5
- package/src/common/ops.ts +10 -19
- package/src/common/render.ts +75 -65
- package/src/fetchers/contributed-to.ts +271 -0
- package/src/fetchers/graphql-error.ts +36 -0
- package/src/fetchers/index.ts +3 -0
- package/src/fetchers/stats.ts +123 -274
- package/src/fetchers/top-languages.ts +5 -19
- package/src/fetchers/types.ts +26 -0
- package/src/graphql/contributionsCollection.ts +18 -0
- package/src/graphql/contributionsDocument.ts +11 -18
- package/src/graphql/generated/contributed-to.ts +25 -0
- package/src/graphql/generated/stats.ts +19 -6
- package/src/graphql/graphqlDocument.ts +1 -1
- package/src/graphql/queries/contributed-to.graphql +8 -0
- package/src/graphql/queries/stats.graphql +14 -2
- package/src/graphql/reposContributedToDocument.ts +11 -9
- package/src/translations.ts +77 -0
package/src/api/wakatime.ts
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import * as z from 'zod/mini';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import type { CardConfig } from '../common/config.ts';
|
|
3
|
+
import { renderWakatimeCard } from '../cards/wakatime.ts';
|
|
5
4
|
import { fetchWakatimeStats } from '../fetchers/wakatime.ts';
|
|
6
5
|
|
|
7
|
-
import
|
|
8
|
-
import { errorResult } from './api-result.ts';
|
|
9
|
-
import type { ApiQuery } from './params.ts';
|
|
6
|
+
import { cardHandler } from './handler.ts';
|
|
10
7
|
import {
|
|
11
8
|
booleanParam,
|
|
12
9
|
enumParam,
|
|
@@ -14,8 +11,6 @@ import {
|
|
|
14
11
|
localeParam,
|
|
15
12
|
looseIntParam,
|
|
16
13
|
numberParam,
|
|
17
|
-
parseColorParams,
|
|
18
|
-
parseParams,
|
|
19
14
|
rawParam,
|
|
20
15
|
safeParam,
|
|
21
16
|
} from './params.ts';
|
|
@@ -30,18 +25,15 @@ const wakatimeQuery = z.object({
|
|
|
30
25
|
hide_progress: booleanParam,
|
|
31
26
|
custom_title: rawParam,
|
|
32
27
|
locale: localeParam,
|
|
33
|
-
layout: enumParam(
|
|
28
|
+
layout: enumParam(renderWakatimeCard.OPTIONS.layout),
|
|
34
29
|
langs_count: looseIntParam,
|
|
35
30
|
hide: listParam,
|
|
36
31
|
api_domain: rawParam,
|
|
37
32
|
border_radius: numberParam,
|
|
38
|
-
display_format: enumParam(
|
|
33
|
+
display_format: enumParam(renderWakatimeCard.OPTIONS.display_format),
|
|
39
34
|
disable_animations: booleanParam,
|
|
40
35
|
});
|
|
41
36
|
|
|
42
|
-
/** The query this endpoint accepts, checked against the schema above. */
|
|
43
|
-
type WakatimeApiQuery = ApiQuery<typeof wakatimeQuery>;
|
|
44
|
-
|
|
45
37
|
/**
|
|
46
38
|
* Render the WakaTime card for a set of query params.
|
|
47
39
|
*
|
|
@@ -49,17 +41,10 @@ type WakatimeApiQuery = ApiQuery<typeof wakatimeQuery>;
|
|
|
49
41
|
*
|
|
50
42
|
* @returns The rendered card, or a rendered error.
|
|
51
43
|
*/
|
|
52
|
-
const renderWakatime =
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
} catch (error) {
|
|
57
|
-
// A rejected color cannot be used to draw its own error card.
|
|
58
|
-
return errorResult(error);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
try {
|
|
62
|
-
const {
|
|
44
|
+
const renderWakatime = cardHandler(
|
|
45
|
+
wakatimeQuery,
|
|
46
|
+
async (
|
|
47
|
+
{
|
|
63
48
|
username,
|
|
64
49
|
hide_border,
|
|
65
50
|
card_width,
|
|
@@ -75,39 +60,33 @@ const renderWakatime = async (query: WakatimeApiQuery, config: CardConfig): Prom
|
|
|
75
60
|
border_radius,
|
|
76
61
|
display_format,
|
|
77
62
|
disable_animations,
|
|
78
|
-
}
|
|
79
|
-
|
|
63
|
+
},
|
|
64
|
+
colors,
|
|
65
|
+
config,
|
|
66
|
+
) => {
|
|
80
67
|
const stats = await fetchWakatimeStats({ username, api_domain }, config);
|
|
81
68
|
|
|
82
|
-
return {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
};
|
|
101
|
-
} catch (error) {
|
|
102
|
-
return errorResult(error, colors);
|
|
103
|
-
}
|
|
104
|
-
};
|
|
69
|
+
return renderWakatimeCard(stats, {
|
|
70
|
+
...colors,
|
|
71
|
+
custom_title,
|
|
72
|
+
hide_title,
|
|
73
|
+
hide_border,
|
|
74
|
+
card_width,
|
|
75
|
+
hide,
|
|
76
|
+
line_height,
|
|
77
|
+
hide_progress,
|
|
78
|
+
border_radius,
|
|
79
|
+
locale,
|
|
80
|
+
layout,
|
|
81
|
+
langs_count,
|
|
82
|
+
display_format,
|
|
83
|
+
disable_animations,
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
);
|
|
105
87
|
|
|
106
88
|
/**
|
|
107
|
-
* The card,
|
|
108
|
-
* A UI reads them off the function it calls
|
|
89
|
+
* The card, and the values each of its options accepts, keyed by the option's own name.
|
|
90
|
+
* A UI reads them off the function it calls: `wakatime.OPTIONS.layout`.
|
|
109
91
|
*/
|
|
110
|
-
export const wakatime = Object.assign(renderWakatime, {
|
|
111
|
-
LAYOUTS: WAKATIME_LAYOUTS,
|
|
112
|
-
DISPLAY_FORMATS,
|
|
113
|
-
});
|
|
92
|
+
export const wakatime = Object.assign(renderWakatime, { OPTIONS: renderWakatimeCard.OPTIONS });
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { CARD_ICON, CARD_WIDTH, FONT_SIZE, firefoxFontSize, font } from '../common/brand.ts';
|
|
2
|
+
import { Card } from '../common/Card.ts';
|
|
3
|
+
import { getLightDarkColors } from '../common/color.ts';
|
|
4
|
+
import { formatYears } from '../common/date.ts';
|
|
5
|
+
import { kFormatter } from '../common/fmt.ts';
|
|
6
|
+
import { I18n } from '../common/I18n.ts';
|
|
7
|
+
import { clampValue } from '../common/ops.ts';
|
|
8
|
+
import { createProgressNode, measureText } from '../common/render.ts';
|
|
9
|
+
import type { ContributedRepo, ContributedToData } from '../fetchers/types.ts';
|
|
10
|
+
import type { Child } from '../markup/index.ts';
|
|
11
|
+
import { atRule, el, rule } from '../markup/index.ts';
|
|
12
|
+
import { contributedToCardLocales } from '../translations.ts';
|
|
13
|
+
|
|
14
|
+
import type { CardOptions, CommonCardOptions } from './options.ts';
|
|
15
|
+
|
|
16
|
+
const CARD_DEFAULT_WIDTH = CARD_WIDTH.wide;
|
|
17
|
+
const MIN_CARD_WIDTH = 340;
|
|
18
|
+
/** Padding the card keeps at its edges; matches `Card`'s own `paddingX`. */
|
|
19
|
+
const CARD_PADDING = 25;
|
|
20
|
+
|
|
21
|
+
const TITLE_FONT_SIZE = FONT_SIZE.title;
|
|
22
|
+
/** What `Card`'s title layout reserves for the prefix icon, so the title text starts past it. */
|
|
23
|
+
const TITLE_ICON_COLUMN = 25;
|
|
24
|
+
const NAME_FONT_SIZE = FONT_SIZE.meta;
|
|
25
|
+
|
|
26
|
+
/** Room kept at the right edge for the contribution count. */
|
|
27
|
+
const COUNT_WIDTH = 46;
|
|
28
|
+
const BAR_WIDTH = 130;
|
|
29
|
+
const COLUMN_GAP = 12;
|
|
30
|
+
|
|
31
|
+
/** Baseline of the first row, measured from the top of the card body. */
|
|
32
|
+
const FIRST_ROW_Y = 10;
|
|
33
|
+
const ROW_HEIGHT = 38;
|
|
34
|
+
/** A row with no year marks under it needs only its own line. */
|
|
35
|
+
const ROW_HEIGHT_NO_YEARS = 24;
|
|
36
|
+
/** How far the year marks sit below their row's baseline. */
|
|
37
|
+
const YEAR_STRIP_Y = 8;
|
|
38
|
+
const YEAR_MARK_SIZE = 7;
|
|
39
|
+
const YEAR_MARK_GAP = 3;
|
|
40
|
+
|
|
41
|
+
/** Gap between the last row and the footer's own baseline. */
|
|
42
|
+
const FOOTER_GAP = 12;
|
|
43
|
+
/** Where `Card` puts the body while the title is shown; `setHideTitle` adjusts the height by the same 30. */
|
|
44
|
+
const BODY_OFFSET_Y = 55;
|
|
45
|
+
const BOTTOM_PADDING = 18;
|
|
46
|
+
|
|
47
|
+
interface ContributedToCardOptions extends CommonCardOptions {
|
|
48
|
+
locale: string;
|
|
49
|
+
hide_title: boolean;
|
|
50
|
+
card_width: number;
|
|
51
|
+
custom_title: string;
|
|
52
|
+
disable_animations: boolean;
|
|
53
|
+
hide_years: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The card's own translation table, so `t` is checked against the keys it declares. */
|
|
57
|
+
type ContributedToI18n = I18n<ReturnType<typeof contributedToCardLocales>>;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The title, naming the account unless that makes it too wide for the card.
|
|
61
|
+
*
|
|
62
|
+
* A login is up to 39 characters, which at the title's size overruns the default width
|
|
63
|
+
* by half again — so the plain wording is the fallback rather than a truncated login.
|
|
64
|
+
*
|
|
65
|
+
* @returns The default title, which `custom_title` still overrides.
|
|
66
|
+
*/
|
|
67
|
+
const defaultTitleFor = (i18n: ContributedToI18n, contentWidth: number): string => {
|
|
68
|
+
const named = i18n.t('contributedtocard.title');
|
|
69
|
+
return measureText(named, TITLE_FONT_SIZE) <= contentWidth
|
|
70
|
+
? named
|
|
71
|
+
: i18n.t('contributedtocard.title-unnamed');
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Shortens a name to fit its column, ending it with an ellipsis when it does not.
|
|
76
|
+
*
|
|
77
|
+
* @returns The name as it is drawn.
|
|
78
|
+
*/
|
|
79
|
+
const truncateName = (name: string, maxWidth: number): string => {
|
|
80
|
+
if (measureText(name, NAME_FONT_SIZE) <= maxWidth) {
|
|
81
|
+
return name;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let trimmed = name;
|
|
85
|
+
while (trimmed.length > 1 && measureText(`${trimmed}…`, NAME_FONT_SIZE) > maxWidth) {
|
|
86
|
+
trimmed = trimmed.slice(0, -1);
|
|
87
|
+
}
|
|
88
|
+
return `${trimmed}…`;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The marks under a repository's name, one per contribution year of the account,
|
|
93
|
+
* filled for the years that repository got a contribution.
|
|
94
|
+
*
|
|
95
|
+
* The step shrinks to fit, so a long-lived account keeps the strip inside its column
|
|
96
|
+
* rather than running it under the bar.
|
|
97
|
+
*
|
|
98
|
+
* @returns The strip, positioned relative to its row's baseline.
|
|
99
|
+
*/
|
|
100
|
+
const createYearStrip = (
|
|
101
|
+
repoYears: Array<number>,
|
|
102
|
+
years: Array<number>,
|
|
103
|
+
columnWidth: number,
|
|
104
|
+
): Child => {
|
|
105
|
+
const contributed = new Set(repoYears);
|
|
106
|
+
const step = Math.min(YEAR_MARK_SIZE + YEAR_MARK_GAP, Math.floor(columnWidth / years.length));
|
|
107
|
+
const size = Math.max(step - YEAR_MARK_GAP, 3);
|
|
108
|
+
|
|
109
|
+
return el(
|
|
110
|
+
'g',
|
|
111
|
+
{ 'data-testid': 'year-strip', transform: `translate(0, ${YEAR_STRIP_Y})` },
|
|
112
|
+
years.map((year, index) =>
|
|
113
|
+
el('rect', {
|
|
114
|
+
'data-testid': contributed.has(year) ? 'year-on' : 'year-off',
|
|
115
|
+
class: contributed.has(year) ? 'year-on' : 'year-off',
|
|
116
|
+
x: index * step,
|
|
117
|
+
y: 0,
|
|
118
|
+
width: size,
|
|
119
|
+
height: size,
|
|
120
|
+
rx: 1,
|
|
121
|
+
ry: 1,
|
|
122
|
+
}),
|
|
123
|
+
),
|
|
124
|
+
);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* One repository: its name, a bar for its share of the top repository's contributions,
|
|
129
|
+
* and the count itself.
|
|
130
|
+
*
|
|
131
|
+
* @returns The row, positioned by its own transform.
|
|
132
|
+
*/
|
|
133
|
+
const createRepoRow = ({
|
|
134
|
+
repo,
|
|
135
|
+
index,
|
|
136
|
+
maxContributions,
|
|
137
|
+
years,
|
|
138
|
+
nameWidth,
|
|
139
|
+
contentWidth,
|
|
140
|
+
rowHeight,
|
|
141
|
+
}: {
|
|
142
|
+
repo: ContributedRepo;
|
|
143
|
+
index: number;
|
|
144
|
+
maxContributions: number;
|
|
145
|
+
/** Empty when the year marks are hidden. */
|
|
146
|
+
years: Array<number>;
|
|
147
|
+
nameWidth: number;
|
|
148
|
+
contentWidth: number;
|
|
149
|
+
rowHeight: number;
|
|
150
|
+
}): Child =>
|
|
151
|
+
el(
|
|
152
|
+
'g',
|
|
153
|
+
{
|
|
154
|
+
class: 'stagger',
|
|
155
|
+
style: `animation-delay: ${450 + index * 150}ms`,
|
|
156
|
+
transform: `translate(0, ${FIRST_ROW_Y + index * rowHeight})`,
|
|
157
|
+
},
|
|
158
|
+
el(
|
|
159
|
+
'text',
|
|
160
|
+
{ class: 'repo-name', 'data-testid': 'repo-name', x: 0, y: 0 },
|
|
161
|
+
truncateName(repo.nameWithOwner, nameWidth),
|
|
162
|
+
),
|
|
163
|
+
createProgressNode({
|
|
164
|
+
x: nameWidth + COLUMN_GAP,
|
|
165
|
+
y: -9,
|
|
166
|
+
width: BAR_WIDTH,
|
|
167
|
+
progress: (repo.contributions / maxContributions) * 100,
|
|
168
|
+
delay: 450 + index * 150,
|
|
169
|
+
}),
|
|
170
|
+
el(
|
|
171
|
+
'text',
|
|
172
|
+
{
|
|
173
|
+
class: 'count',
|
|
174
|
+
'data-testid': 'repo-contributions',
|
|
175
|
+
x: contentWidth,
|
|
176
|
+
y: 0,
|
|
177
|
+
'text-anchor': 'end',
|
|
178
|
+
},
|
|
179
|
+
kFormatter(repo.contributions),
|
|
180
|
+
),
|
|
181
|
+
years.length > 0 && createYearStrip(repo.years, years, nameWidth),
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* The line under the rows: how much of the walk the card is showing, and over what years.
|
|
186
|
+
*
|
|
187
|
+
* Never dropped, because the card shows a slice of the repositories
|
|
188
|
+
* and a rank means nothing without the total it was taken from.
|
|
189
|
+
*
|
|
190
|
+
* @returns The footer text.
|
|
191
|
+
*/
|
|
192
|
+
const footerText = ({
|
|
193
|
+
i18n,
|
|
194
|
+
shown,
|
|
195
|
+
totalRepos,
|
|
196
|
+
years,
|
|
197
|
+
}: {
|
|
198
|
+
i18n: ContributedToI18n;
|
|
199
|
+
shown: number;
|
|
200
|
+
totalRepos: number;
|
|
201
|
+
/** Empty when the year marks are hidden, which is what puts the years on the card. */
|
|
202
|
+
years: Array<number>;
|
|
203
|
+
}): string => {
|
|
204
|
+
const parts = [
|
|
205
|
+
i18n.t(shown < totalRepos ? 'contributedtocard.footer-top' : 'contributedtocard.footer-all'),
|
|
206
|
+
];
|
|
207
|
+
|
|
208
|
+
const [firstYear] = years;
|
|
209
|
+
const lastYear = years.at(-1);
|
|
210
|
+
if (firstYear !== undefined && lastYear !== undefined) {
|
|
211
|
+
parts.push(formatYears(firstYear, lastYear));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return parts.join(' · ');
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Renders the repositories a user contributed to, ranked by contributions.
|
|
219
|
+
*
|
|
220
|
+
* @returns Contributed-to card SVG string.
|
|
221
|
+
*/
|
|
222
|
+
const renderContributedToCard = (
|
|
223
|
+
data: ContributedToData,
|
|
224
|
+
options: CardOptions<ContributedToCardOptions> = {},
|
|
225
|
+
): string => {
|
|
226
|
+
const {
|
|
227
|
+
locale,
|
|
228
|
+
hide_title = false,
|
|
229
|
+
hide_border = false,
|
|
230
|
+
card_width,
|
|
231
|
+
custom_title,
|
|
232
|
+
disable_animations = false,
|
|
233
|
+
hide_years = false,
|
|
234
|
+
border_radius,
|
|
235
|
+
theme = 'default',
|
|
236
|
+
} = options;
|
|
237
|
+
|
|
238
|
+
const { login, repos, totalRepos } = data;
|
|
239
|
+
const years = hide_years ? [] : data.years;
|
|
240
|
+
|
|
241
|
+
const width =
|
|
242
|
+
card_width && !Number.isNaN(card_width)
|
|
243
|
+
? clampValue(card_width, MIN_CARD_WIDTH, Number.MAX_SAFE_INTEGER)
|
|
244
|
+
: CARD_DEFAULT_WIDTH;
|
|
245
|
+
const contentWidth = width - 2 * CARD_PADDING;
|
|
246
|
+
const nameWidth = contentWidth - COUNT_WIDTH - BAR_WIDTH - 2 * COLUMN_GAP;
|
|
247
|
+
|
|
248
|
+
const rowHeight = years.length > 0 ? ROW_HEIGHT : ROW_HEIGHT_NO_YEARS;
|
|
249
|
+
// the count is a share of the busiest repository, so the first bar is always full
|
|
250
|
+
const maxContributions = Math.max(...repos.map((repo) => repo.contributions), 1);
|
|
251
|
+
|
|
252
|
+
const rows = repos.map((repo, index) =>
|
|
253
|
+
createRepoRow({
|
|
254
|
+
repo,
|
|
255
|
+
index,
|
|
256
|
+
maxContributions,
|
|
257
|
+
years,
|
|
258
|
+
nameWidth,
|
|
259
|
+
contentWidth,
|
|
260
|
+
rowHeight,
|
|
261
|
+
}),
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
const i18n = new I18n({
|
|
265
|
+
locale,
|
|
266
|
+
translations: contributedToCardLocales({ login, shown: repos.length, totalRepos }),
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const footer = footerText({ i18n, shown: repos.length, totalRepos, years });
|
|
270
|
+
const footerY = FIRST_ROW_Y + Math.max(repos.length, 1) * rowHeight + FOOTER_GAP;
|
|
271
|
+
const height = BODY_OFFSET_Y + footerY + BOTTOM_PADDING;
|
|
272
|
+
|
|
273
|
+
const { lightColors, darkColors } = getLightDarkColors({ ...options, theme });
|
|
274
|
+
|
|
275
|
+
const card = new Card({
|
|
276
|
+
customTitle: custom_title,
|
|
277
|
+
defaultTitle: defaultTitleFor(i18n, contentWidth - TITLE_ICON_COLUMN),
|
|
278
|
+
titlePrefixIcon: CARD_ICON.contributedTo,
|
|
279
|
+
width,
|
|
280
|
+
height,
|
|
281
|
+
border_radius,
|
|
282
|
+
colors: { light: lightColors, dark: darkColors },
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
if (disable_animations) {
|
|
286
|
+
card.disableAnimations();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
card.setHideBorder(hide_border);
|
|
290
|
+
card.setHideTitle(hide_title);
|
|
291
|
+
card.setCSS({
|
|
292
|
+
light: ({ textColor, titleColor, iconColor, progBarBgColor }) => [
|
|
293
|
+
atRule(
|
|
294
|
+
'@keyframes growWidthAnimation',
|
|
295
|
+
rule('from', { width: 0 }),
|
|
296
|
+
rule('to', { width: '100%' }),
|
|
297
|
+
),
|
|
298
|
+
rule('.repo-name', {
|
|
299
|
+
font: font('regular', 'meta'),
|
|
300
|
+
fill: textColor,
|
|
301
|
+
}),
|
|
302
|
+
rule('.count', {
|
|
303
|
+
font: font('semibold', 'meta'),
|
|
304
|
+
fill: textColor,
|
|
305
|
+
'font-variant-numeric': 'tabular-nums',
|
|
306
|
+
}),
|
|
307
|
+
rule('.footer', {
|
|
308
|
+
font: font('regular', 'micro'),
|
|
309
|
+
fill: textColor,
|
|
310
|
+
opacity: 0.7,
|
|
311
|
+
}),
|
|
312
|
+
firefoxFontSize(['.repo-name', '.count'], 'small'),
|
|
313
|
+
rule('.year-on', { fill: titleColor }),
|
|
314
|
+
// a year with no contribution reads off the text color, not `progBarBgColor`:
|
|
315
|
+
// on a dark theme that color is a light grey, and the empty marks came out looking filled
|
|
316
|
+
rule('.year-off', { fill: textColor, opacity: 0.25 }),
|
|
317
|
+
rule('.progress-background', { fill: progBarBgColor }),
|
|
318
|
+
rule('.lang-progress', {
|
|
319
|
+
fill: titleColor,
|
|
320
|
+
animation: 'growWidthAnimation 0.6s ease-in-out forwards',
|
|
321
|
+
}),
|
|
322
|
+
rule('.stagger', { opacity: 0, animation: 'fadeInAnimation 0.3s ease-in-out forwards' }),
|
|
323
|
+
rule('.icon', { fill: iconColor, display: 'block' }),
|
|
324
|
+
],
|
|
325
|
+
dark: ({ textColor, titleColor, iconColor, progBarBgColor }) => [
|
|
326
|
+
rule('.repo-name', { fill: textColor }),
|
|
327
|
+
rule('.count', { fill: textColor }),
|
|
328
|
+
rule('.footer', { fill: textColor }),
|
|
329
|
+
rule('.year-on', { fill: titleColor }),
|
|
330
|
+
// a year with no contribution reads off the text color, not `progBarBgColor`:
|
|
331
|
+
// on a dark theme that color is a light grey, and the empty marks came out looking filled
|
|
332
|
+
rule('.year-off', { fill: textColor, opacity: 0.25 }),
|
|
333
|
+
rule('.progress-background', { fill: progBarBgColor }),
|
|
334
|
+
rule('.lang-progress', { fill: titleColor }),
|
|
335
|
+
rule('.icon', { fill: iconColor }),
|
|
336
|
+
],
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// `role="img"` hides the inner text from assistive tech, so everything the card
|
|
340
|
+
// shows has to be repeated here — the year marks included, since they carry
|
|
341
|
+
// information no other element on the card does.
|
|
342
|
+
card.setAccessibilityLabel({
|
|
343
|
+
title: card.title,
|
|
344
|
+
desc: [
|
|
345
|
+
...repos.map((repo) => {
|
|
346
|
+
const inYears = hide_years
|
|
347
|
+
? ''
|
|
348
|
+
: `, ${i18n.t('contributedtocard.years')}: ${repo.years.join(', ')}`;
|
|
349
|
+
return `${repo.nameWithOwner}: ${repo.contributions} ${i18n.t(
|
|
350
|
+
'contributedtocard.contributions',
|
|
351
|
+
)}${inYears}`;
|
|
352
|
+
}),
|
|
353
|
+
footer,
|
|
354
|
+
].join('; '),
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
return card.render(
|
|
358
|
+
el(
|
|
359
|
+
'g',
|
|
360
|
+
{ 'data-testid': 'contributed-to-body', transform: `translate(${CARD_PADDING}, 0)` },
|
|
361
|
+
repos.length === 0
|
|
362
|
+
? el(
|
|
363
|
+
'text',
|
|
364
|
+
{ class: 'repo-name', 'data-testid': 'no-repos', x: 0, y: FIRST_ROW_Y },
|
|
365
|
+
i18n.t('contributedtocard.no-contributions'),
|
|
366
|
+
)
|
|
367
|
+
: rows,
|
|
368
|
+
el('text', { class: 'footer', 'data-testid': 'footer', x: 0, y: footerY }, footer),
|
|
369
|
+
),
|
|
370
|
+
);
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
export { renderContributedToCard, MIN_CARD_WIDTH };
|
package/src/cards/gist.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { CARD_ICON, CARD_WIDTH, FONT_SIZE, font } from '../common/brand.ts';
|
|
1
2
|
import { Card } from '../common/Card.ts';
|
|
2
3
|
import { getLightDarkColors } from '../common/color.ts';
|
|
3
4
|
import { kFormatter } from '../common/fmt.ts';
|
|
5
|
+
import { I18n } from '../common/I18n.ts';
|
|
4
6
|
import { icons } from '../common/icons.ts';
|
|
5
7
|
import { getLanguageColor } from '../common/languageColors.ts';
|
|
6
8
|
import { parseEmojis } from '../common/ops.ts';
|
|
@@ -17,19 +19,21 @@ import {
|
|
|
17
19
|
import type { GistData } from '../fetchers/types.ts';
|
|
18
20
|
import type { Child } from '../markup/index.ts';
|
|
19
21
|
import { el, rule } from '../markup/index.ts';
|
|
22
|
+
import { gistCardLocales } from '../translations.ts';
|
|
20
23
|
|
|
21
24
|
import type { CardOptions, CommonCardOptions } from './options.ts';
|
|
22
25
|
|
|
23
26
|
const ICON_SIZE = 16;
|
|
24
|
-
const CARD_DEFAULT_WIDTH =
|
|
27
|
+
const CARD_DEFAULT_WIDTH = CARD_WIDTH.standard;
|
|
25
28
|
const X_OFFSET = 25;
|
|
26
29
|
const HEADER_MAX_LENGTH = 35;
|
|
27
30
|
const DESCRIPTION_BOX_WIDTH = CARD_DEFAULT_WIDTH - 2 * X_OFFSET;
|
|
28
|
-
const DESCRIPTION_FONT_SIZE =
|
|
31
|
+
const DESCRIPTION_FONT_SIZE = FONT_SIZE.meta;
|
|
29
32
|
const DESCRIPTION_LINE_HEIGHT_PX = 16;
|
|
30
33
|
const DESCRIPTION_MAX_LINES = 10;
|
|
31
34
|
|
|
32
35
|
interface GistCardOptions extends CommonCardOptions {
|
|
36
|
+
locale: string;
|
|
33
37
|
show_owner: boolean;
|
|
34
38
|
browser_rendering: boolean;
|
|
35
39
|
}
|
|
@@ -42,6 +46,7 @@ interface GistCardOptions extends CommonCardOptions {
|
|
|
42
46
|
const renderGistCard = (gistData: GistData, options: CardOptions<GistCardOptions> = {}): string => {
|
|
43
47
|
const { name, nameWithOwner, description, language, starsCount, forksCount } = gistData;
|
|
44
48
|
const {
|
|
49
|
+
locale,
|
|
45
50
|
theme = 'default_repocard',
|
|
46
51
|
border_radius,
|
|
47
52
|
show_owner = false,
|
|
@@ -51,7 +56,9 @@ const renderGistCard = (gistData: GistData, options: CardOptions<GistCardOptions
|
|
|
51
56
|
|
|
52
57
|
const { lightColors, darkColors } = getLightDarkColors({ ...options, theme });
|
|
53
58
|
|
|
54
|
-
const
|
|
59
|
+
const i18n = new I18n({ locale, translations: gistCardLocales });
|
|
60
|
+
|
|
61
|
+
const desc = parseEmojis(description || i18n.t('gistcard.no-description'));
|
|
55
62
|
|
|
56
63
|
let descriptionLines: number;
|
|
57
64
|
let descriptionSvg: Child;
|
|
@@ -101,7 +108,7 @@ const renderGistCard = (gistData: GistData, options: CardOptions<GistCardOptions
|
|
|
101
108
|
const svgStars = iconWithLabel(icons.star, totalStars, 'starsCount', ICON_SIZE);
|
|
102
109
|
const svgForks = iconWithLabel(icons.fork, totalForks, 'forksCount', ICON_SIZE);
|
|
103
110
|
|
|
104
|
-
const languageName = language || '
|
|
111
|
+
const languageName = language || i18n.t('gistcard.unspecified-language');
|
|
105
112
|
const languageColor = getLanguageColor(languageName);
|
|
106
113
|
|
|
107
114
|
const svgLanguage = createLanguageNode(languageName, languageColor);
|
|
@@ -121,7 +128,7 @@ const renderGistCard = (gistData: GistData, options: CardOptions<GistCardOptions
|
|
|
121
128
|
const card = new Card({
|
|
122
129
|
defaultTitle:
|
|
123
130
|
header.length > HEADER_MAX_LENGTH ? `${header.slice(0, HEADER_MAX_LENGTH)}...` : header,
|
|
124
|
-
titlePrefixIcon:
|
|
131
|
+
titlePrefixIcon: CARD_ICON.gist,
|
|
125
132
|
width: CARD_DEFAULT_WIDTH,
|
|
126
133
|
height,
|
|
127
134
|
border_radius,
|
|
@@ -131,11 +138,11 @@ const renderGistCard = (gistData: GistData, options: CardOptions<GistCardOptions
|
|
|
131
138
|
card.setCSS({
|
|
132
139
|
light: ({ textColor, iconColor }) => [
|
|
133
140
|
rule('.description', {
|
|
134
|
-
font:
|
|
141
|
+
font: font('regular', 'meta'),
|
|
135
142
|
fill: textColor,
|
|
136
143
|
...(browser_rendering ? wrappedTextStyles(textColor) : {}),
|
|
137
144
|
}),
|
|
138
|
-
rule('.gray', { font:
|
|
145
|
+
rule('.gray', { font: font('regular', 'small'), fill: textColor }),
|
|
139
146
|
rule('.icon', { fill: iconColor }),
|
|
140
147
|
],
|
|
141
148
|
dark: ({ textColor, iconColor }) => [
|
|
@@ -154,7 +161,9 @@ const renderGistCard = (gistData: GistData, options: CardOptions<GistCardOptions
|
|
|
154
161
|
// shows has to be repeated here.
|
|
155
162
|
card.setAccessibilityLabel({
|
|
156
163
|
title: card.title,
|
|
157
|
-
desc: `${desc}.
|
|
164
|
+
desc: `${desc}. ${i18n.t('gistcard.language')}: ${languageName}, ${i18n.t(
|
|
165
|
+
'gistcard.stars',
|
|
166
|
+
)}: ${totalStars}, ${i18n.t('gistcard.forks')}: ${totalForks}`,
|
|
158
167
|
});
|
|
159
168
|
|
|
160
169
|
return card.render([
|