feat(layout): improve flexLayout & fixed layout overlaps (#1314)
* feat(layout): improve flexLayout & fixed layout overlaps * chore: fix vercel build
This commit is contained in:
1 parent
4dbb9e93b9
commit
97690e173d
6 files changed
+148
-111
No files matched your search
+26
-29
@@ -5,6 +5,7 @@ const {
|
|||||||
getCardColors,
|
getCardColors,
|
||||||
flexLayout,
|
flexLayout,
|
||||||
wrapTextMultiline,
|
wrapTextMultiline,
|
||||||
|
measureText,
|
||||||
} = require("../common/utils");
|
} = require("../common/utils");
|
||||||
const I18n = require("../common/I18n");
|
const I18n = require("../common/I18n");
|
||||||
const Card = require("../common/Card");
|
const Card = require("../common/Card");
|
||||||
@@ -61,20 +62,15 @@ const renderRepoCard = (repo, options = {}) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// returns theme based colors with proper overrides and defaults
|
// returns theme based colors with proper overrides and defaults
|
||||||
const {
|
const { titleColor, textColor, iconColor, bgColor, borderColor } =
|
||||||
titleColor,
|
getCardColors({
|
||||||
textColor,
|
title_color,
|
||||||
iconColor,
|
icon_color,
|
||||||
bgColor,
|
text_color,
|
||||||
borderColor,
|
bg_color,
|
||||||
} = getCardColors({
|
border_color,
|
||||||
title_color,
|
theme,
|
||||||
icon_color,
|
});
|
||||||
text_color,
|
|
||||||
bg_color,
|
|
||||||
border_color,
|
|
||||||
theme,
|
|
||||||
});
|
|
||||||
|
|
||||||
const totalStars = kFormatter(stargazers.totalCount);
|
const totalStars = kFormatter(stargazers.totalCount);
|
||||||
const totalForks = kFormatter(forkCount);
|
const totalForks = kFormatter(forkCount);
|
||||||
@@ -96,21 +92,24 @@ const renderRepoCard = (repo, options = {}) => {
|
|||||||
|
|
||||||
const svgLanguage = primaryLanguage
|
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}" />
|
<circle data-testid="lang-color" cx="0" cy="-5" r="6" fill="${langColor}" />
|
||||||
<text data-testid="lang-name" class="gray" x="15">${langName}</text>
|
<text data-testid="lang-name" class="gray" x="15">${langName}</text>
|
||||||
</g>
|
</g>
|
||||||
`
|
`
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
|
const iconSize = 16;
|
||||||
const iconWithLabel = (icon, label, testid) => {
|
const iconWithLabel = (icon, label, testid) => {
|
||||||
return `
|
const iconSvg = `
|
||||||
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="${iconSize}" height="${iconSize}">
|
||||||
${icon}
|
${icon}
|
||||||
</svg>
|
</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 =
|
const svgStars =
|
||||||
stargazers.totalCount > 0 &&
|
stargazers.totalCount > 0 &&
|
||||||
iconWithLabel(icons.star, totalStars, "stargazers");
|
iconWithLabel(icons.star, totalStars, "stargazers");
|
||||||
@@ -118,8 +117,13 @@ const renderRepoCard = (repo, options = {}) => {
|
|||||||
forkCount > 0 && iconWithLabel(icons.fork, totalForks, "forkcount");
|
forkCount > 0 && iconWithLabel(icons.fork, totalForks, "forkcount");
|
||||||
|
|
||||||
const starAndForkCount = flexLayout({
|
const starAndForkCount = flexLayout({
|
||||||
items: [svgStars, svgForks],
|
items: [svgLanguage, svgStars, svgForks],
|
||||||
gap: 65,
|
sizes: [
|
||||||
|
measureText(langName, 12),
|
||||||
|
iconSize + measureText(`${totalStars}`, 12),
|
||||||
|
iconSize + measureText(`${totalForks}`, 12),
|
||||||
|
],
|
||||||
|
gap: 25,
|
||||||
}).join("");
|
}).join("");
|
||||||
|
|
||||||
const card = new Card({
|
const card = new Card({
|
||||||
@@ -163,15 +167,8 @@ const renderRepoCard = (repo, options = {}) => {
|
|||||||
.join("")}
|
.join("")}
|
||||||
</text>
|
</text>
|
||||||
|
|
||||||
<g transform="translate(0, ${height - 75})">
|
<g transform="translate(30, ${height - 75})">
|
||||||
${svgLanguage}
|
${starAndForkCount}
|
||||||
|
|
||||||
<g
|
|
||||||
data-testid="star-fork-group"
|
|
||||||
transform="translate(${primaryLanguage ? 155 - shiftText : 25}, 0)"
|
|
||||||
>
|
|
||||||
${starAndForkCount}
|
|
||||||
</g>
|
|
||||||
</g>
|
</g>
|
||||||
`);
|
`);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ const {
|
|||||||
getCardColors,
|
getCardColors,
|
||||||
flexLayout,
|
flexLayout,
|
||||||
lowercaseTrim,
|
lowercaseTrim,
|
||||||
|
measureText,
|
||||||
|
chunkArray,
|
||||||
} = require("../common/utils");
|
} = require("../common/utils");
|
||||||
|
|
||||||
const DEFAULT_CARD_WIDTH = 300;
|
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 percentage = ((lang.size / totalSize) * 100).toFixed(2);
|
||||||
const color = lang.color || "#858585";
|
const color = lang.color || "#858585";
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<g transform="translate(${x}, ${y})">
|
<g>
|
||||||
<circle cx="5" cy="6" r="5" fill="${color}" />
|
<circle cx="5" cy="6" r="5" fill="${color}" />
|
||||||
<text data-testid="lang-name" x="15" y="10" class='lang-name'>
|
<text data-testid="lang-name" x="15" y="10" class='lang-name'>
|
||||||
${lang.name} ${percentage}%
|
${lang.name} ${percentage}%
|
||||||
@@ -47,25 +49,38 @@ const createCompactLangNode = ({ lang, totalSize, x, y }) => {
|
|||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const createLanguageTextNode = ({ langs, totalSize, x, y }) => {
|
const getLongestLang = (arr) =>
|
||||||
return langs.map((lang, index) => {
|
arr.reduce(
|
||||||
if (index % 2 === 0) {
|
(savedLang, lang) =>
|
||||||
return createCompactLangNode({
|
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,
|
lang,
|
||||||
x,
|
|
||||||
y: 12.5 * index + y,
|
|
||||||
totalSize,
|
totalSize,
|
||||||
index,
|
index,
|
||||||
});
|
}),
|
||||||
}
|
);
|
||||||
return createCompactLangNode({
|
return flexLayout({
|
||||||
lang,
|
items,
|
||||||
x: 150,
|
gap: 25,
|
||||||
y: 12.5 + 12.5 * index,
|
direction: "column",
|
||||||
totalSize,
|
}).join("");
|
||||||
index,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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" />
|
<rect x="0" y="0" width="${offsetWidth}" height="8" fill="white" rx="5" />
|
||||||
</mask>
|
</mask>
|
||||||
${compactProgressBar}
|
${compactProgressBar}
|
||||||
${createLanguageTextNode({
|
|
||||||
x: 0,
|
<g transform="translate(0, 25)">
|
||||||
y: 25,
|
${createLanguageTextNode({
|
||||||
langs,
|
langs,
|
||||||
totalSize: totalLanguageSize,
|
totalSize: totalLanguageSize,
|
||||||
}).join("")}
|
width,
|
||||||
|
})}
|
||||||
|
</g>
|
||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+31
-5
@@ -89,21 +89,26 @@ function request(data, headers) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {String[]} items
|
* @param {string[]} items
|
||||||
* @param {Number} gap
|
* @param {Number} gap
|
||||||
* @param {string} direction
|
* @param {"column" | "row"} direction
|
||||||
|
*
|
||||||
|
* @returns {string[]}
|
||||||
*
|
*
|
||||||
* @description
|
* @description
|
||||||
* Auto layout utility, allows us to layout things
|
* Auto layout utility, allows us to layout things
|
||||||
* vertically or horizontally with proper gaping
|
* 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
|
// filter() for filtering out empty strings
|
||||||
return items.filter(Boolean).map((item, i) => {
|
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") {
|
if (direction === "column") {
|
||||||
transform = `translate(0, ${gap * i})`;
|
transform = `translate(0, ${lastSize})`;
|
||||||
}
|
}
|
||||||
|
lastSize += size + gap;
|
||||||
return `<g transform="${transform}">${item}</g>`;
|
return `<g transform="${transform}">${item}</g>`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -232,6 +237,26 @@ function measureText(str, fontSize = 10) {
|
|||||||
}
|
}
|
||||||
const lowercaseTrim = (name) => name.toLowerCase().trim();
|
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 = {
|
module.exports = {
|
||||||
renderError,
|
renderError,
|
||||||
kFormatter,
|
kFormatter,
|
||||||
@@ -250,4 +275,5 @@ module.exports = {
|
|||||||
CONSTANTS,
|
CONSTANTS,
|
||||||
CustomError,
|
CustomError,
|
||||||
lowercaseTrim,
|
lowercaseTrim,
|
||||||
|
chunkArray,
|
||||||
};
|
};
|
||||||
@@ -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", () => {
|
it("should hide language if primaryLanguage is null & fallback to correct values", () => {
|
||||||
document.body.innerHTML = renderRepoCard({
|
document.body.innerHTML = renderRepoCard({
|
||||||
...data_repo.repository,
|
...data_repo.repository,
|
||||||
@@ -332,11 +302,13 @@ describe("Test renderRepoCard", () => {
|
|||||||
);
|
);
|
||||||
expect(queryByTestId(document.body, "badge")).toHaveTextContent("模板");
|
expect(queryByTestId(document.body, "badge")).toHaveTextContent("模板");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render without rounding", () => {
|
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");
|
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");
|
expect(document.querySelector("rect")).toHaveAttribute("rx", "4.5");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -44,27 +44,6 @@ describe("Test utils.js", () => {
|
|||||||
).toHaveTextContent(/Secondary Message/gim);
|
).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", () => {
|
it("getCardColors: should return expected values", () => {
|
||||||
let colors = getCardColors({
|
let colors = getCardColors({
|
||||||
title_color: "f00",
|
title_color: "f00",
|
||||||
|
|||||||
Reference in new issue
Block a user