mirror of
https://github.com/System-End/github-readme-stats.git
synced 2026-04-20 00:35:23 +00:00
feat(layout): improve flexLayout & fixed layout overlaps (#1314)
* feat(layout): improve flexLayout & fixed layout overlaps * chore: fix vercel build
This commit is contained in:
parent
4dbb9e93b9
commit
97690e173d
6 changed files with 148 additions and 111 deletions
|
|
@ -5,6 +5,7 @@ const {
|
|||
getCardColors,
|
||||
flexLayout,
|
||||
wrapTextMultiline,
|
||||
measureText,
|
||||
} = require("../common/utils");
|
||||
const I18n = require("../common/I18n");
|
||||
const Card = require("../common/Card");
|
||||
|
|
@ -61,20 +62,15 @@ const renderRepoCard = (repo, options = {}) => {
|
|||
});
|
||||
|
||||
// returns theme based colors with proper overrides and defaults
|
||||
const {
|
||||
titleColor,
|
||||
textColor,
|
||||
iconColor,
|
||||
bgColor,
|
||||
borderColor,
|
||||
} = getCardColors({
|
||||
title_color,
|
||||
icon_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
border_color,
|
||||
theme,
|
||||
});
|
||||
const { titleColor, textColor, iconColor, bgColor, borderColor } =
|
||||
getCardColors({
|
||||
title_color,
|
||||
icon_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
border_color,
|
||||
theme,
|
||||
});
|
||||
|
||||
const totalStars = kFormatter(stargazers.totalCount);
|
||||
const totalForks = kFormatter(forkCount);
|
||||
|
|
@ -96,21 +92,24 @@ const renderRepoCard = (repo, options = {}) => {
|
|||
|
||||
const svgLanguage = primaryLanguage
|
||||
? `
|
||||
<g data-testid="primary-lang" transform="translate(30, 0)">
|
||||
<g data-testid="primary-lang">
|
||||
<circle data-testid="lang-color" cx="0" cy="-5" r="6" fill="${langColor}" />
|
||||
<text data-testid="lang-name" class="gray" x="15">${langName}</text>
|
||||
</g>
|
||||
`
|
||||
: "";
|
||||
|
||||
const iconSize = 16;
|
||||
const iconWithLabel = (icon, label, testid) => {
|
||||
return `
|
||||
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||
const iconSvg = `
|
||||
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="${iconSize}" height="${iconSize}">
|
||||
${icon}
|
||||
</svg>
|
||||
<text data-testid="${testid}" class="gray" x="25">${label}</text>
|
||||
`;
|
||||
const text = `<text data-testid="${testid}" class="gray">${label}</text>`;
|
||||
return flexLayout({ items: [iconSvg, text], gap: 20 }).join("");
|
||||
};
|
||||
|
||||
const svgStars =
|
||||
stargazers.totalCount > 0 &&
|
||||
iconWithLabel(icons.star, totalStars, "stargazers");
|
||||
|
|
@ -118,8 +117,13 @@ const renderRepoCard = (repo, options = {}) => {
|
|||
forkCount > 0 && iconWithLabel(icons.fork, totalForks, "forkcount");
|
||||
|
||||
const starAndForkCount = flexLayout({
|
||||
items: [svgStars, svgForks],
|
||||
gap: 65,
|
||||
items: [svgLanguage, svgStars, svgForks],
|
||||
sizes: [
|
||||
measureText(langName, 12),
|
||||
iconSize + measureText(`${totalStars}`, 12),
|
||||
iconSize + measureText(`${totalForks}`, 12),
|
||||
],
|
||||
gap: 25,
|
||||
}).join("");
|
||||
|
||||
const card = new Card({
|
||||
|
|
@ -163,15 +167,8 @@ const renderRepoCard = (repo, options = {}) => {
|
|||
.join("")}
|
||||
</text>
|
||||
|
||||
<g transform="translate(0, ${height - 75})">
|
||||
${svgLanguage}
|
||||
|
||||
<g
|
||||
data-testid="star-fork-group"
|
||||
transform="translate(${primaryLanguage ? 155 - shiftText : 25}, 0)"
|
||||
>
|
||||
${starAndForkCount}
|
||||
</g>
|
||||
<g transform="translate(30, ${height - 75})">
|
||||
${starAndForkCount}
|
||||
</g>
|
||||
`);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ const {
|
|||
getCardColors,
|
||||
flexLayout,
|
||||
lowercaseTrim,
|
||||
measureText,
|
||||
chunkArray,
|
||||
} = require("../common/utils");
|
||||
|
||||
const DEFAULT_CARD_WIDTH = 300;
|
||||
|
|
@ -33,12 +35,12 @@ const createProgressTextNode = ({ width, color, name, progress }) => {
|
|||
`;
|
||||
};
|
||||
|
||||
const createCompactLangNode = ({ lang, totalSize, x, y }) => {
|
||||
const createCompactLangNode = ({ lang, totalSize }) => {
|
||||
const percentage = ((lang.size / totalSize) * 100).toFixed(2);
|
||||
const color = lang.color || "#858585";
|
||||
|
||||
return `
|
||||
<g transform="translate(${x}, ${y})">
|
||||
<g>
|
||||
<circle cx="5" cy="6" r="5" fill="${color}" />
|
||||
<text data-testid="lang-name" x="15" y="10" class='lang-name'>
|
||||
${lang.name} ${percentage}%
|
||||
|
|
@ -47,25 +49,38 @@ const createCompactLangNode = ({ lang, totalSize, x, y }) => {
|
|||
`;
|
||||
};
|
||||
|
||||
const createLanguageTextNode = ({ langs, totalSize, x, y }) => {
|
||||
return langs.map((lang, index) => {
|
||||
if (index % 2 === 0) {
|
||||
return createCompactLangNode({
|
||||
const getLongestLang = (arr) =>
|
||||
arr.reduce(
|
||||
(savedLang, lang) =>
|
||||
lang.name.length > savedLang.name.length ? lang : savedLang,
|
||||
{ name: "" },
|
||||
);
|
||||
|
||||
const createLanguageTextNode = ({ langs, totalSize }) => {
|
||||
const longestLang = getLongestLang(langs);
|
||||
const chunked = chunkArray(langs, langs.length / 2);
|
||||
const layouts = chunked.map((array) => {
|
||||
const items = array.map((lang, index) =>
|
||||
createCompactLangNode({
|
||||
lang,
|
||||
x,
|
||||
y: 12.5 * index + y,
|
||||
totalSize,
|
||||
index,
|
||||
});
|
||||
}
|
||||
return createCompactLangNode({
|
||||
lang,
|
||||
x: 150,
|
||||
y: 12.5 + 12.5 * index,
|
||||
totalSize,
|
||||
index,
|
||||
});
|
||||
}),
|
||||
);
|
||||
return flexLayout({
|
||||
items,
|
||||
gap: 25,
|
||||
direction: "column",
|
||||
}).join("");
|
||||
});
|
||||
|
||||
const percent = ((longestLang.size / totalSize) * 100).toFixed(2);
|
||||
const minGap = 150;
|
||||
const maxGap = 20 + measureText(`${longestLang.name} ${percent}%`, 11);
|
||||
return flexLayout({
|
||||
items: layouts,
|
||||
gap: maxGap < minGap ? minGap : maxGap,
|
||||
}).join("");
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -132,12 +147,14 @@ const renderCompactLayout = (langs, width, totalLanguageSize) => {
|
|||
<rect x="0" y="0" width="${offsetWidth}" height="8" fill="white" rx="5" />
|
||||
</mask>
|
||||
${compactProgressBar}
|
||||
${createLanguageTextNode({
|
||||
x: 0,
|
||||
y: 25,
|
||||
langs,
|
||||
totalSize: totalLanguageSize,
|
||||
}).join("")}
|
||||
|
||||
<g transform="translate(0, 25)">
|
||||
${createLanguageTextNode({
|
||||
langs,
|
||||
totalSize: totalLanguageSize,
|
||||
width,
|
||||
})}
|
||||
</g>
|
||||
`;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -89,21 +89,26 @@ function request(data, headers) {
|
|||
|
||||
/**
|
||||
*
|
||||
* @param {String[]} items
|
||||
* @param {string[]} items
|
||||
* @param {Number} gap
|
||||
* @param {string} direction
|
||||
* @param {"column" | "row"} direction
|
||||
*
|
||||
* @returns {string[]}
|
||||
*
|
||||
* @description
|
||||
* Auto layout utility, allows us to layout things
|
||||
* vertically or horizontally with proper gaping
|
||||
*/
|
||||
function flexLayout({ items, gap, direction }) {
|
||||
function flexLayout({ items, gap, direction, sizes = [] }) {
|
||||
let lastSize = 0;
|
||||
// filter() for filtering out empty strings
|
||||
return items.filter(Boolean).map((item, i) => {
|
||||
let transform = `translate(${gap * i}, 0)`;
|
||||
const size = sizes[i] || 0;
|
||||
let transform = `translate(${lastSize}, 0)`;
|
||||
if (direction === "column") {
|
||||
transform = `translate(0, ${gap * i})`;
|
||||
transform = `translate(0, ${lastSize})`;
|
||||
}
|
||||
lastSize += size + gap;
|
||||
return `<g transform="${transform}">${item}</g>`;
|
||||
});
|
||||
}
|
||||
|
|
@ -232,6 +237,26 @@ function measureText(str, fontSize = 10) {
|
|||
}
|
||||
const lowercaseTrim = (name) => name.toLowerCase().trim();
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {Array<T>} arr
|
||||
* @param {number} perChunk
|
||||
* @returns {Array<T>}
|
||||
*/
|
||||
function chunkArray(arr, perChunk) {
|
||||
return arr.reduce((resultArray, item, index) => {
|
||||
const chunkIndex = Math.floor(index / perChunk);
|
||||
|
||||
if (!resultArray[chunkIndex]) {
|
||||
resultArray[chunkIndex] = []; // start a new chunk
|
||||
}
|
||||
|
||||
resultArray[chunkIndex].push(item);
|
||||
|
||||
return resultArray;
|
||||
}, []);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
renderError,
|
||||
kFormatter,
|
||||
|
|
@ -250,4 +275,5 @@ module.exports = {
|
|||
CONSTANTS,
|
||||
CustomError,
|
||||
lowercaseTrim,
|
||||
chunkArray,
|
||||
};
|
||||
|
|
|
|||
46
tests/flexLayout.test.js
Normal file
46
tests/flexLayout.test.js
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
const { flexLayout } = require("../src/common/utils");
|
||||
|
||||
describe("flexLayout", () => {
|
||||
it("should work with row & col layouts", () => {
|
||||
const layout = flexLayout({
|
||||
items: ["<text>1</text>", "<text>2</text>"],
|
||||
gap: 60,
|
||||
});
|
||||
|
||||
expect(layout).toStrictEqual([
|
||||
`<g transform="translate(0, 0)"><text>1</text></g>`,
|
||||
`<g transform="translate(60, 0)"><text>2</text></g>`,
|
||||
]);
|
||||
|
||||
const columns = flexLayout({
|
||||
items: ["<text>1</text>", "<text>2</text>"],
|
||||
gap: 60,
|
||||
direction: "column",
|
||||
});
|
||||
|
||||
expect(columns).toStrictEqual([
|
||||
`<g transform="translate(0, 0)"><text>1</text></g>`,
|
||||
`<g transform="translate(0, 60)"><text>2</text></g>`,
|
||||
]);
|
||||
});
|
||||
|
||||
it("should work with sizes", () => {
|
||||
const layout = flexLayout({
|
||||
items: [
|
||||
"<text>1</text>",
|
||||
"<text>2</text>",
|
||||
"<text>3</text>",
|
||||
"<text>4</text>",
|
||||
],
|
||||
gap: 20,
|
||||
sizes: [200, 100, 55, 25],
|
||||
});
|
||||
|
||||
expect(layout).toStrictEqual([
|
||||
`<g transform="translate(0, 0)"><text>1</text></g>`,
|
||||
`<g transform="translate(220, 0)"><text>2</text></g>`,
|
||||
`<g transform="translate(340, 0)"><text>3</text></g>`,
|
||||
`<g transform="translate(415, 0)"><text>4</text></g>`,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
@ -89,36 +89,6 @@ describe("Test renderRepoCard", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("should shift the text position depending on language length", () => {
|
||||
document.body.innerHTML = renderRepoCard({
|
||||
...data_repo.repository,
|
||||
primaryLanguage: {
|
||||
...data_repo.repository.primaryLanguage,
|
||||
name: "Jupyter Notebook",
|
||||
},
|
||||
});
|
||||
|
||||
expect(queryByTestId(document.body, "primary-lang")).toBeInTheDocument();
|
||||
expect(queryByTestId(document.body, "star-fork-group")).toHaveAttribute(
|
||||
"transform",
|
||||
"translate(155, 0)",
|
||||
);
|
||||
|
||||
// Small lang
|
||||
document.body.innerHTML = renderRepoCard({
|
||||
...data_repo.repository,
|
||||
primaryLanguage: {
|
||||
...data_repo.repository.primaryLanguage,
|
||||
name: "Ruby",
|
||||
},
|
||||
});
|
||||
|
||||
expect(queryByTestId(document.body, "star-fork-group")).toHaveAttribute(
|
||||
"transform",
|
||||
"translate(125, 0)",
|
||||
);
|
||||
});
|
||||
|
||||
it("should hide language if primaryLanguage is null & fallback to correct values", () => {
|
||||
document.body.innerHTML = renderRepoCard({
|
||||
...data_repo.repository,
|
||||
|
|
@ -332,11 +302,13 @@ describe("Test renderRepoCard", () => {
|
|||
);
|
||||
expect(queryByTestId(document.body, "badge")).toHaveTextContent("模板");
|
||||
});
|
||||
|
||||
|
||||
it("should render without rounding", () => {
|
||||
document.body.innerHTML = renderRepoCard(data_repo.repository, { border_radius: "0" });
|
||||
document.body.innerHTML = renderRepoCard(data_repo.repository, {
|
||||
border_radius: "0",
|
||||
});
|
||||
expect(document.querySelector("rect")).toHaveAttribute("rx", "0");
|
||||
document.body.innerHTML = renderRepoCard(data_repo.repository, { });
|
||||
document.body.innerHTML = renderRepoCard(data_repo.repository, {});
|
||||
expect(document.querySelector("rect")).toHaveAttribute("rx", "4.5");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -44,27 +44,6 @@ describe("Test utils.js", () => {
|
|||
).toHaveTextContent(/Secondary Message/gim);
|
||||
});
|
||||
|
||||
it("should test flexLayout", () => {
|
||||
const layout = flexLayout({
|
||||
items: ["<text>1</text>", "<text>2</text>"],
|
||||
gap: 60,
|
||||
}).join("");
|
||||
|
||||
expect(layout).toBe(
|
||||
`<g transform=\"translate(0, 0)\"><text>1</text></g><g transform=\"translate(60, 0)\"><text>2</text></g>`,
|
||||
);
|
||||
|
||||
const columns = flexLayout({
|
||||
items: ["<text>1</text>", "<text>2</text>"],
|
||||
gap: 60,
|
||||
direction: "column",
|
||||
}).join("");
|
||||
|
||||
expect(columns).toBe(
|
||||
`<g transform=\"translate(0, 0)\"><text>1</text></g><g transform=\"translate(0, 60)\"><text>2</text></g>`,
|
||||
);
|
||||
});
|
||||
|
||||
it("getCardColors: should return expected values", () => {
|
||||
let colors = getCardColors({
|
||||
title_color: "f00",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue