@stats-forge/github-stats-forge-core 0.3.0 → 0.4.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.js +1 -1
- package/build/api/gist.js +1 -1
- 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/organization.d.ts +29 -0
- package/build/api/organization.d.ts.map +1 -0
- package/build/api/organization.js +53 -0
- package/build/api/params.js +1 -1
- package/build/api/pin.js +1 -1
- package/build/api/stats.js +1 -1
- package/build/api/top-langs.js +1 -1
- package/build/api/wakatime.js +1 -1
- package/build/cards/{contributed-to.d.ts → contributed-to/index.d.ts} +3 -3
- package/build/cards/contributed-to/index.d.ts.map +1 -0
- package/build/cards/{contributed-to.js → contributed-to/index.js} +31 -30
- package/build/cards/contributed-to/locales.d.ts +38 -0
- package/build/cards/contributed-to/locales.d.ts.map +1 -0
- package/build/cards/contributed-to/locales.js +39 -0
- package/build/cards/{gist.d.ts → gist/index.d.ts} +3 -3
- package/build/cards/gist/index.d.ts.map +1 -0
- package/build/cards/{gist.js → gist/index.js} +20 -15
- package/build/cards/gist/locales.d.ts +14 -0
- package/build/cards/gist/locales.d.ts.map +1 -0
- package/build/cards/gist/locales.js +15 -0
- package/build/cards/organization/index.d.ts +36 -0
- package/build/cards/organization/index.d.ts.map +1 -0
- package/build/cards/organization/index.js +225 -0
- package/build/cards/organization/locales.d.ts +47 -0
- package/build/cards/organization/locales.d.ts.map +1 -0
- package/build/cards/organization/locales.js +47 -0
- package/build/cards/{repo.d.ts → repo/index.d.ts} +3 -3
- package/build/cards/repo/index.d.ts.map +1 -0
- package/build/cards/{repo.js → repo/index.js} +20 -23
- package/build/cards/repo/locales.d.ts +129 -0
- package/build/cards/repo/locales.d.ts.map +1 -0
- package/build/cards/repo/locales.js +129 -0
- package/build/cards/{stats.d.ts → stats/index.d.ts} +3 -3
- package/build/cards/stats/index.d.ts.map +1 -0
- package/build/cards/{stats.js → stats/index.js} +38 -42
- package/build/cards/stats/locales.d.ts +621 -0
- package/build/cards/stats/locales.d.ts.map +1 -0
- package/build/cards/stats/locales.js +623 -0
- package/build/cards/{top-languages.d.ts → top-languages/index.d.ts} +3 -3
- package/build/cards/top-languages/index.d.ts.map +1 -0
- package/build/cards/{top-languages.js → top-languages/index.js} +13 -16
- package/build/cards/top-languages/locales.d.ts +103 -0
- package/build/cards/top-languages/locales.d.ts.map +1 -0
- package/build/cards/top-languages/locales.js +103 -0
- package/build/cards/{wakatime.d.ts → wakatime/index.d.ts} +3 -3
- package/build/cards/wakatime/index.d.ts.map +1 -0
- package/build/cards/{wakatime.js → wakatime/index.js} +22 -23
- package/build/cards/wakatime/locales.d.ts +250 -0
- package/build/cards/wakatime/locales.d.ts.map +1 -0
- package/build/cards/wakatime/locales.js +250 -0
- package/build/common/brand.d.ts +1 -0
- package/build/common/brand.d.ts.map +1 -1
- package/build/common/brand.js +1 -0
- package/build/common/error.d.ts +3 -1
- package/build/common/error.d.ts.map +1 -1
- package/build/common/error.js +3 -1
- package/build/common/icons.d.ts +5 -0
- package/build/common/icons.d.ts.map +1 -1
- package/build/common/icons.js +19 -4
- package/build/common/locales.d.ts +59 -0
- package/build/common/locales.d.ts.map +1 -0
- package/build/common/locales.js +59 -0
- package/build/common/localize.d.ts +60 -0
- package/build/common/localize.d.ts.map +1 -0
- package/build/common/localize.js +163 -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/organization.d.ts +15 -0
- package/build/fetchers/organization.d.ts.map +1 -0
- package/build/fetchers/organization.js +115 -0
- package/build/fetchers/types.d.ts +38 -0
- package/build/fetchers/types.d.ts.map +1 -1
- package/build/graphql/generated/organization.d.ts +88 -0
- package/build/graphql/generated/organization.d.ts.map +1 -0
- package/build/graphql/generated/organization.js +59 -0
- package/package.json +1 -1
- package/src/api/contributed-to.ts +1 -1
- package/src/api/gist.ts +1 -1
- package/src/api/index.ts +1 -0
- package/src/api/organization.ts +90 -0
- package/src/api/params.ts +1 -1
- package/src/api/pin.ts +1 -1
- package/src/api/stats.ts +1 -1
- package/src/api/top-langs.ts +1 -1
- package/src/api/wakatime.ts +1 -1
- package/src/cards/{contributed-to.ts → contributed-to/index.ts} +42 -39
- package/src/cards/contributed-to/locales.ts +41 -0
- package/src/cards/{gist.ts → gist/index.ts} +23 -20
- package/src/cards/gist/locales.ts +17 -0
- package/src/cards/organization/index.ts +358 -0
- package/src/cards/organization/locales.ts +49 -0
- package/src/cards/{repo.ts → repo/index.ts} +23 -26
- package/src/cards/repo/locales.ts +131 -0
- package/src/cards/{stats.ts → stats/index.ts} +48 -56
- package/src/cards/stats/locales.ts +625 -0
- package/src/cards/{top-languages.ts → top-languages/index.ts} +17 -20
- package/src/cards/top-languages/locales.ts +105 -0
- package/src/cards/{wakatime.ts → wakatime/index.ts} +26 -27
- package/src/cards/wakatime/locales.ts +252 -0
- package/src/common/brand.ts +1 -0
- package/src/common/error.ts +4 -0
- package/src/common/icons.ts +20 -4
- package/src/common/locales.ts +61 -0
- package/src/common/localize.ts +251 -0
- package/src/fetchers/index.ts +2 -0
- package/src/fetchers/organization.ts +153 -0
- package/src/fetchers/types.ts +36 -0
- package/src/graphql/generated/organization.ts +109 -0
- package/src/graphql/queries/organization.graphql +57 -0
- package/build/cards/contributed-to.d.ts.map +0 -1
- package/build/cards/gist.d.ts.map +0 -1
- package/build/cards/repo.d.ts.map +0 -1
- package/build/cards/stats.d.ts.map +0 -1
- package/build/cards/top-languages.d.ts.map +0 -1
- package/build/cards/wakatime.d.ts.map +0 -1
- package/build/common/I18n.d.ts +0 -21
- package/build/common/I18n.d.ts.map +0 -1
- package/build/common/I18n.js +0 -34
- package/build/translations.d.ts +0 -1212
- package/build/translations.d.ts.map +0 -1
- package/build/translations.js +0 -1213
- package/src/common/I18n.ts +0 -50
- package/src/translations.ts +0 -1239
package/src/api/params.ts
CHANGED
|
@@ -5,8 +5,8 @@ import { COLOR_PARAM_KEYS, THEME_PARAM_KEYS, isValidColorInput } from '../common
|
|
|
5
5
|
import { GITHUB_USERNAME_PATTERN } from '../common/constants.ts';
|
|
6
6
|
import { getWidestRange, parseRangeDate } from '../common/date.ts';
|
|
7
7
|
import { CardError } from '../common/error.ts';
|
|
8
|
+
import { isLocaleAvailable } from '../common/localize.ts';
|
|
8
9
|
import { parseArray, parseBoolean } from '../common/ops.ts';
|
|
9
|
-
import { isLocaleAvailable } from '../translations.ts';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* @file The api layer is the trust boundary:
|
package/src/api/pin.ts
CHANGED
package/src/api/stats.ts
CHANGED
package/src/api/top-langs.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as z from 'zod/mini';
|
|
2
2
|
|
|
3
|
-
import { renderTopLanguages } from '../cards/top-languages.ts';
|
|
3
|
+
import { renderTopLanguages } from '../cards/top-languages/index.ts';
|
|
4
4
|
import { OWNER_AFFILIATIONS } from '../common/constants.ts';
|
|
5
5
|
import { fetchTopLanguages } from '../fetchers/top-languages.ts';
|
|
6
6
|
|
package/src/api/wakatime.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as z from 'zod/mini';
|
|
2
2
|
|
|
3
|
-
import { renderWakatimeCard } from '../cards/wakatime.ts';
|
|
3
|
+
import { renderWakatimeCard } from '../cards/wakatime/index.ts';
|
|
4
4
|
import { fetchWakatimeStats } from '../fetchers/wakatime.ts';
|
|
5
5
|
|
|
6
6
|
import { cardHandler } from './handler.ts';
|
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
import { CARD_ICON, CARD_WIDTH, FONT_SIZE, firefoxFontSize, font } from '
|
|
2
|
-
import { Card } from '
|
|
3
|
-
import { getLightDarkColors } from '
|
|
4
|
-
import { formatYears } from '
|
|
5
|
-
import { kFormatter } from '
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
10
|
-
import type {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
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 type { Localized } from '../../common/localize.ts';
|
|
7
|
+
import { localize } from '../../common/localize.ts';
|
|
8
|
+
import { clampValue } from '../../common/ops.ts';
|
|
9
|
+
import { createProgressNode, measureText } from '../../common/render.ts';
|
|
10
|
+
import type { ContributedRepo, ContributedToData } from '../../fetchers/types.ts';
|
|
11
|
+
import type { Child } from '../../markup/index.ts';
|
|
12
|
+
import { atRule, el, rule } from '../../markup/index.ts';
|
|
13
|
+
import type { CardOptions, CommonCardOptions } from '../options.ts';
|
|
13
14
|
|
|
14
|
-
import
|
|
15
|
+
import { contributedToCardLocales } from './locales.ts';
|
|
15
16
|
|
|
16
17
|
const CARD_DEFAULT_WIDTH = CARD_WIDTH.wide;
|
|
17
18
|
const MIN_CARD_WIDTH = 340;
|
|
@@ -53,9 +54,6 @@ interface ContributedToCardOptions extends CommonCardOptions {
|
|
|
53
54
|
hide_years: boolean;
|
|
54
55
|
}
|
|
55
56
|
|
|
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
57
|
/**
|
|
60
58
|
* The title, naming the account unless that makes it too wide for the card.
|
|
61
59
|
*
|
|
@@ -64,11 +62,13 @@ type ContributedToI18n = I18n<ReturnType<typeof contributedToCardLocales>>;
|
|
|
64
62
|
*
|
|
65
63
|
* @returns The default title, which `custom_title` still overrides.
|
|
66
64
|
*/
|
|
67
|
-
const defaultTitleFor = (
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
65
|
+
const defaultTitleFor = (
|
|
66
|
+
t: Localized<typeof contributedToCardLocales>,
|
|
67
|
+
login: string,
|
|
68
|
+
contentWidth: number,
|
|
69
|
+
): string => {
|
|
70
|
+
const named = t.title({ login });
|
|
71
|
+
return measureText(named, TITLE_FONT_SIZE) <= contentWidth ? named : t.titleUnnamed();
|
|
72
72
|
};
|
|
73
73
|
|
|
74
74
|
/**
|
|
@@ -190,19 +190,21 @@ const createRepoRow = ({
|
|
|
190
190
|
* @returns The footer text.
|
|
191
191
|
*/
|
|
192
192
|
const footerText = ({
|
|
193
|
-
|
|
193
|
+
t,
|
|
194
194
|
shown,
|
|
195
195
|
totalRepos,
|
|
196
196
|
years,
|
|
197
197
|
}: {
|
|
198
|
-
|
|
198
|
+
t: Localized<typeof contributedToCardLocales>;
|
|
199
199
|
shown: number;
|
|
200
200
|
totalRepos: number;
|
|
201
201
|
/** Empty when the year marks are hidden, which is what puts the years on the card. */
|
|
202
202
|
years: Array<number>;
|
|
203
203
|
}): string => {
|
|
204
204
|
const parts = [
|
|
205
|
-
|
|
205
|
+
shown < totalRepos
|
|
206
|
+
? t.footerTop({ shown, count: totalRepos })
|
|
207
|
+
: t.footerAll({ count: totalRepos }),
|
|
206
208
|
];
|
|
207
209
|
|
|
208
210
|
const [firstYear] = years;
|
|
@@ -261,12 +263,9 @@ const renderContributedToCard = (
|
|
|
261
263
|
}),
|
|
262
264
|
);
|
|
263
265
|
|
|
264
|
-
const
|
|
265
|
-
locale,
|
|
266
|
-
translations: contributedToCardLocales({ login, shown: repos.length, totalRepos }),
|
|
267
|
-
});
|
|
266
|
+
const t = localize(contributedToCardLocales, locale);
|
|
268
267
|
|
|
269
|
-
const footer = footerText({
|
|
268
|
+
const footer = footerText({ t, shown: repos.length, totalRepos, years });
|
|
270
269
|
const footerY = FIRST_ROW_Y + Math.max(repos.length, 1) * rowHeight + FOOTER_GAP;
|
|
271
270
|
const height = BODY_OFFSET_Y + footerY + BOTTOM_PADDING;
|
|
272
271
|
|
|
@@ -274,7 +273,7 @@ const renderContributedToCard = (
|
|
|
274
273
|
|
|
275
274
|
const card = new Card({
|
|
276
275
|
customTitle: custom_title,
|
|
277
|
-
defaultTitle: defaultTitleFor(
|
|
276
|
+
defaultTitle: defaultTitleFor(t, login, contentWidth - TITLE_ICON_COLUMN),
|
|
278
277
|
titlePrefixIcon: CARD_ICON.contributedTo,
|
|
279
278
|
width,
|
|
280
279
|
height,
|
|
@@ -342,14 +341,18 @@ const renderContributedToCard = (
|
|
|
342
341
|
card.setAccessibilityLabel({
|
|
343
342
|
title: card.title,
|
|
344
343
|
desc: [
|
|
345
|
-
...repos.map((repo) =>
|
|
346
|
-
|
|
347
|
-
?
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
344
|
+
...repos.map((repo) =>
|
|
345
|
+
hide_years
|
|
346
|
+
? t.accessibilityRepo({
|
|
347
|
+
repo: repo.nameWithOwner,
|
|
348
|
+
count: repo.contributions,
|
|
349
|
+
})
|
|
350
|
+
: t.accessibilityRepoYears({
|
|
351
|
+
repo: repo.nameWithOwner,
|
|
352
|
+
count: repo.contributions,
|
|
353
|
+
years: repo.years.join(', '),
|
|
354
|
+
}),
|
|
355
|
+
),
|
|
353
356
|
footer,
|
|
354
357
|
].join('; '),
|
|
355
358
|
});
|
|
@@ -362,7 +365,7 @@ const renderContributedToCard = (
|
|
|
362
365
|
? el(
|
|
363
366
|
'text',
|
|
364
367
|
{ class: 'repo-name', 'data-testid': 'no-repos', x: 0, y: FIRST_ROW_Y },
|
|
365
|
-
|
|
368
|
+
t.noContributions(),
|
|
366
369
|
)
|
|
367
370
|
: rows,
|
|
368
371
|
el('text', { class: 'footer', 'data-testid': 'footer', x: 0, y: footerY }, footer),
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { defineLocales } from '../../common/localize.ts';
|
|
2
|
+
|
|
3
|
+
/** The contributed-to card's labels in the locales they have been translated into. */
|
|
4
|
+
const contributedToCardLocales = defineLocales({
|
|
5
|
+
title: {
|
|
6
|
+
en: 'Repositories {login} contributed to',
|
|
7
|
+
},
|
|
8
|
+
// Drawn instead of the title above when naming the account overruns the card.
|
|
9
|
+
titleUnnamed: {
|
|
10
|
+
en: 'Repositories contributed to',
|
|
11
|
+
},
|
|
12
|
+
noContributions: {
|
|
13
|
+
en: 'No contributions found',
|
|
14
|
+
},
|
|
15
|
+
footerAll: {
|
|
16
|
+
en: { one: '{count} repository', other: '{count} repositories' },
|
|
17
|
+
},
|
|
18
|
+
footerTop: {
|
|
19
|
+
en: {
|
|
20
|
+
one: 'top {shown} of {count} repository',
|
|
21
|
+
other: 'top {shown} of {count} repositories',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
// One row of what an assistive reader gets in place of the card, whose inner text
|
|
25
|
+
// `role="img"` hides. The year marks carry information no other element does, so the
|
|
26
|
+
// row naming them is a wording of its own rather than a fragment appended to the other.
|
|
27
|
+
accessibilityRepo: {
|
|
28
|
+
en: {
|
|
29
|
+
one: '{repo}: {count} contribution',
|
|
30
|
+
other: '{repo}: {count} contributions',
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
accessibilityRepoYears: {
|
|
34
|
+
en: {
|
|
35
|
+
one: '{repo}: {count} contribution, years: {years}',
|
|
36
|
+
other: '{repo}: {count} contributions, years: {years}',
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export { contributedToCardLocales };
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { CARD_ICON, CARD_WIDTH, FONT_SIZE, font } from '
|
|
2
|
-
import { Card } from '
|
|
3
|
-
import { getLightDarkColors } from '
|
|
4
|
-
import { kFormatter } from '
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { parseEmojis } from '
|
|
1
|
+
import { CARD_ICON, CARD_WIDTH, FONT_SIZE, font } from '../../common/brand.ts';
|
|
2
|
+
import { Card } from '../../common/Card.ts';
|
|
3
|
+
import { getLightDarkColors } from '../../common/color.ts';
|
|
4
|
+
import { kFormatter } from '../../common/fmt.ts';
|
|
5
|
+
import { icons } from '../../common/icons.ts';
|
|
6
|
+
import { getLanguageColor } from '../../common/languageColors.ts';
|
|
7
|
+
import { localize } from '../../common/localize.ts';
|
|
8
|
+
import { parseEmojis } from '../../common/ops.ts';
|
|
9
9
|
import {
|
|
10
10
|
countWrappedLines,
|
|
11
11
|
wrapTextMultiline,
|
|
@@ -15,13 +15,13 @@ import {
|
|
|
15
15
|
measureText,
|
|
16
16
|
wrappedTextNode,
|
|
17
17
|
wrappedTextStyles,
|
|
18
|
-
} from '
|
|
19
|
-
import type { GistData } from '
|
|
20
|
-
import type { Child } from '
|
|
21
|
-
import { el, rule } from '
|
|
22
|
-
import {
|
|
18
|
+
} from '../../common/render.ts';
|
|
19
|
+
import type { GistData } from '../../fetchers/types.ts';
|
|
20
|
+
import type { Child } from '../../markup/index.ts';
|
|
21
|
+
import { el, rule } from '../../markup/index.ts';
|
|
22
|
+
import type { CardOptions, CommonCardOptions } from '../options.ts';
|
|
23
23
|
|
|
24
|
-
import
|
|
24
|
+
import { gistCardLocales } from './locales.ts';
|
|
25
25
|
|
|
26
26
|
const ICON_SIZE = 16;
|
|
27
27
|
const CARD_DEFAULT_WIDTH = CARD_WIDTH.standard;
|
|
@@ -56,9 +56,9 @@ const renderGistCard = (gistData: GistData, options: CardOptions<GistCardOptions
|
|
|
56
56
|
|
|
57
57
|
const { lightColors, darkColors } = getLightDarkColors({ ...options, theme });
|
|
58
58
|
|
|
59
|
-
const
|
|
59
|
+
const t = localize(gistCardLocales, locale);
|
|
60
60
|
|
|
61
|
-
const desc = parseEmojis(description ||
|
|
61
|
+
const desc = parseEmojis(description || t.noDescription());
|
|
62
62
|
|
|
63
63
|
let descriptionLines: number;
|
|
64
64
|
let descriptionSvg: Child;
|
|
@@ -108,7 +108,7 @@ const renderGistCard = (gistData: GistData, options: CardOptions<GistCardOptions
|
|
|
108
108
|
const svgStars = iconWithLabel(icons.star, totalStars, 'starsCount', ICON_SIZE);
|
|
109
109
|
const svgForks = iconWithLabel(icons.fork, totalForks, 'forksCount', ICON_SIZE);
|
|
110
110
|
|
|
111
|
-
const languageName = language ||
|
|
111
|
+
const languageName = language || t.unspecifiedLanguage();
|
|
112
112
|
const languageColor = getLanguageColor(languageName);
|
|
113
113
|
|
|
114
114
|
const svgLanguage = createLanguageNode(languageName, languageColor);
|
|
@@ -161,9 +161,12 @@ const renderGistCard = (gistData: GistData, options: CardOptions<GistCardOptions
|
|
|
161
161
|
// shows has to be repeated here.
|
|
162
162
|
card.setAccessibilityLabel({
|
|
163
163
|
title: card.title,
|
|
164
|
-
desc:
|
|
165
|
-
|
|
166
|
-
|
|
164
|
+
desc: t.accessibilityDesc({
|
|
165
|
+
desc,
|
|
166
|
+
language: languageName,
|
|
167
|
+
stars: totalStars,
|
|
168
|
+
forks: totalForks,
|
|
169
|
+
}),
|
|
167
170
|
});
|
|
168
171
|
|
|
169
172
|
return card.render([
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineLocales } from '../../common/localize.ts';
|
|
2
|
+
|
|
3
|
+
/** The gist card's labels in the locales they have been translated into. */
|
|
4
|
+
const gistCardLocales = defineLocales({
|
|
5
|
+
noDescription: {
|
|
6
|
+
en: 'No description provided',
|
|
7
|
+
},
|
|
8
|
+
unspecifiedLanguage: {
|
|
9
|
+
en: 'Unspecified',
|
|
10
|
+
},
|
|
11
|
+
// What an assistive reader gets in place of the card, whose inner text `role="img"` hides.
|
|
12
|
+
accessibilityDesc: {
|
|
13
|
+
en: '{desc}. Language: {language}, Stars: {stars}, Forks: {forks}',
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export { gistCardLocales };
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import { CARD_ICON, CARD_WIDTH, FONT_SIZE, FONT_WEIGHT, font } from '../../common/brand.ts';
|
|
2
|
+
import { Card } from '../../common/Card.ts';
|
|
3
|
+
import { getLightDarkColors } from '../../common/color.ts';
|
|
4
|
+
import type { CardColors } from '../../common/color.ts';
|
|
5
|
+
import { CardError } from '../../common/error.ts';
|
|
6
|
+
import { kFormatter } from '../../common/fmt.ts';
|
|
7
|
+
import { icons } from '../../common/icons.ts';
|
|
8
|
+
import { localize } from '../../common/localize.ts';
|
|
9
|
+
import { parseEmojis } from '../../common/ops.ts';
|
|
10
|
+
import {
|
|
11
|
+
NUMBER_FORMATS,
|
|
12
|
+
createTextNode,
|
|
13
|
+
flexLayout,
|
|
14
|
+
measureText,
|
|
15
|
+
wrapTextMultiline,
|
|
16
|
+
} from '../../common/render.ts';
|
|
17
|
+
import type { OrganizationData } from '../../fetchers/types.ts';
|
|
18
|
+
import type { Child, CssChild } from '../../markup/index.ts';
|
|
19
|
+
import { el, rule } from '../../markup/index.ts';
|
|
20
|
+
import type { CardOptions, CommonCardOptions } from '../options.ts';
|
|
21
|
+
|
|
22
|
+
import { orgCardLocales } from './locales.ts';
|
|
23
|
+
|
|
24
|
+
const CARD_DEFAULT_WIDTH = CARD_WIDTH.standard;
|
|
25
|
+
/** Padding the card keeps at its edges; matches `Card`'s own `paddingX`. */
|
|
26
|
+
const CARD_PADDING_X = 25;
|
|
27
|
+
/** How far a stat row is translated into the card; see `createTextNode`. */
|
|
28
|
+
const STAT_ROW_X = 25;
|
|
29
|
+
/** `createTextNode`'s own label offset, which it applies only when icons are shown. */
|
|
30
|
+
const LABEL_X_OFFSET = 25;
|
|
31
|
+
/** Smallest gap kept between the longest label and its value. */
|
|
32
|
+
const LABEL_VALUE_GAP = 16;
|
|
33
|
+
const DESCRIPTION_FONT_SIZE = FONT_SIZE.meta;
|
|
34
|
+
const DESCRIPTION_LINE_HEIGHT_PX = 16;
|
|
35
|
+
const DESCRIPTION_MAX_LINES = 2;
|
|
36
|
+
/** Air between the description and the first stat row. */
|
|
37
|
+
const DESCRIPTION_GAP = 8;
|
|
38
|
+
|
|
39
|
+
/** Stats the card draws only when `show` names them. */
|
|
40
|
+
const SHOW_STATS = ['releases', 'commits', 'members', 'top_language', 'created'] as const;
|
|
41
|
+
type ShowStat = (typeof SHOW_STATS)[number];
|
|
42
|
+
|
|
43
|
+
/** Stats the card always draws, and so the ones `hide` has anything to remove. */
|
|
44
|
+
const HIDE_STATS = ['repos', 'stars', 'forks', 'watchers', 'open_issues', 'open_prs'] as const;
|
|
45
|
+
type HideStat = (typeof HIDE_STATS)[number];
|
|
46
|
+
|
|
47
|
+
type StatId = HideStat | ShowStat;
|
|
48
|
+
|
|
49
|
+
interface OrgCardOptions extends CommonCardOptions {
|
|
50
|
+
locale: string;
|
|
51
|
+
hide: Array<string>;
|
|
52
|
+
show: Array<string>;
|
|
53
|
+
show_icons: boolean;
|
|
54
|
+
hide_title: boolean;
|
|
55
|
+
hide_description: boolean;
|
|
56
|
+
card_width: number;
|
|
57
|
+
line_height: number | string;
|
|
58
|
+
custom_title: string;
|
|
59
|
+
disable_animations: boolean;
|
|
60
|
+
number_format: string;
|
|
61
|
+
text_bold: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Meta data for a stat, used to build its text node and accessibility label. */
|
|
65
|
+
interface StatItem {
|
|
66
|
+
icon: Child;
|
|
67
|
+
label: string;
|
|
68
|
+
value: number | string;
|
|
69
|
+
id: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @returns Card CSS styles.
|
|
74
|
+
*/
|
|
75
|
+
const getStyles = ({
|
|
76
|
+
textColor,
|
|
77
|
+
iconColor,
|
|
78
|
+
show_icons,
|
|
79
|
+
}: {
|
|
80
|
+
textColor: string;
|
|
81
|
+
iconColor: string;
|
|
82
|
+
show_icons: boolean;
|
|
83
|
+
}): Array<CssChild> => [
|
|
84
|
+
rule('.stat', {
|
|
85
|
+
font: font('regular', 'body'),
|
|
86
|
+
fill: textColor,
|
|
87
|
+
'font-variant-numeric': 'tabular-nums',
|
|
88
|
+
}),
|
|
89
|
+
rule('.description', { font: font('regular', 'meta'), fill: textColor, opacity: 0.9 }),
|
|
90
|
+
rule('.stagger', { opacity: 0, animation: 'fadeInAnimation 0.3s ease-in-out forwards' }),
|
|
91
|
+
rule('.not_bold', { 'font-weight': FONT_WEIGHT.regular, opacity: 0.75 }),
|
|
92
|
+
rule('.bold', { 'font-weight': FONT_WEIGHT.semibold }),
|
|
93
|
+
rule('.icon', { fill: iconColor, opacity: 0.75, display: show_icons ? 'block' : 'none' }),
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Renders the organization card.
|
|
98
|
+
*
|
|
99
|
+
* @returns The organization card SVG object.
|
|
100
|
+
*/
|
|
101
|
+
const renderCard = (
|
|
102
|
+
organization: OrganizationData,
|
|
103
|
+
options: CardOptions<OrgCardOptions> = {},
|
|
104
|
+
): string => {
|
|
105
|
+
const {
|
|
106
|
+
name,
|
|
107
|
+
description,
|
|
108
|
+
createdAt,
|
|
109
|
+
publicRepos,
|
|
110
|
+
totalStars,
|
|
111
|
+
totalForks,
|
|
112
|
+
totalWatchers,
|
|
113
|
+
openIssues,
|
|
114
|
+
openPRs,
|
|
115
|
+
totalReleases,
|
|
116
|
+
totalCommits,
|
|
117
|
+
publicMembers,
|
|
118
|
+
topLanguage,
|
|
119
|
+
truncated,
|
|
120
|
+
} = organization;
|
|
121
|
+
const {
|
|
122
|
+
hide = [],
|
|
123
|
+
show = [],
|
|
124
|
+
show_icons = true,
|
|
125
|
+
hide_title = false,
|
|
126
|
+
hide_border = false,
|
|
127
|
+
hide_description = false,
|
|
128
|
+
card_width,
|
|
129
|
+
line_height = 25,
|
|
130
|
+
text_bold = true,
|
|
131
|
+
custom_title,
|
|
132
|
+
border_radius,
|
|
133
|
+
number_format = 'short',
|
|
134
|
+
locale,
|
|
135
|
+
disable_animations = false,
|
|
136
|
+
} = options;
|
|
137
|
+
|
|
138
|
+
const lheight = Number.parseInt(String(line_height), 10);
|
|
139
|
+
|
|
140
|
+
const { lightColors, darkColors } = getLightDarkColors(options);
|
|
141
|
+
|
|
142
|
+
// Typed against the exported list, so a stat drawn here cannot be missing from it.
|
|
143
|
+
const shows = (stat: ShowStat): boolean => show.includes(stat);
|
|
144
|
+
|
|
145
|
+
const apostrophe = /s$/i.test(name.trim()) ? '' : 's';
|
|
146
|
+
const t = localize(orgCardLocales, locale);
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* A total the walk may have stopped short of reads as a lower bound.
|
|
150
|
+
*
|
|
151
|
+
* @returns The value, as the row draws it.
|
|
152
|
+
*/
|
|
153
|
+
const total = (value: number): number | string =>
|
|
154
|
+
truncated
|
|
155
|
+
? `${number_format.toLowerCase() === 'long' ? String(value) : kFormatter(value)}+`
|
|
156
|
+
: value;
|
|
157
|
+
|
|
158
|
+
const STATS: Partial<Record<StatId, StatItem>> = {
|
|
159
|
+
repos: {
|
|
160
|
+
icon: icons.repo,
|
|
161
|
+
label: t.repos(),
|
|
162
|
+
value: publicRepos,
|
|
163
|
+
id: 'repos',
|
|
164
|
+
},
|
|
165
|
+
stars: {
|
|
166
|
+
icon: icons.star,
|
|
167
|
+
label: t.stars(),
|
|
168
|
+
value: total(totalStars),
|
|
169
|
+
id: 'stars',
|
|
170
|
+
},
|
|
171
|
+
forks: {
|
|
172
|
+
icon: icons.fork,
|
|
173
|
+
label: t.forks(),
|
|
174
|
+
value: total(totalForks),
|
|
175
|
+
id: 'forks',
|
|
176
|
+
},
|
|
177
|
+
watchers: {
|
|
178
|
+
icon: icons.watchers,
|
|
179
|
+
label: t.watchers(),
|
|
180
|
+
value: total(totalWatchers),
|
|
181
|
+
id: 'watchers',
|
|
182
|
+
},
|
|
183
|
+
open_issues: {
|
|
184
|
+
icon: icons.issues,
|
|
185
|
+
label: t.openIssues(),
|
|
186
|
+
value: total(openIssues),
|
|
187
|
+
id: 'open_issues',
|
|
188
|
+
},
|
|
189
|
+
open_prs: {
|
|
190
|
+
icon: icons.prs,
|
|
191
|
+
label: t.openPrs(),
|
|
192
|
+
value: total(openPRs),
|
|
193
|
+
id: 'open_prs',
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
if (shows('releases')) {
|
|
198
|
+
STATS['releases'] = {
|
|
199
|
+
icon: icons.tag,
|
|
200
|
+
label: t.releases(),
|
|
201
|
+
value: total(totalReleases),
|
|
202
|
+
id: 'releases',
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
if (shows('commits')) {
|
|
206
|
+
STATS['commits'] = {
|
|
207
|
+
icon: icons.commits,
|
|
208
|
+
label: t.commits(),
|
|
209
|
+
value: total(totalCommits),
|
|
210
|
+
id: 'commits',
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (shows('members')) {
|
|
215
|
+
STATS['members'] = {
|
|
216
|
+
icon: icons.people,
|
|
217
|
+
label: t.members(),
|
|
218
|
+
value: publicMembers,
|
|
219
|
+
id: 'members',
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
if (shows('top_language')) {
|
|
223
|
+
STATS['top_language'] = {
|
|
224
|
+
icon: icons.code,
|
|
225
|
+
label: t.topLanguage(),
|
|
226
|
+
value: topLanguage?.name ?? t.unspecifiedLanguage(),
|
|
227
|
+
id: 'top_language',
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
if (shows('created')) {
|
|
231
|
+
const year = new Date(createdAt).getUTCFullYear();
|
|
232
|
+
STATS['created'] = {
|
|
233
|
+
icon: icons.calendar,
|
|
234
|
+
label: t.created(),
|
|
235
|
+
value: Number.isNaN(year) ? '—' : String(year),
|
|
236
|
+
id: 'created',
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const visibleStats = Object.entries(STATS).filter(([key]) => !hide.includes(key));
|
|
241
|
+
|
|
242
|
+
if (visibleStats.length === 0) {
|
|
243
|
+
throw new CardError('Could not render organization card.', {
|
|
244
|
+
code: 'invalid_param',
|
|
245
|
+
secondaryMessage: 'At least one stat is required.',
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const width = card_width && !Number.isNaN(card_width) ? card_width : CARD_DEFAULT_WIDTH;
|
|
250
|
+
|
|
251
|
+
const desc = hide_description ? '' : parseEmojis(description || t.noDescription());
|
|
252
|
+
const descriptionLines = desc
|
|
253
|
+
? wrapTextMultiline(
|
|
254
|
+
desc,
|
|
255
|
+
width - 2 * CARD_PADDING_X,
|
|
256
|
+
DESCRIPTION_FONT_SIZE,
|
|
257
|
+
DESCRIPTION_MAX_LINES,
|
|
258
|
+
)
|
|
259
|
+
: [];
|
|
260
|
+
const statsTop =
|
|
261
|
+
descriptionLines.length > 0
|
|
262
|
+
? descriptionLines.length * DESCRIPTION_LINE_HEIGHT_PX + DESCRIPTION_GAP
|
|
263
|
+
: 0;
|
|
264
|
+
|
|
265
|
+
const height = 45 + (visibleStats.length + 1) * lheight + statsTop;
|
|
266
|
+
|
|
267
|
+
// A value ends at the card's inner edge — or right after the longest label,
|
|
268
|
+
// on a card too narrow for that.
|
|
269
|
+
const widestLabel = Math.max(
|
|
270
|
+
...visibleStats.map(([, stat]) => measureText(`${stat.label}:`, FONT_SIZE.body)),
|
|
271
|
+
);
|
|
272
|
+
const valueAnchorX = Math.round(
|
|
273
|
+
Math.max(
|
|
274
|
+
width - CARD_PADDING_X - STAT_ROW_X,
|
|
275
|
+
(show_icons ? LABEL_X_OFFSET : 0) + widestLabel + LABEL_VALUE_GAP,
|
|
276
|
+
),
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
const statItems = visibleStats.map(([, stat], index) =>
|
|
280
|
+
createTextNode({
|
|
281
|
+
icon: stat.icon,
|
|
282
|
+
label: stat.label,
|
|
283
|
+
value: stat.value,
|
|
284
|
+
id: stat.id,
|
|
285
|
+
index,
|
|
286
|
+
showIcons: show_icons,
|
|
287
|
+
shiftValuePos: 0,
|
|
288
|
+
valueAnchorX,
|
|
289
|
+
bold: text_bold,
|
|
290
|
+
labelBold: false,
|
|
291
|
+
numberFormat: number_format,
|
|
292
|
+
}),
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
const card = new Card({
|
|
296
|
+
customTitle: custom_title,
|
|
297
|
+
defaultTitle: t.title({ name, apostrophe }),
|
|
298
|
+
titlePrefixIcon: CARD_ICON.organization,
|
|
299
|
+
width,
|
|
300
|
+
height,
|
|
301
|
+
border_radius,
|
|
302
|
+
colors: { light: lightColors, dark: darkColors },
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
card.setHideBorder(hide_border);
|
|
306
|
+
card.setHideTitle(hide_title);
|
|
307
|
+
const cardStyles = ({ textColor, iconColor }: CardColors): Array<CssChild> =>
|
|
308
|
+
getStyles({ textColor, iconColor, show_icons });
|
|
309
|
+
card.setCSS({ light: cardStyles, dark: cardStyles });
|
|
310
|
+
|
|
311
|
+
if (disable_animations) {
|
|
312
|
+
card.disableAnimations();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// `role="img"` hides the inner text from assistive tech, so everything the card
|
|
316
|
+
// shows has to be repeated here.
|
|
317
|
+
card.setAccessibilityLabel({
|
|
318
|
+
title: card.title,
|
|
319
|
+
desc: [
|
|
320
|
+
desc && `${desc}.`,
|
|
321
|
+
visibleStats.map(([, stat]) => `${stat.label}: ${String(stat.value)}`).join(', '),
|
|
322
|
+
]
|
|
323
|
+
.filter(Boolean)
|
|
324
|
+
.join(' '),
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
const descriptionSvg =
|
|
328
|
+
descriptionLines.length > 0 &&
|
|
329
|
+
el(
|
|
330
|
+
'text',
|
|
331
|
+
{ class: 'description', 'data-testid': 'description', x: CARD_PADDING_X, y: -5 },
|
|
332
|
+
descriptionLines.map((line) => el('tspan', { dy: '1.2em', x: CARD_PADDING_X }, line)),
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
return card.render([
|
|
336
|
+
descriptionSvg,
|
|
337
|
+
el(
|
|
338
|
+
'g',
|
|
339
|
+
{ transform: `translate(0, ${statsTop})` },
|
|
340
|
+
flexLayout({ items: statItems, gap: lheight, direction: 'column' }),
|
|
341
|
+
),
|
|
342
|
+
]);
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* The card, and the values each of its options accepts, keyed by the option's own name.
|
|
347
|
+
* They ride on the renderer so a list cannot be found without what draws it;
|
|
348
|
+
* the api handler forwards them onto its own export, which is what a UI reads.
|
|
349
|
+
*/
|
|
350
|
+
const renderOrganizationCard = Object.assign(renderCard, {
|
|
351
|
+
OPTIONS: {
|
|
352
|
+
show: SHOW_STATS,
|
|
353
|
+
hide: HIDE_STATS,
|
|
354
|
+
number_format: NUMBER_FORMATS,
|
|
355
|
+
},
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
export { renderOrganizationCard };
|