@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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CARD_ICON, CARD_WIDTH, FONT_WEIGHT, firefoxFontSize, font } from '../common/brand.ts';
|
|
1
2
|
import { Card } from '../common/Card.ts';
|
|
2
3
|
import { getLightDarkColors, isPrefixedHexColor } from '../common/color.ts';
|
|
3
4
|
import { formatBytes } from '../common/fmt.ts';
|
|
@@ -7,12 +8,12 @@ import { chunkArray, clampValue, lowercaseTrim } from '../common/ops.ts';
|
|
|
7
8
|
import { createProgressNode, flexLayout, measureText } from '../common/render.ts';
|
|
8
9
|
import type { Lang, TopLangData } from '../fetchers/types.ts';
|
|
9
10
|
import type { Child, MarkupElement } from '../markup/index.ts';
|
|
10
|
-
import { atRule,
|
|
11
|
+
import { atRule, el, rule } from '../markup/index.ts';
|
|
11
12
|
import { langCardLocales } from '../translations.ts';
|
|
12
13
|
|
|
13
14
|
import type { CardOptions, CommonCardOptions } from './options.ts';
|
|
14
15
|
|
|
15
|
-
const DEFAULT_CARD_WIDTH =
|
|
16
|
+
const DEFAULT_CARD_WIDTH = CARD_WIDTH.compact;
|
|
16
17
|
const MIN_CARD_WIDTH = 280;
|
|
17
18
|
const CARD_PADDING = 25;
|
|
18
19
|
const COMPACT_LAYOUT_BASE_HEIGHT = 90;
|
|
@@ -21,6 +22,8 @@ const MAXIMUM_LANGS_COUNT = 20;
|
|
|
21
22
|
const NORMAL_LAYOUT_DEFAULT_LANGS_COUNT = 5;
|
|
22
23
|
const COMPACT_LAYOUT_DEFAULT_LANGS_COUNT = 6;
|
|
23
24
|
const DONUT_LAYOUT_DEFAULT_LANGS_COUNT = 5;
|
|
25
|
+
/** Room for the chart that sits beside the language list. */
|
|
26
|
+
const DONUT_EXTRA_WIDTH = 50;
|
|
24
27
|
const PIE_LAYOUT_DEFAULT_LANGS_COUNT = 6;
|
|
25
28
|
const DONUT_VERTICAL_LAYOUT_DEFAULT_LANGS_COUNT = 6;
|
|
26
29
|
|
|
@@ -801,10 +804,7 @@ const getDefaultLanguagesCountByLayout = ({
|
|
|
801
804
|
*
|
|
802
805
|
* @returns Language card SVG object.
|
|
803
806
|
*/
|
|
804
|
-
const
|
|
805
|
-
topLangs: TopLangData,
|
|
806
|
-
options: CardOptions<TopLangOptions> = {},
|
|
807
|
-
): string => {
|
|
807
|
+
const renderCard = (topLangs: TopLangData, options: CardOptions<TopLangOptions> = {}): string => {
|
|
808
808
|
const {
|
|
809
809
|
hide_title = false,
|
|
810
810
|
hide_border = false,
|
|
@@ -865,7 +865,9 @@ const renderTopLanguages = (
|
|
|
865
865
|
);
|
|
866
866
|
} else if (layout === 'donut') {
|
|
867
867
|
height = calculateDonutLayoutHeight(langs.length);
|
|
868
|
-
|
|
868
|
+
// A donut defaults a step wider so the chart has its room; a width asked for
|
|
869
|
+
// explicitly keeps the extra it has always been given instead of being snapped.
|
|
870
|
+
width = width === DEFAULT_CARD_WIDTH ? CARD_WIDTH.standard : width + DONUT_EXTRA_WIDTH;
|
|
869
871
|
finalLayout = renderDonutLayout(langs, width, totalLanguageSize, stats_format, hide_values);
|
|
870
872
|
} else {
|
|
871
873
|
finalLayout = renderNormalLayout(langs, width, totalLanguageSize, stats_format, hide_values);
|
|
@@ -874,6 +876,7 @@ const renderTopLanguages = (
|
|
|
874
876
|
const card = new Card({
|
|
875
877
|
customTitle: custom_title,
|
|
876
878
|
defaultTitle: i18n.t('langcard.title'),
|
|
879
|
+
titlePrefixIcon: CARD_ICON.topLanguages,
|
|
877
880
|
width,
|
|
878
881
|
height,
|
|
879
882
|
border_radius,
|
|
@@ -901,16 +904,12 @@ const renderTopLanguages = (
|
|
|
901
904
|
rule('to', { width: '100%' }),
|
|
902
905
|
),
|
|
903
906
|
rule('.stat', {
|
|
904
|
-
font:
|
|
907
|
+
font: font('regular', 'body'),
|
|
905
908
|
fill: textColor,
|
|
906
909
|
}),
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
rule('.stat', { 'font-size': '12px' }),
|
|
911
|
-
),
|
|
912
|
-
rule('.bold', { 'font-weight': 700 }),
|
|
913
|
-
rule('.lang-name', { font: '400 11px "Segoe UI", Ubuntu, Sans-Serif', fill: textColor }),
|
|
910
|
+
firefoxFontSize(['.stat'], 'small'),
|
|
911
|
+
rule('.bold', { 'font-weight': FONT_WEIGHT.semibold }),
|
|
912
|
+
rule('.lang-name', { font: font('regular', 'micro'), fill: textColor }),
|
|
914
913
|
rule('.stagger', { opacity: 0, animation: 'fadeInAnimation 0.3s ease-in-out forwards' }),
|
|
915
914
|
rule('#rect-mask rect', { animation: 'slideInAnimation 1s ease-in-out forwards' }),
|
|
916
915
|
rule('.lang-progress', { animation: 'growWidthAnimation 0.6s ease-in-out forwards' }),
|
|
@@ -939,6 +938,18 @@ const renderTopLanguages = (
|
|
|
939
938
|
return card.render(el('svg', { 'data-testid': 'lang-items', x: CARD_PADDING }, finalLayout));
|
|
940
939
|
};
|
|
941
940
|
|
|
941
|
+
/**
|
|
942
|
+
* The card, and the values each of its options accepts, keyed by the option's own name.
|
|
943
|
+
* They ride on the renderer so a list cannot be found without what draws it;
|
|
944
|
+
* the api handler forwards them onto its own export, which is what a UI reads.
|
|
945
|
+
*/
|
|
946
|
+
const renderTopLanguages = Object.assign(renderCard, {
|
|
947
|
+
OPTIONS: {
|
|
948
|
+
layout: TOP_LANG_LAYOUTS,
|
|
949
|
+
stats_format: TOP_LANG_STATS_FORMATS,
|
|
950
|
+
},
|
|
951
|
+
});
|
|
952
|
+
|
|
942
953
|
export {
|
|
943
954
|
getLongestLang,
|
|
944
955
|
degreesToRadians,
|
|
@@ -953,9 +964,7 @@ export {
|
|
|
953
964
|
calculatePieLayoutHeight,
|
|
954
965
|
donutCenterTranslation,
|
|
955
966
|
trimTopLanguages,
|
|
956
|
-
renderTopLanguages,
|
|
957
967
|
MIN_CARD_WIDTH,
|
|
958
968
|
getDefaultLanguagesCountByLayout,
|
|
959
|
-
|
|
960
|
-
TOP_LANG_STATS_FORMATS,
|
|
969
|
+
renderTopLanguages,
|
|
961
970
|
};
|
package/src/cards/wakatime.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CARD_ICON, CARD_WIDTH, FONT_WEIGHT, firefoxFontSize, font } from '../common/brand.ts';
|
|
1
2
|
import { Card } from '../common/Card.ts';
|
|
2
3
|
import { getLightDarkColors, isPrefixedHexColor } from '../common/color.ts';
|
|
3
4
|
import { I18n } from '../common/I18n.ts';
|
|
@@ -6,12 +7,12 @@ import { clampValue, lowercaseTrim } from '../common/ops.ts';
|
|
|
6
7
|
import { createProgressNode, flexLayout } from '../common/render.ts';
|
|
7
8
|
import type { WakaTimeData, WakaTimeLang } from '../fetchers/types.ts';
|
|
8
9
|
import type { Child, CssChild, MarkupElement } from '../markup/index.ts';
|
|
9
|
-
import { atRule,
|
|
10
|
+
import { atRule, el, rule } from '../markup/index.ts';
|
|
10
11
|
import { wakatimeCardLocales } from '../translations.ts';
|
|
11
12
|
|
|
12
13
|
import type { CardOptions, CommonCardOptions } from './options.ts';
|
|
13
14
|
|
|
14
|
-
const DEFAULT_CARD_WIDTH =
|
|
15
|
+
const DEFAULT_CARD_WIDTH = CARD_WIDTH.wide;
|
|
15
16
|
const MIN_CARD_WIDTH = 250;
|
|
16
17
|
const COMPACT_LAYOUT_MIN_WIDTH = 400;
|
|
17
18
|
const DEFAULT_LINE_HEIGHT = 25;
|
|
@@ -216,17 +217,13 @@ const getStyles = ({ textColor }: { textColor: string }): Array<CssChild> => {
|
|
|
216
217
|
|
|
217
218
|
return [
|
|
218
219
|
rule('.stat', {
|
|
219
|
-
font:
|
|
220
|
+
font: font('regular', 'body'),
|
|
220
221
|
fill: textColor,
|
|
221
222
|
}),
|
|
222
|
-
|
|
223
|
-
'@supports(-moz-appearance: auto)',
|
|
224
|
-
cssComment('Selector detects Firefox'),
|
|
225
|
-
rule('.stat', { 'font-size': '12px' }),
|
|
226
|
-
),
|
|
223
|
+
firefoxFontSize(['.stat'], 'small'),
|
|
227
224
|
rule('.stagger', { opacity: 0, animation: 'fadeInAnimation 0.3s ease-in-out forwards' }),
|
|
228
|
-
rule('.not_bold', { 'font-weight':
|
|
229
|
-
rule('.bold', { 'font-weight':
|
|
225
|
+
rule('.not_bold', { 'font-weight': FONT_WEIGHT.regular }),
|
|
226
|
+
rule('.bold', { 'font-weight': FONT_WEIGHT.semibold }),
|
|
230
227
|
];
|
|
231
228
|
};
|
|
232
229
|
|
|
@@ -257,7 +254,7 @@ const normalizeCardWidth = ({
|
|
|
257
254
|
*
|
|
258
255
|
* @returns WakaTime card SVG.
|
|
259
256
|
*/
|
|
260
|
-
const
|
|
257
|
+
const renderCard = (
|
|
261
258
|
stats: Partial<WakaTimeData> = {},
|
|
262
259
|
options: CardOptions<WakaTimeOptions> = {},
|
|
263
260
|
): string => {
|
|
@@ -412,6 +409,7 @@ const renderWakatimeCard = (
|
|
|
412
409
|
const card = new Card({
|
|
413
410
|
customTitle: custom_title,
|
|
414
411
|
defaultTitle: titleText,
|
|
412
|
+
titlePrefixIcon: CARD_ICON.wakatime,
|
|
415
413
|
width: normalizedWidth,
|
|
416
414
|
height,
|
|
417
415
|
border_radius,
|
|
@@ -439,7 +437,7 @@ const renderWakatimeCard = (
|
|
|
439
437
|
rule('from', { width: 0 }),
|
|
440
438
|
rule('to', { width: '100%' }),
|
|
441
439
|
),
|
|
442
|
-
rule('.lang-name', { font:
|
|
440
|
+
rule('.lang-name', { font: font('regular', 'micro'), fill: textColor }),
|
|
443
441
|
rule('#rect-mask rect', { animation: 'slideInAnimation 1s ease-in-out forwards' }),
|
|
444
442
|
rule('.lang-progress', {
|
|
445
443
|
animation: 'growWidthAnimation 0.6s ease-in-out forwards',
|
|
@@ -470,4 +468,16 @@ const renderWakatimeCard = (
|
|
|
470
468
|
return card.render(el('svg', { x: 0, y: 0, width: '100%' }, finalLayout));
|
|
471
469
|
};
|
|
472
470
|
|
|
473
|
-
|
|
471
|
+
/**
|
|
472
|
+
* The card, and the values each of its options accepts, keyed by the option's own name.
|
|
473
|
+
* They ride on the renderer so a list cannot be found without what draws it;
|
|
474
|
+
* the api handler forwards them onto its own export, which is what a UI reads.
|
|
475
|
+
*/
|
|
476
|
+
const renderWakatimeCard = Object.assign(renderCard, {
|
|
477
|
+
OPTIONS: {
|
|
478
|
+
layout: WAKATIME_LAYOUTS,
|
|
479
|
+
display_format: DISPLAY_FORMATS,
|
|
480
|
+
},
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
export { renderWakatimeCard };
|
package/src/common/Card.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Child, CssChild, MarkupElement } from '../markup/index.ts';
|
|
2
2
|
import { atRule, cssComment, el, renderMarkup, rule, style } from '../markup/index.ts';
|
|
3
3
|
|
|
4
|
+
import { ACCENT, firefoxFontSize, font, TITLE_BAND, TITLE_FIREFOX_SIZE } from './brand.ts';
|
|
4
5
|
import { getCardColors, isPrefixedHexColor, isValidGradient } from './color.ts';
|
|
5
6
|
import type { CardColors } from './color.ts';
|
|
6
7
|
import { flexLayout } from './render.ts';
|
|
@@ -104,7 +105,7 @@ class Card {
|
|
|
104
105
|
this.darkCss = [];
|
|
105
106
|
|
|
106
107
|
this.paddingX = 25;
|
|
107
|
-
this.paddingY =
|
|
108
|
+
this.paddingY = 30;
|
|
108
109
|
this.titlePrefixIcon = titlePrefixIcon;
|
|
109
110
|
this.animations = true;
|
|
110
111
|
this.a11yTitle = '';
|
|
@@ -164,7 +165,7 @@ class Card {
|
|
|
164
165
|
el(
|
|
165
166
|
'svg',
|
|
166
167
|
{
|
|
167
|
-
class: 'icon',
|
|
168
|
+
class: 'title-icon',
|
|
168
169
|
x: 0,
|
|
169
170
|
y: -13,
|
|
170
171
|
viewBox: '0 0 16 16',
|
|
@@ -178,9 +179,41 @@ class Card {
|
|
|
178
179
|
],
|
|
179
180
|
gap: 25,
|
|
180
181
|
}),
|
|
182
|
+
el('rect', {
|
|
183
|
+
'data-testid': 'title-accent',
|
|
184
|
+
class: 'title-accent',
|
|
185
|
+
x: 0,
|
|
186
|
+
y: ACCENT.y,
|
|
187
|
+
width: ACCENT.width,
|
|
188
|
+
height: ACCENT.height,
|
|
189
|
+
rx: ACCENT.height / 2,
|
|
190
|
+
}),
|
|
181
191
|
);
|
|
182
192
|
}
|
|
183
193
|
|
|
194
|
+
/** Where the body starts, and so where the title band ends. */
|
|
195
|
+
private get bodyOffset(): number {
|
|
196
|
+
return this.hideTitle ? this.paddingX : this.paddingY + 25;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* The band behind the title. Its top corners follow the card's own and its bottom
|
|
201
|
+
* ones stay square, so it meets the body as an edge rather than a floating box.
|
|
202
|
+
*
|
|
203
|
+
* @returns The rendered title band.
|
|
204
|
+
*/
|
|
205
|
+
private renderTitleBand(): MarkupElement {
|
|
206
|
+
const height = this.bodyOffset - TITLE_BAND.gap;
|
|
207
|
+
const innerWidth = this.width - 1;
|
|
208
|
+
const r = Math.min(this.border_radius, height, innerWidth / 2);
|
|
209
|
+
|
|
210
|
+
return el('path', {
|
|
211
|
+
'data-testid': 'title-band',
|
|
212
|
+
class: 'title-band',
|
|
213
|
+
d: `M0.5 ${0.5 + r}a${r} ${r} 0 0 1 ${r} ${-r}h${innerWidth - 2 * r}a${r} ${r} 0 0 1 ${r} ${r}v${height - r - 0.5}h${-innerWidth}z`,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
184
217
|
/** @returns The card's gradient definitions, or nothing when no color is a gradient. */
|
|
185
218
|
renderGradient(): Child {
|
|
186
219
|
if (
|
|
@@ -225,6 +258,9 @@ class Card {
|
|
|
225
258
|
return atRule(
|
|
226
259
|
'@media (prefers-color-scheme: dark)',
|
|
227
260
|
rule('.header', { fill: this.colors.dark.titleColor }),
|
|
261
|
+
rule('.title-icon', { fill: this.colors.dark.iconColor }),
|
|
262
|
+
rule('.title-accent', { fill: this.colors.dark.iconColor }),
|
|
263
|
+
rule('.title-band', { fill: this.colors.dark.titleColor }),
|
|
228
264
|
rule('.card-bg', { fill: bgFill, stroke: this.colors.dark.borderColor }),
|
|
229
265
|
this.darkCss,
|
|
230
266
|
);
|
|
@@ -259,21 +295,29 @@ class Card {
|
|
|
259
295
|
fill: 'none',
|
|
260
296
|
xmlns: 'http://www.w3.org/2000/svg',
|
|
261
297
|
role: 'img',
|
|
262
|
-
'aria-labelledby': '
|
|
298
|
+
'aria-labelledby': 'title-id desc-id',
|
|
263
299
|
},
|
|
264
|
-
el('title', { id: '
|
|
265
|
-
el('desc', { id: '
|
|
300
|
+
el('title', { id: 'title-id' }, this.a11yTitle),
|
|
301
|
+
el('desc', { id: 'desc-id' }, this.a11yDesc),
|
|
266
302
|
style(
|
|
267
303
|
rule('.header', {
|
|
268
|
-
font:
|
|
304
|
+
font: font('semibold', 'title'),
|
|
269
305
|
fill: this.colors.light.titleColor,
|
|
270
306
|
animation: 'fadeInAnimation 0.8s ease-in-out forwards',
|
|
271
307
|
}),
|
|
272
|
-
|
|
273
|
-
'
|
|
274
|
-
cssComment('Selector detects Firefox'),
|
|
275
|
-
rule('.header', { 'font-size': '15.5px' }),
|
|
308
|
+
cssComment(
|
|
309
|
+
'The icon reads against the title rather than with it, so it keeps its own color.',
|
|
276
310
|
),
|
|
311
|
+
rule('.title-icon', {
|
|
312
|
+
fill: this.colors.light.iconColor,
|
|
313
|
+
animation: 'fadeInAnimation 0.8s ease-in-out forwards',
|
|
314
|
+
}),
|
|
315
|
+
rule('.title-accent', {
|
|
316
|
+
fill: this.colors.light.iconColor,
|
|
317
|
+
animation: 'fadeInAnimation 0.8s ease-in-out forwards',
|
|
318
|
+
}),
|
|
319
|
+
rule('.title-band', { fill: this.colors.light.titleColor, opacity: TITLE_BAND.opacity }),
|
|
320
|
+
firefoxFontSize(['.header'], TITLE_FIREFOX_SIZE),
|
|
277
321
|
this.css,
|
|
278
322
|
this.renderDarkMediaBlock(),
|
|
279
323
|
getAnimations(),
|
|
@@ -295,12 +339,13 @@ class Card {
|
|
|
295
339
|
: this.colors.light.bgColor,
|
|
296
340
|
'stroke-opacity': this.hideBorder ? 0 : 1,
|
|
297
341
|
}),
|
|
342
|
+
!this.hideTitle && this.renderTitleBand(),
|
|
298
343
|
!this.hideTitle && this.renderTitle(),
|
|
299
344
|
el(
|
|
300
345
|
'g',
|
|
301
346
|
{
|
|
302
347
|
'data-testid': 'main-card-body',
|
|
303
|
-
transform: `translate(0, ${this.
|
|
348
|
+
transform: `translate(0, ${this.bodyOffset})`,
|
|
304
349
|
},
|
|
305
350
|
body,
|
|
306
351
|
),
|
package/src/common/I18n.ts
CHANGED
|
@@ -23,7 +23,7 @@ class I18n<Translations extends TranslationsMap = TranslationsMap> {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
|
-
* Get translation.
|
|
26
|
+
* Get translation, falling back to the English string when the locale has no entry for the key.
|
|
27
27
|
*
|
|
28
28
|
* @returns Translated string.
|
|
29
29
|
*/
|
|
@@ -36,7 +36,9 @@ class I18n<Translations extends TranslationsMap = TranslationsMap> {
|
|
|
36
36
|
throw new Error(`${str} Translation string not found`);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
// A key is written in `en` first and translated afterwards, so a locale the key has
|
|
40
|
+
// not reached yet reads the English string rather than failing the whole card.
|
|
41
|
+
const localized = translation[this.locale] ?? translation[FALLBACK_LOCALE];
|
|
40
42
|
if (!localized) {
|
|
41
43
|
throw new Error(`'${str}' translation not found for locale '${this.locale}'`);
|
|
42
44
|
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The visual vocabulary every card draws with: one font stack, one type scale,
|
|
3
|
+
* one set of widths and the accent rule that marks a card as this tool's.
|
|
4
|
+
* A card composes from here; it never writes a `font` shorthand or a size of its own.
|
|
5
|
+
*/
|
|
6
|
+
import type { Child, CssChild } from '../markup/index.ts';
|
|
7
|
+
import { atRule, cssComment, rule } from '../markup/index.ts';
|
|
8
|
+
|
|
9
|
+
import { icons } from './icons.ts';
|
|
10
|
+
|
|
11
|
+
const FONT_STACK = `'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif`;
|
|
12
|
+
|
|
13
|
+
/** The type scale, in px. */
|
|
14
|
+
const FONT_SIZE = {
|
|
15
|
+
display: 22,
|
|
16
|
+
title: 18,
|
|
17
|
+
lead: 16,
|
|
18
|
+
body: 14,
|
|
19
|
+
meta: 13,
|
|
20
|
+
small: 12,
|
|
21
|
+
micro: 11,
|
|
22
|
+
} as const;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The one size off the scale. Firefox is given 15.5px rather than the scale's next step down,
|
|
26
|
+
* because that half pixel is inherited tuning for long titles and nothing here can test the
|
|
27
|
+
* wrapping it protects; it lives with the scale so `brand.ts` still owns every size a card draws.
|
|
28
|
+
*/
|
|
29
|
+
const TITLE_FIREFOX_SIZE = 15.5;
|
|
30
|
+
|
|
31
|
+
/** `regular` labels, `semibold` the values they label, `bold` the rank glyph alone. */
|
|
32
|
+
const FONT_WEIGHT = {
|
|
33
|
+
regular: 400,
|
|
34
|
+
semibold: 600,
|
|
35
|
+
bold: 700,
|
|
36
|
+
} as const;
|
|
37
|
+
|
|
38
|
+
type FontSize = keyof typeof FONT_SIZE;
|
|
39
|
+
type FontWeight = keyof typeof FONT_WEIGHT;
|
|
40
|
+
|
|
41
|
+
/** @returns The `font` shorthand for one step of the scale. */
|
|
42
|
+
const font = (weight: FontWeight, size: FontSize): string =>
|
|
43
|
+
`${String(FONT_WEIGHT[weight])} ${String(FONT_SIZE[size])}px ${FONT_STACK}`;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Firefox lays Segoe UI out wider than the other engines, so text that fits
|
|
47
|
+
* elsewhere overflows there; each affected selector steps down a size.
|
|
48
|
+
*
|
|
49
|
+
* @returns The `@supports` block that detects Firefox and resizes those selectors.
|
|
50
|
+
*/
|
|
51
|
+
const firefoxFontSize = (selectors: Array<string>, size: FontSize | number): CssChild => {
|
|
52
|
+
const px = typeof size === 'number' ? size : FONT_SIZE[size];
|
|
53
|
+
return atRule(
|
|
54
|
+
'@supports(-moz-appearance: auto)',
|
|
55
|
+
cssComment('Selector detects Firefox'),
|
|
56
|
+
selectors.map((selector) => rule(selector, { 'font-size': `${String(px)}px` })),
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The widths a card may default to. A card picks the step nearest what its layout
|
|
62
|
+
* needs, so cards stacked in a README share an edge instead of forming a ragged one.
|
|
63
|
+
*/
|
|
64
|
+
const CARD_WIDTH = {
|
|
65
|
+
compact: 300,
|
|
66
|
+
standard: 400,
|
|
67
|
+
wide: 500,
|
|
68
|
+
} as const;
|
|
69
|
+
|
|
70
|
+
/** The tinted band behind every card title: the mark a card is recognized by. */
|
|
71
|
+
const TITLE_BAND = {
|
|
72
|
+
/** Enough to read as a band on every theme, little enough to leave the title legible. */
|
|
73
|
+
opacity: 0.08,
|
|
74
|
+
/** Clear air the band leaves between its foot and the first row of the body. */
|
|
75
|
+
gap: 9,
|
|
76
|
+
} as const;
|
|
77
|
+
|
|
78
|
+
/** The rule under the title icon, sitting inside the band. */
|
|
79
|
+
const ACCENT = {
|
|
80
|
+
width: 32,
|
|
81
|
+
height: 2,
|
|
82
|
+
/** Below the title's descenders, clear of the band's foot. */
|
|
83
|
+
y: 8,
|
|
84
|
+
} as const;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The title icon each card wears. They are distinct on purpose: with the band
|
|
88
|
+
* and the shared type, the icon is what tells one card from another at a glance.
|
|
89
|
+
*/
|
|
90
|
+
const CARD_ICON = {
|
|
91
|
+
contributedTo: icons.prs_merged,
|
|
92
|
+
gist: icons.gist,
|
|
93
|
+
repo: icons.repo,
|
|
94
|
+
stats: icons.pulse,
|
|
95
|
+
topLanguages: icons.code,
|
|
96
|
+
wakatime: icons.clock,
|
|
97
|
+
} satisfies Record<string, Child>;
|
|
98
|
+
|
|
99
|
+
export {
|
|
100
|
+
ACCENT,
|
|
101
|
+
CARD_ICON,
|
|
102
|
+
CARD_WIDTH,
|
|
103
|
+
FONT_SIZE,
|
|
104
|
+
FONT_WEIGHT,
|
|
105
|
+
firefoxFontSize,
|
|
106
|
+
font,
|
|
107
|
+
TITLE_BAND,
|
|
108
|
+
TITLE_FIREFOX_SIZE,
|
|
109
|
+
};
|
package/src/common/constants.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { RepositoryAffiliation } from '../graphql/generated/common.ts';
|
|
2
2
|
|
|
3
|
+
/** The year GitHub launched, and so the earliest one a card can be asked to count from. */
|
|
4
|
+
const GITHUB_EPOCH_YEAR = 2008;
|
|
5
|
+
|
|
3
6
|
/** A GitHub login: alphanumerics and single inner hyphens, 39 characters at most. */
|
|
4
7
|
const GITHUB_USERNAME_PATTERN = /^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}$/i;
|
|
5
8
|
|
|
@@ -12,4 +15,4 @@ const OWNER_AFFILIATIONS: Array<RepositoryAffiliation> = [
|
|
|
12
15
|
'ORGANIZATION_MEMBER',
|
|
13
16
|
];
|
|
14
17
|
|
|
15
|
-
export { GITHUB_USERNAME_PATTERN, OWNER_AFFILIATIONS };
|
|
18
|
+
export { GITHUB_EPOCH_YEAR, GITHUB_USERNAME_PATTERN, OWNER_AFFILIATIONS };
|
package/src/common/date.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
/** A
|
|
1
|
+
/** @file A range reaches GitHub one calendar year at a time; `contributionsCollection` refuses a longer one. */
|
|
2
|
+
|
|
3
|
+
import { GITHUB_EPOCH_YEAR } from './constants.ts';
|
|
4
|
+
|
|
5
|
+
/** A range between two dates, as GitHub's range arguments take it. */
|
|
2
6
|
interface GitHubDateRange {
|
|
3
7
|
/** Start of the range, inclusive. */
|
|
4
8
|
from: Date;
|
|
@@ -6,6 +10,9 @@ interface GitHubDateRange {
|
|
|
6
10
|
to: Date;
|
|
7
11
|
}
|
|
8
12
|
|
|
13
|
+
/** A year, a year and month, or a full date. */
|
|
14
|
+
const RANGE_DATE_PATTERN = /^(?<year>\d{4})(?:-(?<month>\d{2})(?:-(?<day>\d{2}))?)?$/;
|
|
15
|
+
|
|
9
16
|
/**
|
|
10
17
|
* Format a date as a GitHub `DateTime` scalar.
|
|
11
18
|
* Seconds precision, no milliseconds.
|
|
@@ -15,7 +22,7 @@ interface GitHubDateRange {
|
|
|
15
22
|
const toGitHubDateTime = (date: Date): string => `${date.toISOString().slice(0, 19)}Z`;
|
|
16
23
|
|
|
17
24
|
/**
|
|
18
|
-
* The full UTC
|
|
25
|
+
* The full UTC range of a calendar year, both ends inclusive.
|
|
19
26
|
*
|
|
20
27
|
* The end matters to callers that would otherwise leave a range open:
|
|
21
28
|
* GitHub's `contributionsCollection` defaults `to` to one year after `from`,
|
|
@@ -28,5 +35,130 @@ const getGitHubYearRange = (year: number): GitHubDateRange => ({
|
|
|
28
35
|
to: new Date(Date.UTC(year, 11, 31, 23, 59, 59)),
|
|
29
36
|
});
|
|
30
37
|
|
|
31
|
-
|
|
38
|
+
/**
|
|
39
|
+
* The widest range a card can be asked for: GitHub's first year through the end of this one.
|
|
40
|
+
* The far end is the end of the year, not today, because a bare `to=2026` widens to its 31st of December.
|
|
41
|
+
*
|
|
42
|
+
* @returns The range, in UTC.
|
|
43
|
+
*/
|
|
44
|
+
const getWidestRange = (): GitHubDateRange => ({
|
|
45
|
+
from: getGitHubYearRange(GITHUB_EPOCH_YEAR).from,
|
|
46
|
+
to: getGitHubYearRange(new Date().getUTCFullYear()).to,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The range a card counts within, from the ends the query named.
|
|
51
|
+
*
|
|
52
|
+
* @returns The range, each open end filled from {@link getWidestRange}.
|
|
53
|
+
*/
|
|
54
|
+
const toRange = (from: Date | undefined, to: Date | undefined): GitHubDateRange => {
|
|
55
|
+
const widest = getWidestRange();
|
|
56
|
+
return { from: from ?? widest.from, to: to ?? widest.to };
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Read one end of a range, written as `2024`, `2024-03` or `2024-03-15`.
|
|
61
|
+
* A partial date covers the whole of what it names, so `2024` starts on the 1st of January
|
|
62
|
+
* and ends on the 31st of December.
|
|
63
|
+
*
|
|
64
|
+
* @returns The instant, or `undefined` when the text does not name a date that exists.
|
|
65
|
+
*/
|
|
66
|
+
const parseRangeDate = (value: string, end: 'from' | 'to'): Date | undefined => {
|
|
67
|
+
const match = RANGE_DATE_PATTERN.exec(value);
|
|
68
|
+
if (!match) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const { year: yearText, month: monthText, day: dayText } = match.groups ?? {};
|
|
73
|
+
const year = Number(yearText);
|
|
74
|
+
const month = monthText === undefined ? 1 : Number(monthText);
|
|
75
|
+
const day = dayText === undefined ? 1 : Number(dayText);
|
|
76
|
+
|
|
77
|
+
const start = new Date(Date.UTC(year, month - 1, day));
|
|
78
|
+
// Date.UTC rolls an impossible date forward rather than refusing it
|
|
79
|
+
if (start.getUTCMonth() + 1 !== month || start.getUTCDate() !== day) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
if (end === 'from') {
|
|
83
|
+
return start;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const nextUnit =
|
|
87
|
+
dayText !== undefined
|
|
88
|
+
? Date.UTC(year, month - 1, day + 1)
|
|
89
|
+
: monthText !== undefined
|
|
90
|
+
? Date.UTC(year, month, 1)
|
|
91
|
+
: Date.UTC(year + 1, 0, 1);
|
|
92
|
+
return new Date(nextUnit - 1000);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Split a range into one per calendar year, each clipped to it.
|
|
97
|
+
*
|
|
98
|
+
* @returns Them, oldest first; empty when the range is inverted.
|
|
99
|
+
*/
|
|
100
|
+
const toYearRanges = ({ from, to }: GitHubDateRange): Array<GitHubDateRange> => {
|
|
101
|
+
const ranges: Array<GitHubDateRange> = [];
|
|
102
|
+
for (let year = from.getUTCFullYear(); year <= to.getUTCFullYear(); year += 1) {
|
|
103
|
+
const yearRange = getGitHubYearRange(year);
|
|
104
|
+
const start = from > yearRange.from ? from : yearRange.from;
|
|
105
|
+
const end = to < yearRange.to ? to : yearRange.to;
|
|
106
|
+
if (start <= end) {
|
|
107
|
+
ranges.push({ from: start, to: end });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return ranges;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Whole years as a card writes them.
|
|
115
|
+
*
|
|
116
|
+
* @returns e.g. `2024` or `2020–2024`.
|
|
117
|
+
*/
|
|
118
|
+
const formatYears = (first: number, last: number): string =>
|
|
119
|
+
first === last ? `${first}` : `${first}–${last}`;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* A range as a card writes it, at the coarsest granularity that stays true.
|
|
123
|
+
*
|
|
124
|
+
* @returns e.g. `2024`, `2020–2024` or `2024-03-15 – 2024-06-30`.
|
|
125
|
+
*/
|
|
126
|
+
const formatRange = ({ from, to }: GitHubDateRange): string => {
|
|
127
|
+
const firstYear = from.getUTCFullYear();
|
|
128
|
+
const lastYear = to.getUTCFullYear();
|
|
129
|
+
const wholeYears =
|
|
130
|
+
from.getTime() === getGitHubYearRange(firstYear).from.getTime() &&
|
|
131
|
+
to.getTime() === getGitHubYearRange(lastYear).to.getTime();
|
|
132
|
+
|
|
133
|
+
if (!wholeYears) {
|
|
134
|
+
return `${from.toISOString().slice(0, 10)} – ${to.toISOString().slice(0, 10)}`;
|
|
135
|
+
}
|
|
136
|
+
return formatYears(firstYear, lastYear);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The ranges a walk asks GitHub for: one per calendar year the account contributed in.
|
|
141
|
+
* Ordered here, which is what makes a walk's ties chronological.
|
|
142
|
+
*
|
|
143
|
+
* @returns Them, oldest first.
|
|
144
|
+
*/
|
|
145
|
+
const toContributionRanges = (
|
|
146
|
+
contributionYears: ReadonlyArray<number>,
|
|
147
|
+
range: GitHubDateRange,
|
|
148
|
+
): Array<GitHubDateRange> => {
|
|
149
|
+
const contributed = new Set(contributionYears);
|
|
150
|
+
return toYearRanges(range).filter((year) => contributed.has(year.from.getUTCFullYear()));
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
export {
|
|
154
|
+
formatRange,
|
|
155
|
+
formatYears,
|
|
156
|
+
getWidestRange,
|
|
157
|
+
getGitHubYearRange,
|
|
158
|
+
parseRangeDate,
|
|
159
|
+
toContributionRanges,
|
|
160
|
+
toGitHubDateTime,
|
|
161
|
+
toRange,
|
|
162
|
+
toYearRanges,
|
|
163
|
+
};
|
|
32
164
|
export type { GitHubDateRange };
|
package/src/common/icons.ts
CHANGED
|
@@ -51,11 +51,7 @@ const icons = {
|
|
|
51
51
|
'fill-rule': 'evenodd',
|
|
52
52
|
d: 'M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm9 3a1 1 0 11-2 0 1 1 0 012 0zm-.25-6.25a.75.75 0 00-1.5 0v3.5a.75.75 0 001.5 0v-3.5z',
|
|
53
53
|
}),
|
|
54
|
-
|
|
55
|
-
'fill-rule': 'evenodd',
|
|
56
|
-
d: 'M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z',
|
|
57
|
-
}),
|
|
58
|
-
contribs: el('path', {
|
|
54
|
+
repo: el('path', {
|
|
59
55
|
'fill-rule': 'evenodd',
|
|
60
56
|
d: 'M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z',
|
|
61
57
|
}),
|
|
@@ -85,6 +81,15 @@ const icons = {
|
|
|
85
81
|
'fill-rule': 'evenodd',
|
|
86
82
|
d: 'M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v12.5A1.75 1.75 0 0 1 14.25 16H1.75A1.75 1.75 0 0 1 0 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25V1.75a.25.25 0 0 0-.25-.25Zm7.47 3.97a.75.75 0 0 1 1.06 0l2 2a.75.75 0 0 1 0 1.06l-2 2a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734L10.69 8 9.22 6.53a.75.75 0 0 1 0-1.06ZM6.78 6.53 5.31 8l1.47 1.47a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215l-2-2a.75.75 0 0 1 0-1.06l2-2a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042Z',
|
|
87
83
|
}),
|
|
84
|
+
pulse: el('path', {
|
|
85
|
+
d: 'M6 2c.306 0 .582.187.696.471L10 10.731l1.304-3.26A.751.751 0 0 1 12 7h3.25a.75.75 0 0 1 0 1.5h-2.742l-1.812 4.529a.751.751 0 0 1-1.392 0L6 4.769l-1.304 3.26A.751.751 0 0 1 4 8.5H.75a.75.75 0 0 1 0-1.5h2.742l1.812-4.529A.751.751 0 0 1 6 2Z',
|
|
86
|
+
}),
|
|
87
|
+
code: el('path', {
|
|
88
|
+
d: 'M4.72 3.22a.75.75 0 0 1 1.06 1.06L2.06 8l3.72 3.72a.75.75 0 1 1-1.06 1.06L.47 8.53a.75.75 0 0 1 0-1.06Zm6.56 0a.75.75 0 1 0-1.06 1.06L13.94 8l-3.72 3.72a.75.75 0 1 0 1.06 1.06l4.25-4.25a.75.75 0 0 0 0-1.06Z',
|
|
89
|
+
}),
|
|
90
|
+
clock: el('path', {
|
|
91
|
+
d: 'M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Zm7-3.25v2.992l2.028.812a.75.75 0 0 1-.557 1.392l-2.5-1A.75.75 0 0 1 7 8.25v-3.5a.75.75 0 0 1 1.5 0Z',
|
|
92
|
+
}),
|
|
88
93
|
};
|
|
89
94
|
|
|
90
95
|
/** Attributes every rank glyph shares, so the three variants differ only where they mean to. */
|