Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfb07a69b9 | ||
|
|
41b51dcfd9 | ||
|
|
7687c47d85 | ||
|
|
7917d54fdf | ||
|
|
7771a34790 | ||
|
|
1c266ad51e | ||
|
|
6258d90604 | ||
|
|
6d857d0f34 | ||
|
|
e15deeac76 | ||
|
|
b5d50fc28f | ||
|
|
1d394c7fad | ||
|
|
76e09ce3b0 | ||
|
|
96d141e8c7 | ||
|
|
6310073428 | ||
|
|
1d8aaf0fc8 | ||
|
|
4a2785e286 | ||
|
|
16ea9afebe | ||
|
|
c707e6b7b1 | ||
|
|
9dc4910d4b | ||
|
|
5d1978011e | ||
|
|
d84cc27b9c | ||
|
|
e43e55c2b7 | ||
|
|
f884df6312 |
@@ -31,7 +31,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Install Dependencies
|
||||
uses: ./.github/actions/install-dependencies
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Install Dependencies
|
||||
uses: ./.github/actions/install-dependencies
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Install Dependencies
|
||||
uses: ./.github/actions/install-dependencies
|
||||
@@ -106,7 +106,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Install Dependencies
|
||||
uses: ./.github/actions/install-dependencies
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Install Dependencies
|
||||
uses: ./.github/actions/install-dependencies
|
||||
|
||||
@@ -1,18 +1,30 @@
|
||||
name: Update version tags
|
||||
name: Publish core package to npm
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- "**"
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [released]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
update-semver:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: haya14busa/action-update-semver@7d2c558640ea49e798d46539536190aff8c18715 # v1.5.1
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Install Dependencies
|
||||
uses: ./.github/actions/install-dependencies
|
||||
|
||||
- name: Verify core package
|
||||
run: |
|
||||
pnpm --filter @stats-organization/github-readme-stats-core run typecheck
|
||||
pnpm test
|
||||
pnpm build:packages
|
||||
pnpm --filter @stats-organization/github-readme-stats-core pack --dry-run
|
||||
|
||||
- name: Publish core package to npm
|
||||
run: pnpm --filter @stats-organization/github-readme-stats-core publish --provenance --access public --no-git-checks
|
||||
|
||||
@@ -15,6 +15,10 @@ on:
|
||||
|
||||
jobs:
|
||||
triggerRepeatRecent:
|
||||
if: |
|
||||
github.repository == 'anuraghazra/github-readme-stats' ||
|
||||
github.repository == 'stats-organization/github-readme-stats' ||
|
||||
github.repository == 'stats-organization/github-stats-extended'
|
||||
name: Trigger server to update cached data.
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Install Dependencies
|
||||
uses: ./.github/actions/install-dependencies
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
name: Update version tags
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- "**"
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
update-semver:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- uses: haya14busa/action-update-semver@7d2c558640ea49e798d46539536190aff8c18715 # v1.5.1
|
||||
@@ -5,13 +5,14 @@
|
||||
<a href="https://github-stats-extended.vercel.app/api?username=anuraghazra"><img src="https://github-stats-extended.vercel.app/api?username=anuraghazra"></a>
|
||||
</div>
|
||||
|
||||
This project is an [extended version](docs/fork.md) of [github-readme-stats](https://github.com/anuraghazra/github-readme-stats). It generates [various stats cards](#card-types), e.g. about your GitHub contributions, your top languages, etc. You can [customize](#advanced-customization) the cards via multiple parameters.
|
||||
This project is the [extended, actively maintained successor](docs/fork.md) of [github-readme-stats](https://github.com/anuraghazra/github-readme-stats). It generates [various stats cards](#card-types), e.g. about your GitHub contributions, your top languages, etc. You can [customize](#advanced-customization) the cards via multiple parameters.
|
||||
|
||||
# Table of Contents
|
||||
- [Quick Start](#quick-start)
|
||||
- [Migration from github-readme-stats](#migration-from-github-readme-stats)
|
||||
- [Card Types](#card-types)
|
||||
- [Advanced Customization](#advanced-customization)
|
||||
- [Self-Hosting](#self-hosting)
|
||||
- [Run It Yourself](#run-it-yourself)
|
||||
- [Acknowledgements](#acknowledgements)
|
||||
- [Contributing](#contributing)
|
||||
|
||||
@@ -27,6 +28,16 @@ This project is an [extended version](docs/fork.md) of [github-readme-stats](htt
|
||||
|
||||
As more comfortable alternative, use the [GitHub-Stats-Extended Wizard](https://github-stats-extended.vercel.app/frontend) to create your custom stats card. Copy the generated markdown code and paste it into your [GitHub profile README](https://docs.github.com/en/account-and-profile/how-tos/profile-customization/managing-your-profile-readme#adding-a-profile-readme). Done!
|
||||
|
||||
# Migration from github-readme-stats
|
||||
To migrate from [github-readme-stats](https://github.com/anuraghazra/github-readme-stats) you only need to change the domain from `github-readme-stats.vercel.app` to `github-stats-extended.vercel.app`:
|
||||
|
||||
```diff
|
||||
- https://github-readme-stats.vercel.app/api?username=octocat&theme=radical
|
||||
+ https://github-stats-extended.vercel.app/api?username=octocat&theme=radical
|
||||
```
|
||||
|
||||
GitHub-Stats-Extended aims to be fully compatible with github-readme-stats. For more details see [Compatibility Notes](docs/fork.md#compatibility-notes).
|
||||
|
||||
# Card Types
|
||||
- Show your GitHub statistics:
|
||||
|
||||
@@ -55,13 +66,13 @@ As more comfortable alternative, use the [GitHub-Stats-Extended Wizard](https://
|
||||
# Advanced Customization
|
||||
The [GitHub-Stats-Extended Wizard](https://github-stats-extended.vercel.app/frontend) offers some essential customization options. For more advanced customization check out the [advanced documentation](docs/advanced_documentation.md).
|
||||
|
||||
# Run It Yourself
|
||||
If you want to run GitHub-Stats-Extended on your own, there are two main deployment options: you can use [github-readme-stats-action](https://github.com/stats-organization/github-readme-stats-action) to generate cards in your own GitHub Actions workflow. Or you can self-host GitHub-Stats-Extended on Vercel.
|
||||
|
||||
See [Run It Yourself](docs/deploy.md) for detailed instructions.
|
||||
|
||||
# Acknowledgements
|
||||
This project is based on [github-readme-stats](https://github.com/anuraghazra/github-readme-stats). On top of their functionality I added several new features and improvements. See [Fork Information](docs/fork.md) for a list of changes. The frontend I added to the project is based on [GitHub Trends](https://github.com/avgupta456/github-trends). Big thanks to [@anuraghazra](https://github.com/anuraghazra), [@avgupta456](https://github.com/avgupta456), [@rickstaa](https://github.com/rickstaa), [@qwerty541](https://github.com/qwerty541) and everyone else who worked on these projects! ❤️
|
||||
|
||||
# Self-Hosting
|
||||
Since the GitHub API only allows a limited number of requests per hour, the public instance of GitHub-Stats-Extended at https://github-stats-extended.vercel.app/api could possibly hit the rate limiter. If you host your own instance you do not have to worry about anything. Also, if you don't want to give my GitHub-Stats-Extended instance access to your private contributions but still want to include these contributions in your stats, you can simply host your own instance.
|
||||
|
||||
See [Deploy on your own](docs/deploy.md) for various deployment options.
|
||||
This project is based on [github-readme-stats](https://github.com/anuraghazra/github-readme-stats). On top of that project's functionality GitHub-Stats-Extended adds several new features and improvements. See [Fork Information](docs/fork.md) for a list of changes. The frontend added to GitHub-Stats-Extended is based on [GitHub Trends](https://github.com/avgupta456/github-trends). Big thanks to [@anuraghazra](https://github.com/anuraghazra), [@avgupta456](https://github.com/avgupta456), [@rickstaa](https://github.com/rickstaa), [@qwerty541](https://github.com/qwerty541) and everyone else who worked on these projects! ❤️
|
||||
|
||||
# Contributing
|
||||
Contributions are welcome!
|
||||
@@ -18,12 +18,12 @@
|
||||
"devDependencies": {
|
||||
"axios-mock-adapter": "2.1.0",
|
||||
"express": "5.2.1",
|
||||
"jsdom": "29.0.2",
|
||||
"jsdom": "catalog:default",
|
||||
"vitest": "catalog:default"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.15.2",
|
||||
"axios": "catalog:default",
|
||||
"@stats-organization/github-readme-stats-core": "workspace:^",
|
||||
"pg": "^8.20.0"
|
||||
"pg": "^8.21.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ exports[`Test /api/gist contract > should match the private missing-id response
|
||||
|
||||
exports[`Test /api/gist contract > should match the public happy-path response snapshot 1`] = `
|
||||
{
|
||||
"content": "<svg width="400" height="192" viewBox="0 0 400 192" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="descId">
|
||||
"content": "<svg width="400" height="180" viewBox="0 0 400 180" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="descId">
|
||||
<title id="titleId"/>
|
||||
<desc id="descId"/>
|
||||
<style>
|
||||
@@ -44,7 +44,10 @@ exports[`Test /api/gist contract > should match the public happy-path response s
|
||||
.header { font-size: 15.5px; }
|
||||
}
|
||||
|
||||
.description { font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
|
||||
.description {
|
||||
font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif;fill: #434d58;
|
||||
|
||||
}
|
||||
.gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
|
||||
.icon { fill: #586069 }
|
||||
|
||||
@@ -90,20 +93,20 @@ exports[`Test /api/gist contract > should match the public happy-path response s
|
||||
<g data-testid="main-card-body" transform="translate(0, 55)">
|
||||
|
||||
<text class="description" x="25" y="-5">
|
||||
<tspan dy="1.2em" x="25">List of countries and territories in English and Spanish:</tspan><tspan dy="1.2em" x="25">name, continent, capital, dial code, country codes, TLD,</tspan><tspan dy="1.2em" x="25">and area in sq km. Lista de países y territorios en</tspan><tspan dy="1.2em" x="25">Inglés y Español: nombre, continente, capital,</tspan><tspan dy="1.2em" x="25">código de teléfono, códigos de país,</tspan><tspan dy="1.2em" x="25">dominio y área en km cuadrados. Updated 2023</tspan>
|
||||
<tspan dy="1.2em" x="25">List of countries and territories in English and Spanish: name,</tspan><tspan dy="1.2em" x="25">continent, capital, dial code, country codes, TLD, and area in</tspan><tspan dy="1.2em" x="25">sq km. Lista de países y territorios en Inglés y Español:</tspan><tspan dy="1.2em" x="25">nombre, continente, capital, código de teléfono, códigos de</tspan><tspan dy="1.2em" x="25">país, dominio y área en km cuadrados. Updated 2023</tspan>
|
||||
</text>
|
||||
|
||||
<g transform="translate(30, 117)">
|
||||
<g transform="translate(30, 105)">
|
||||
<g transform="translate(0, 0)">
|
||||
<g data-testid="primary-lang">
|
||||
<circle data-testid="lang-color" cx="0" cy="-5" r="6" fill="#292929"/>
|
||||
<text data-testid="lang-name" class="gray" x="15">JSON</text>
|
||||
</g>
|
||||
</g><g transform="translate(56.96875, 0)"><g transform="translate(0, 0)">
|
||||
</g><g transform="translate(53.67999954223633, 0)"><g transform="translate(0, 0)">
|
||||
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||
<path fill-rule="evenodd" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"/>
|
||||
</svg>
|
||||
</g><g transform="translate(20, 0)"><text data-testid="starsCount" class="gray">33</text></g></g><g transform="translate(111.28125, 0)"><g transform="translate(0, 0)">
|
||||
</g><g transform="translate(20, 0)"><text data-testid="starsCount" class="gray">33</text></g></g><g transform="translate(107.59999923706056, 0)"><g transform="translate(0, 0)">
|
||||
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||
<path fill-rule="evenodd" d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25v-.878a2.25 2.25 0 10-1.5 0v.878a.75.75 0 01-.75.75h-4.5A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"/>
|
||||
</svg>
|
||||
@@ -127,7 +130,7 @@ exports[`Test /api/gist contract > should match the public happy-path response s
|
||||
|
||||
exports[`Test /api/gist contract > should match the public many-params response snapshot 1`] = `
|
||||
{
|
||||
"content": "<svg width="400" height="192" viewBox="0 0 400 192" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="descId">
|
||||
"content": "<svg width="400" height="180" viewBox="0 0 400 180" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="descId">
|
||||
<title id="titleId"/>
|
||||
<desc id="descId"/>
|
||||
<style>
|
||||
@@ -141,7 +144,10 @@ exports[`Test /api/gist contract > should match the public many-params response
|
||||
.header { font-size: 15.5px; }
|
||||
}
|
||||
|
||||
.description { font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif; fill: #abcdef }
|
||||
.description {
|
||||
font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif;fill: #abcdef;
|
||||
|
||||
}
|
||||
.gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #abcdef }
|
||||
.icon { fill: #ff00aa }
|
||||
|
||||
@@ -187,20 +193,20 @@ exports[`Test /api/gist contract > should match the public many-params response
|
||||
<g data-testid="main-card-body" transform="translate(0, 55)">
|
||||
|
||||
<text class="description" x="25" y="-5">
|
||||
<tspan dy="1.2em" x="25">List of countries and territories in English and Spanish:</tspan><tspan dy="1.2em" x="25">name, continent, capital, dial code, country codes, TLD,</tspan><tspan dy="1.2em" x="25">and area in sq km. Lista de países y territorios en</tspan><tspan dy="1.2em" x="25">Inglés y Español: nombre, continente, capital,</tspan><tspan dy="1.2em" x="25">código de teléfono, códigos de país,</tspan><tspan dy="1.2em" x="25">dominio y área en km cuadrados. Updated 2023</tspan>
|
||||
<tspan dy="1.2em" x="25">List of countries and territories in English and Spanish: name,</tspan><tspan dy="1.2em" x="25">continent, capital, dial code, country codes, TLD, and area in</tspan><tspan dy="1.2em" x="25">sq km. Lista de países y territorios en Inglés y Español:</tspan><tspan dy="1.2em" x="25">nombre, continente, capital, código de teléfono, códigos de</tspan><tspan dy="1.2em" x="25">país, dominio y área en km cuadrados. Updated 2023</tspan>
|
||||
</text>
|
||||
|
||||
<g transform="translate(30, 117)">
|
||||
<g transform="translate(30, 105)">
|
||||
<g transform="translate(0, 0)">
|
||||
<g data-testid="primary-lang">
|
||||
<circle data-testid="lang-color" cx="0" cy="-5" r="6" fill="#292929"/>
|
||||
<text data-testid="lang-name" class="gray" x="15">JSON</text>
|
||||
</g>
|
||||
</g><g transform="translate(56.96875, 0)"><g transform="translate(0, 0)">
|
||||
</g><g transform="translate(53.67999954223633, 0)"><g transform="translate(0, 0)">
|
||||
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||
<path fill-rule="evenodd" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"/>
|
||||
</svg>
|
||||
</g><g transform="translate(20, 0)"><text data-testid="starsCount" class="gray">33</text></g></g><g transform="translate(111.28125, 0)"><g transform="translate(0, 0)">
|
||||
</g><g transform="translate(20, 0)"><text data-testid="starsCount" class="gray">33</text></g></g><g transform="translate(107.59999923706056, 0)"><g transform="translate(0, 0)">
|
||||
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||
<path fill-rule="evenodd" d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25v-.878a2.25 2.25 0 10-1.5 0v.878a.75.75 0 01-.75.75h-4.5A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"/>
|
||||
</svg>
|
||||
|
||||
@@ -100,11 +100,14 @@ exports[`Test /api/pin contract > should match the public happy-path response sn
|
||||
.header { font-size: 15.5px; }
|
||||
}
|
||||
|
||||
.description { font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
|
||||
.description {
|
||||
font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif;fill: #434d58;
|
||||
|
||||
}
|
||||
.gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
|
||||
.badge { font: 600 11px 'Segoe UI', Ubuntu, Sans-Serif; }
|
||||
.badge rect { opacity: 0.2 }
|
||||
|
||||
|
||||
.stat { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
|
||||
.stagger {
|
||||
opacity: 0;
|
||||
@@ -160,8 +163,8 @@ exports[`Test /api/pin contract > should match the public happy-path response sn
|
||||
|
||||
|
||||
|
||||
<text class="description" x="25" y="-5">
|
||||
<tspan dy="1.2em" x="25">Help us take over the world with a deeply customizable</tspan><tspan dy="1.2em" x="25">React, TypeScript and GraphQL chat app that has enough text</tspan><tspan dy="1.2em" x="25">to wrap across multiple lines in the repository card.</tspan>
|
||||
<text class="description" x="25" y="-5">
|
||||
<tspan dy="1.2em" x="25">Help us take over the world with a deeply customizable</tspan><tspan dy="1.2em" x="25">React, TypeScript and GraphQL chat app that has enough</tspan><tspan dy="1.2em" x="25">text to wrap across multiple lines in the repository card.</tspan>
|
||||
</text>
|
||||
|
||||
<g transform="translate(30, 75)">
|
||||
@@ -170,11 +173,11 @@ exports[`Test /api/pin contract > should match the public happy-path response sn
|
||||
<circle data-testid="lang-color" cx="0" cy="-5" r="6" fill="#2b7489"/>
|
||||
<text data-testid="lang-name" class="gray" x="15">TypeScript</text>
|
||||
</g>
|
||||
</g><g transform="translate(82.24375, 0)"><g transform="translate(0, 0)">
|
||||
</g><g transform="translate(80.53999967575072, 0)"><g transform="translate(0, 0)">
|
||||
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||
<path fill-rule="evenodd" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"/>
|
||||
</svg>
|
||||
</g><g transform="translate(20, 0)"><text data-testid="stargazers" class="gray">38k</text></g></g><g transform="translate(142.55625, 0)"><g transform="translate(0, 0)">
|
||||
</g><g transform="translate(20, 0)"><text data-testid="stargazers" class="gray">38k</text></g></g><g transform="translate(140.4199994087219, 0)"><g transform="translate(0, 0)">
|
||||
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||
<path fill-rule="evenodd" d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25v-.878a2.25 2.25 0 10-1.5 0v.878a.75.75 0 01-.75.75h-4.5A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"/>
|
||||
</svg>
|
||||
@@ -217,11 +220,14 @@ exports[`Test /api/pin contract > should match the public many-params response s
|
||||
.header { font-size: 15.5px; }
|
||||
}
|
||||
|
||||
.description { font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif; fill: #abcdef }
|
||||
.description {
|
||||
font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif;fill: #abcdef;
|
||||
|
||||
}
|
||||
.gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #abcdef }
|
||||
.badge { font: 600 11px 'Segoe UI', Ubuntu, Sans-Serif; }
|
||||
.badge rect { opacity: 0.2 }
|
||||
|
||||
|
||||
.stat { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #abcdef }
|
||||
.stagger {
|
||||
opacity: 0;
|
||||
@@ -277,7 +283,7 @@ exports[`Test /api/pin contract > should match the public many-params response s
|
||||
|
||||
|
||||
|
||||
<text class="description" x="25" y="-5">
|
||||
<text class="description" x="25" y="-5">
|
||||
<tspan dy="1.2em" x="25">Help us take over the world with a deeply customizable React, TypeScript and GraphQL...</tspan>
|
||||
</text>
|
||||
|
||||
@@ -287,11 +293,11 @@ exports[`Test /api/pin contract > should match the public many-params response s
|
||||
<circle data-testid="lang-color" cx="0" cy="-5" r="6" fill="#2b7489"/>
|
||||
<text data-testid="lang-name" class="gray" x="15">TypeScript</text>
|
||||
</g>
|
||||
</g><g transform="translate(82.24375, 0)"><g transform="translate(0, 0)">
|
||||
</g><g transform="translate(80.53999967575072, 0)"><g transform="translate(0, 0)">
|
||||
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||
<path fill-rule="evenodd" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"/>
|
||||
</svg>
|
||||
</g><g transform="translate(20, 0)"><text data-testid="stargazers" class="gray">38k</text></g></g><g transform="translate(142.55625, 0)"><g transform="translate(0, 0)">
|
||||
</g><g transform="translate(20, 0)"><text data-testid="stargazers" class="gray">38k</text></g></g><g transform="translate(140.4199994087219, 0)"><g transform="translate(0, 0)">
|
||||
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||
<path fill-rule="evenodd" d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25v-.878a2.25 2.25 0 10-1.5 0v.878a.75.75 0 01-.75.75h-4.5A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"/>
|
||||
</svg>
|
||||
|
||||
+10
-10
@@ -13,18 +13,18 @@
|
||||
"typecheck": "tsc -p tsconfig.typecheck.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "^2.11.2",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"axios": "^1",
|
||||
"@reduxjs/toolkit": "^2.12.0",
|
||||
"@tailwindcss/vite": "^4.3.0",
|
||||
"axios": "catalog:default",
|
||||
"axios-cache-interceptor": "^1",
|
||||
"daisyui": "^5.5.19",
|
||||
"daisyui": "^5.5.23",
|
||||
"@stats-organization/github-readme-stats-backend": "workspace:^",
|
||||
"@stats-organization/github-readme-stats-core": "workspace:^",
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.5",
|
||||
"react": "^19.2.7",
|
||||
"react-dom": "^19.2.7",
|
||||
"react-icons": "^5.6.0",
|
||||
"react-loading-skeleton": "^3.3.1",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-redux": "^9.3.0",
|
||||
"react-spinners": "^0.17.0",
|
||||
"react-toastify": "^11.1.0",
|
||||
"redux": "^5.0.1",
|
||||
@@ -32,11 +32,11 @@
|
||||
"uuid": "^14.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "19.2.14",
|
||||
"@types/react": "19.2.17",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@vitejs/plugin-react": "6.0.1",
|
||||
"@vitejs/plugin-react": "6.0.2",
|
||||
"clsx": "2.1.1",
|
||||
"tailwindcss": "4.2.2",
|
||||
"tailwindcss": "4.3.0",
|
||||
"vite": "catalog:default",
|
||||
"vitest": "catalog:default"
|
||||
},
|
||||
|
||||
@@ -85,6 +85,50 @@ You can look at a preview for [all available themes](../packages/core/src/themes
|
||||
|
||||
Since GitHub will re-upload the cards and serve them from their [CDN](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/about-anonymized-urls), we can not infer the browser/GitHub theme on the server side. There are, however, four methods you can use to create dynamics themes on the client side.
|
||||
|
||||
##### Use GitHub's new media feature (recommended)
|
||||
|
||||
You can use [GitHub's new media feature](https://github.blog/changelog/2022-05-19-specify-theme-context-for-images-in-markdown-beta/) in HTML to specify whether to display images for light or dark themes. This is done using the HTML `<picture>` element in combination with the `prefers-color-scheme` media feature.
|
||||
|
||||
```html
|
||||
<picture>
|
||||
<source
|
||||
srcset="https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true&theme=dark"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
/>
|
||||
<img src="https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true" /> <!-- light mode -->
|
||||
</picture>
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>:eyes: Show example</summary>
|
||||
|
||||
<picture>
|
||||
<source
|
||||
srcset="https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true&theme=dark"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
/>
|
||||
<img src="https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true" /> <!-- light mode -->
|
||||
</picture>
|
||||
|
||||
</details>
|
||||
|
||||
##### Use GitHub's theme context tag
|
||||
|
||||
You can use [GitHub's theme context](https://github.blog/changelog/2021-11-24-specify-theme-context-for-images-in-markdown/) tags to switch the theme based on the user GitHub theme automatically. This is done by appending `#gh-dark-mode-only` or `#gh-light-mode-only` to the end of an image URL. This tag will define whether the image specified in the markdown is only shown to viewers using a light or a dark GitHub theme:
|
||||
|
||||
```md
|
||||
[](https://github.com/stats-organization/github-stats-extended#gh-dark-mode-only)
|
||||
[](https://github.com/stats-organization/github-stats-extended#gh-light-mode-only)
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>:eyes: Show example</summary>
|
||||
|
||||
[](https://github.com/stats-organization/github-stats-extended#gh-dark-mode-only)
|
||||
[](https://github.com/stats-organization/github-stats-extended#gh-light-mode-only)
|
||||
|
||||
</details>
|
||||
|
||||
##### Use the transparent theme
|
||||
|
||||
We have included a `transparent` theme that has a transparent background. This theme is optimized to look good on GitHub's dark and light default themes. You can enable this theme using the `&theme=transparent` parameter like so:
|
||||
@@ -115,58 +159,6 @@ You can use the `bg_color` parameter to make any of [the available themes](../pa
|
||||
|
||||
</details>
|
||||
|
||||
##### Use GitHub's theme context tag
|
||||
|
||||
You can use [GitHub's theme context](https://github.blog/changelog/2021-11-24-specify-theme-context-for-images-in-markdown/) tags to switch the theme based on the user GitHub theme automatically. This is done by appending `#gh-dark-mode-only` or `#gh-light-mode-only` to the end of an image URL. This tag will define whether the image specified in the markdown is only shown to viewers using a light or a dark GitHub theme:
|
||||
|
||||
```md
|
||||
[](https://github.com/stats-organization/github-stats-extended#gh-dark-mode-only)
|
||||
[](https://github.com/stats-organization/github-stats-extended#gh-light-mode-only)
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>:eyes: Show example</summary>
|
||||
|
||||
[](https://github.com/stats-organization/github-stats-extended#gh-dark-mode-only)
|
||||
[](https://github.com/stats-organization/github-stats-extended#gh-light-mode-only)
|
||||
|
||||
</details>
|
||||
|
||||
##### Use GitHub's new media feature
|
||||
|
||||
You can use [GitHub's new media feature](https://github.blog/changelog/2022-05-19-specify-theme-context-for-images-in-markdown-beta/) in HTML to specify whether to display images for light or dark themes. This is done using the HTML `<picture>` element in combination with the `prefers-color-scheme` media feature.
|
||||
|
||||
```html
|
||||
<picture>
|
||||
<source
|
||||
srcset="https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true&theme=dark"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
/>
|
||||
<source
|
||||
srcset="https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true"
|
||||
media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)"
|
||||
/>
|
||||
<img src="https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true" />
|
||||
</picture>
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>:eyes: Show example</summary>
|
||||
|
||||
<picture>
|
||||
<source
|
||||
srcset="https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true&theme=dark"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
/>
|
||||
<source
|
||||
srcset="https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true"
|
||||
media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)"
|
||||
/>
|
||||
<img src="https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true" />
|
||||
</picture>
|
||||
|
||||
</details>
|
||||
|
||||
### Customization
|
||||
|
||||
You can customize the appearance of all your cards however you wish with URL parameters.
|
||||
@@ -323,6 +315,7 @@ You can customize the appearance and behavior of the pinned repository card usin
|
||||
| Name | Description | Type | Default value |
|
||||
| --- | --- | --- | --- |
|
||||
| `show_owner` | Shows the repo's owner name. | boolean | `false` |
|
||||
| `browser_rendering` | Compute text wrapping of repository description natively in the browser, instead of computing it server-side. | boolean | `false` |
|
||||
| `description_lines_count` | Manually set the number of lines for the description. Specified value will be clamped between 1 and 3. If this parameter is not specified, the number of lines will be automatically adjusted according to the actual length of the description. | number | `null` |
|
||||
| `card_width` | Sets the card's width manually. | number | `400px (approx.)` |
|
||||
| `show_icons` | Shows icons near all stats enabled via `show`. | boolean | `true` |
|
||||
@@ -368,6 +361,7 @@ You can customize the appearance and behavior of the gist card using the [common
|
||||
| Name | Description | Type | Default value |
|
||||
| --- | --- | --- | --- |
|
||||
| `show_owner` | Shows the gist's owner name. | boolean | `false` |
|
||||
| `browser_rendering` | Compute text wrapping of gist description natively in the browser, instead of computing it server-side. | boolean | `false` |
|
||||
|
||||
### Demo
|
||||
|
||||
|
||||
+18
-37
@@ -1,12 +1,12 @@
|
||||
# Deploy on your own
|
||||
# Run It Yourself
|
||||
|
||||
Since the GitHub API only allows a limited number of requests per hour, my `https://github-stats-extended.vercel.app/api` could possibly hit the rate limiter. If you deploy it yourself via GitHub Actions or your own hosted instance, then you do not have to worry about anything. Also, if you don't want to give my GitHub-Stats-Extended instance access to your private contributions but still want to include these contributions in your stats, you can simply host your own instance.
|
||||
We cache generated cards for a few hours to avoid potential rate-limiting in the GitHub API or on Vercel. If you want to set your own cache duration or you want to include private contributions in your stats without granting our hosted version of GitHub-Stats-Extended access to your private contributions, you can run GitHub-Stats-Extended on your own.
|
||||
|
||||
GitHub Actions is the simplest setup with static SVGs stored in your repo but less frequent updates, while self-hosting takes more work and can serve fresher stats (with caching).
|
||||
GitHub Actions is the simplest setup with static SVGs stored in your repo but less frequent updates, while self-hosting GitHub-Stats-Extended on Vercel takes more work and can serve fresher stats (with caching).
|
||||
|
||||
## GitHub Actions
|
||||
## GitHub Action
|
||||
|
||||
GitHub Actions generates static SVGs and avoids per-request API calls. By default it uses `GITHUB_TOKEN` (public stats only), for private stats, set a [PAT](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) as a secret and pass it to the action instead.
|
||||
With [github-readme-stats-action](https://github.com/stats-organization/github-readme-stats-action) you can generate static cards in your GitHub Actions workflow, commit them to your profile repository, and embed them directly from there. This avoids any per-request API calls.
|
||||
|
||||
Create `/.github/workflows/grs.yml` in your profile repo (`USERNAME/USERNAME`):
|
||||
|
||||
@@ -15,17 +15,21 @@ name: Update README cards
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 3 * * *"
|
||||
- cron: "0 0 * * *" # Runs once daily at midnight
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Generate stats card
|
||||
uses: readme-tools/github-readme-stats-action@v1
|
||||
uses: stats-organization/github-readme-stats-action@v2
|
||||
with:
|
||||
card: stats
|
||||
options: username=${{ github.repository_owner }}&show_icons=true
|
||||
@@ -34,14 +38,14 @@ jobs:
|
||||
|
||||
- name: Commit cards
|
||||
run: |
|
||||
git config user.name "github-actions"
|
||||
git config user.email "github-actions@users.noreply.github.com"
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git add profile/*.svg
|
||||
git commit -m "Update README cards" || exit 0
|
||||
git push
|
||||
```
|
||||
|
||||
Then embed from your profile README:
|
||||
Then embed from your [profile README](https://docs.github.com/en/account-and-profile/how-tos/profile-customization/managing-your-profile-readme#adding-a-profile-readme):
|
||||
|
||||
```md
|
||||

|
||||
@@ -49,7 +53,7 @@ Then embed from your profile README:
|
||||
|
||||
See more options and examples in the [GitHub Readme Stats Action README](https://github.com/stats-organization/github-readme-stats-action#readme).
|
||||
|
||||
## Self-hosted (Vercel/Other)
|
||||
## Self-hosted on Vercel
|
||||
|
||||
Running your own instance avoids public rate limits and gives you full control over caching, tokens, and private stats.
|
||||
|
||||
@@ -88,14 +92,11 @@ Selecting the right scopes for your token is important in case you want to displ
|
||||
|
||||
### On Vercel
|
||||
|
||||
<b>:film\_projector: [Check Out Step By Step Video Tutorial By @codeSTACKr](https://youtu.be/n6d4KHSKqGk?t=107)</b>
|
||||
|
||||
Click on the deploy button to get started!
|
||||
|
||||
[](https://vercel.com/import/project?template=https://github.com/stats-organization/github-stats-extended)
|
||||
|
||||
<details>
|
||||
<summary><b>:hammer_and_wrench: Recommended: Step-by-step guide on setting up your own Vercel instance</b></summary>
|
||||
<b>Recommended: Step-by-step guide on setting up your own Vercel instance</b>
|
||||
|
||||
1. Go to [vercel.com](https://vercel.com/).
|
||||
2. Click on `Log in`.
|
||||
@@ -122,27 +123,7 @@ Click on the deploy button to get started!
|
||||
12. Click deploy, and you're good to go. See your domains to use the API!
|
||||
13. optional: add an SQL database; by using e.g. the ["Nile" integration](https://vercel.com/marketplace/nile) or by manually setting the environment variable `POSTGRES_URL`
|
||||
14. optional: [create your own OAuth App](https://github.com/settings/developers) and set environment variables `OAUTH_REDIRECT_URI`, `OAUTH_CLIENT_ID` and `OAUTH_CLIENT_SECRET` on Vercel accordingly
|
||||
15. optional: in addition to the Vercel project based on the `apps/backend` folder, create a second project based on the `apps/frontend` folder. No environment variables needed.
|
||||
16. optional: set the environment variable `TURBO_PLATFORM_ENV_DISABLED` to `true` to disable the build-time warning from [turbo](https://turborepo.dev/) about environment variables missing from "turbo.json" - This warning is not relevant in our project.
|
||||
|
||||
</details>
|
||||
|
||||
### On other platforms
|
||||
|
||||
> [!WARNING]
|
||||
> This way of using GitHub-Stats-Extended is not officially supported and was added to cater to some particular use cases where Vercel could not be used (e.g. [#2341](https://github.com/anuraghazra/github-readme-stats/discussions/2341)). The support for this method, therefore, is limited.
|
||||
|
||||
<details>
|
||||
<summary><b>:hammer_and_wrench: Step-by-step guide for deploying on other platforms</b></summary>
|
||||
|
||||
1. Fork or clone this repo as per your needs
|
||||
2. Move `express` from the devDependencies to the dependencies section of `package.json`
|
||||
<https://github.com/anuraghazra/github-readme-stats/blob/ba7c2f8b55eac8452e479c8bd38b044d204d0424/package.json#L54-L61>
|
||||
3. Run `npm i` if needed (initial setup)
|
||||
4. Run `node express.js` to start the server, or set the entry point to `express.js` in `package.json` if you're deploying on a managed service
|
||||
<https://github.com/anuraghazra/github-readme-stats/blob/ba7c2f8b55eac8452e479c8bd38b044d204d0424/package.json#L11>
|
||||
5. You're done 🎉
|
||||
</details>
|
||||
15. optional: set the environment variable `TURBO_PLATFORM_ENV_DISABLED` to `true` to disable the build-time warning from [turbo](https://turborepo.dev/) about environment variables missing from "turbo.json" - This warning is not relevant in our project.
|
||||
|
||||
### Available environment variables
|
||||
|
||||
|
||||
+19
-9
@@ -1,6 +1,6 @@
|
||||
# Fork Information
|
||||
|
||||
This project is an actively maintained fork of [github-readme-stats](https://github.com/anuraghazra/github-readme-stats).
|
||||
This project is an actively maintained fork and extension of [github-readme-stats](https://github.com/anuraghazra/github-readme-stats).
|
||||
|
||||
## Key Differences
|
||||
|
||||
@@ -28,6 +28,11 @@ GitHub-Stats-Extended proactively precomputes and caches cards. This solves the
|
||||
### Multi-page fetching for accurate star counts
|
||||
GitHub-Stats-Extended fetches up to 1000 of your starred repositories to accurately compute your stars count. In github-readme-stats, this is limited to 100 repos because github-readme-stats doesn't have the above-mentioned performance improvements.
|
||||
|
||||
### Customization of top languages card
|
||||
GitHub-Stats-Extended allows you to display your top languages without any numbers via the `hide_values` parameter. And the `prog_bar_bg_color` parameter allows you to customize the background color of the progress bars, e.g. by setting it to white:
|
||||
|
||||

|
||||
|
||||
### Private contributions support
|
||||
GitHub-Stats-Extended can include private contributions in your stats cards. You no longer have to deploy your own instance for that. Just log into the [GitHub-Stats-Extended Wizard](https://github-stats-extended.vercel.app/frontend) via the "GitHub Private Access" button (or click "Upgrade to Private Access" if already logged in). This will allow GitHub-Stats-Extended to see your private contributions.
|
||||
|
||||
@@ -67,16 +72,21 @@ One of the valued maintainers [wrote](https://github.com/anuraghazra/github-read
|
||||
>
|
||||
> The volume is overwhelming for the small team, especially taking into account that right now I'm alone online and working only sometimes when I have a free hours, so it took some time to get to your PR.
|
||||
|
||||
So I decided to implement some of the highly requested features and make the enhanced project available to everyone.
|
||||
|
||||
## Upstream Sync Strategy
|
||||
|
||||
The maintainers of [github-readme-stats](https://github.com/anuraghazra/github-readme-stats) keep their project up to date with dependency upgrades and do occasionally merge some pull requests. I regularly merge the latest upstream changes into this fork to keep it in sync with the upstream repo.
|
||||
|
||||
I also try to structure my own changes in a way that minimizes merge conflicts with the upstream repo. So some of my changes might even be merged back into [github-readme-stats](https://github.com/anuraghazra/github-readme-stats) if the upstream maintainers are interested.
|
||||
So [@martin-mfg](https://github.com/martin-mfg) decided to fork the project, implement some of the highly requested features and make the enhanced project available to everyone. Since the initial release of this fork @martin-mfg joined forces with the maintainers of [github-readme-stats](https://github.com/anuraghazra/github-readme-stats) and GitHub-Stats-Extended is now becoming the successor of github-readme-stats.
|
||||
|
||||
## Compatibility Notes
|
||||
|
||||
GitHub-Stats-Extended aims to be fully compatible with [github-readme-stats](https://github.com/anuraghazra/github-readme-stats). Generally, additional functionality introduced in this fork has to be explicitly enabled via some parameter.
|
||||
GitHub-Stats-Extended aims to be fully compatible with [github-readme-stats](https://github.com/anuraghazra/github-readme-stats). Additional functionality introduced in this fork has to be explicitly enabled via some parameter.
|
||||
|
||||
So you can change an existing stats card url from [github-readme-stats](https://github.com/anuraghazra/github-readme-stats) to GitHub-Stats-Extended simply by changing the domain from `github-readme-stats.vercel.app` to `github-stats-extended.vercel.app`. The card will look the same.
|
||||
|
||||
There is only one exception to this: GitHub-Stats-Extended improves line wrapping for multi-line gist and repository descriptions.
|
||||
This should be an improvement for existing cards, but it still changes their appearance a bit.
|
||||
|
||||
Previously, line wrapping happened simply after 59 characters, with special handling for Chinese characters:
|
||||
|
||||
<img width="400" height="140" alt="character-based" src="https://github.com/user-attachments/assets/1cf7edba-7f6c-4a37-89d7-334cbe54f0f1" />
|
||||
|
||||
GitHub-Stats-Extended now takes the actual width of each character into account:
|
||||
|
||||
<img width="400" height="150" alt="new-server-side-calculation" src="https://github.com/user-attachments/assets/277b92df-b2a9-48be-bd2a-d28c1d6764c5" />
|
||||
+12
-12
@@ -4,25 +4,25 @@
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@10.33.2+sha512.a90faf6feeab71ad6c6e57f94e0fe1a12f5dcc22cd754db40ae9593eb6a3e0b6b12e3540218bb37ae083404b1f2ce6db2a4121e979829b4aff94b99f49da1cf8",
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "2.0.5",
|
||||
"@eslint/compat": "2.1.0",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@playwright/test": "1.59.1",
|
||||
"@types/node": "24.12.2",
|
||||
"@playwright/test": "1.60.0",
|
||||
"@types/node": "24.13.1",
|
||||
"@vitest/coverage-v8": "catalog:default",
|
||||
"eslint": "10.2.1",
|
||||
"eslint-import-resolver-typescript": "4.4.4",
|
||||
"eslint": "10.4.1",
|
||||
"eslint-import-resolver-typescript": "4.4.5",
|
||||
"eslint-plugin-import-x": "4.16.2",
|
||||
"eslint-plugin-jsdoc": "62.9.0",
|
||||
"eslint-plugin-jsdoc": "63.0.2",
|
||||
"eslint-plugin-react": "7.37.5",
|
||||
"eslint-plugin-react-hooks": "7.1.1",
|
||||
"globals": "17.5.0",
|
||||
"globals": "17.6.0",
|
||||
"husky": "9.1.7",
|
||||
"knip": "6.7.0",
|
||||
"lint-staged": "16.4.0",
|
||||
"prettier": "3.8.3",
|
||||
"turbo": "2.9.6",
|
||||
"knip": "6.16.1",
|
||||
"lint-staged": "17.0.7",
|
||||
"prettier": "3.8.4",
|
||||
"turbo": "2.9.17",
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "8.59.0",
|
||||
"typescript-eslint": "8.61.0",
|
||||
"vitest": "catalog:default"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Core library powering [GitHub Stats Extended](https://github.com/stats-organization/github-stats-extended) and [GitHub Readme Stats Action](https://github.com/stats-organization/github-readme-stats-action).
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@stats-organization/github-readme-stats-core",
|
||||
"version": "2.0.0",
|
||||
"version": "2.1.2",
|
||||
"type": "module",
|
||||
"homepage": "https://github-stats-extended.vercel.app/frontend",
|
||||
"bugs": {
|
||||
@@ -60,13 +60,15 @@
|
||||
},
|
||||
"files": [
|
||||
"build",
|
||||
"src"
|
||||
"src",
|
||||
"README.md"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.build.json",
|
||||
"prepack": "pnpm run build",
|
||||
"test": "vitest",
|
||||
"test:update:snapshot": "vitest -u",
|
||||
"test:e2e": "vitest --config vitest.config.e2e.ts",
|
||||
@@ -81,14 +83,13 @@
|
||||
"@testing-library/jest-dom": "6.9.1",
|
||||
"@uppercod/css-to-object": "1.1.1",
|
||||
"axios-mock-adapter": "2.1.0",
|
||||
"js-yaml": "4.1.1",
|
||||
"jsdom": "29.0.2",
|
||||
"js-yaml": "4.2.0",
|
||||
"jsdom": "catalog:default",
|
||||
"vitest": "catalog:default"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.15.2",
|
||||
"axios": "catalog:default",
|
||||
"emoji-name-map": "^2.0.3",
|
||||
"github-username-regex": "^1.0.0",
|
||||
"word-wrap": "^1.2.5"
|
||||
"github-username-regex": "^1.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ export default async (
|
||||
border_radius,
|
||||
border_color,
|
||||
show_owner,
|
||||
browser_rendering,
|
||||
hide_border,
|
||||
},
|
||||
pat = null,
|
||||
@@ -59,6 +60,7 @@ export default async (
|
||||
border_color,
|
||||
locale: locale ? locale.toLowerCase() : null,
|
||||
show_owner: parseBoolean(show_owner),
|
||||
browser_rendering: parseBoolean(browser_rendering),
|
||||
hide_border: parseBoolean(hide_border),
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -23,6 +23,7 @@ export default async (
|
||||
card_width,
|
||||
theme,
|
||||
show_owner,
|
||||
browser_rendering,
|
||||
show,
|
||||
show_icons,
|
||||
number_format,
|
||||
@@ -99,6 +100,7 @@ export default async (
|
||||
border_color,
|
||||
card_width_input: parseInt(card_width, 10),
|
||||
show_owner: parseBoolean(show_owner),
|
||||
browser_rendering: parseBoolean(browser_rendering),
|
||||
show: showStats,
|
||||
show_icons: parseBoolean(show_icons),
|
||||
number_format,
|
||||
|
||||
@@ -8,16 +8,23 @@ import { icons } from "../common/icons.js";
|
||||
import languageColors from "../common/languageColors.json" with { type: "json" };
|
||||
import { parseEmojis } from "../common/ops.js";
|
||||
import {
|
||||
countWrappedLines,
|
||||
createLanguageNode,
|
||||
flexLayout,
|
||||
iconWithLabel,
|
||||
measureText,
|
||||
wrappedTextNode,
|
||||
wrappedTextStyles,
|
||||
} from "../common/render.js";
|
||||
|
||||
const ICON_SIZE = 16;
|
||||
const CARD_DEFAULT_WIDTH = 400;
|
||||
const X_OFFSET = 25;
|
||||
const HEADER_MAX_LENGTH = 35;
|
||||
const DESCRIPTION_BOX_WIDTH = CARD_DEFAULT_WIDTH - 2 * X_OFFSET;
|
||||
const DESCRIPTION_FONT_SIZE = 13;
|
||||
const DESCRIPTION_LINE_HEIGHT_PX = 16;
|
||||
const DESCRIPTION_MAX_LINES = 10;
|
||||
|
||||
/**
|
||||
* @typedef {import('./types').GistCardOptions} GistCardOptions Gist card options.
|
||||
@@ -43,6 +50,7 @@ const renderGistCard = (gistData, options = {}) => {
|
||||
border_radius,
|
||||
border_color,
|
||||
show_owner = false,
|
||||
browser_rendering = false,
|
||||
hide_border = false,
|
||||
} = options;
|
||||
|
||||
@@ -57,16 +65,50 @@ const renderGistCard = (gistData, options = {}) => {
|
||||
theme,
|
||||
});
|
||||
|
||||
const lineWidth = 59;
|
||||
const linesLimit = 10;
|
||||
const desc = parseEmojis(description || "No description provided");
|
||||
const multiLineDescription = wrapTextMultiline(desc, lineWidth, linesLimit);
|
||||
const descriptionLines = multiLineDescription.length;
|
||||
const descriptionSvg = multiLineDescription
|
||||
.map(
|
||||
(line) => `<tspan dy="1.2em" x="${X_OFFSET}">${encodeHTML(line)}</tspan>`,
|
||||
)
|
||||
.join("");
|
||||
|
||||
let descriptionLines, descriptionSvg;
|
||||
if (browser_rendering) {
|
||||
// The browser performs the actual text wrapping inside the foreignObject;
|
||||
// we only estimate the line count server-side so the SVG can reserve enough
|
||||
// height. The estimate uses measureText for font-aware widths instead of a
|
||||
// fixed character count.
|
||||
descriptionLines = countWrappedLines(
|
||||
desc,
|
||||
DESCRIPTION_FONT_SIZE,
|
||||
DESCRIPTION_BOX_WIDTH,
|
||||
DESCRIPTION_MAX_LINES,
|
||||
);
|
||||
|
||||
descriptionSvg = wrappedTextNode({
|
||||
text: desc,
|
||||
x: X_OFFSET,
|
||||
y: -3,
|
||||
width: DESCRIPTION_BOX_WIDTH,
|
||||
height: descriptionLines * DESCRIPTION_LINE_HEIGHT_PX + 10, // 10px extra for "descenders" like g, j, q, p, y
|
||||
lineCount: descriptionLines,
|
||||
className: "description",
|
||||
testId: "description-text",
|
||||
});
|
||||
} else {
|
||||
const linesLimit = 10;
|
||||
const multiLineDescription = wrapTextMultiline(
|
||||
desc,
|
||||
DESCRIPTION_BOX_WIDTH,
|
||||
DESCRIPTION_FONT_SIZE,
|
||||
linesLimit,
|
||||
);
|
||||
descriptionLines = multiLineDescription.length;
|
||||
descriptionSvg = multiLineDescription
|
||||
.map(
|
||||
(line) =>
|
||||
`<tspan dy="1.2em" x="${X_OFFSET}">${encodeHTML(line)}</tspan>`,
|
||||
)
|
||||
.join("");
|
||||
descriptionSvg = `<text class="description" x="${X_OFFSET}" y="-5">
|
||||
${descriptionSvg}
|
||||
</text>`;
|
||||
}
|
||||
|
||||
const lineHeight = descriptionLines > 3 ? 12 : 10;
|
||||
const height =
|
||||
@@ -124,16 +166,17 @@ const renderGistCard = (gistData, options = {}) => {
|
||||
});
|
||||
|
||||
card.setCSS(`
|
||||
.description { font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} }
|
||||
.description {
|
||||
font: 400 ${DESCRIPTION_FONT_SIZE}px 'Segoe UI', Ubuntu, Sans-Serif;fill: ${textColor};
|
||||
${browser_rendering ? wrappedTextStyles(textColor) : ""}
|
||||
}
|
||||
.gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} }
|
||||
.icon { fill: ${iconColor} }
|
||||
`);
|
||||
card.setHideBorder(hide_border);
|
||||
|
||||
return card.render(`
|
||||
<text class="description" x="${X_OFFSET}" y="-5">
|
||||
${descriptionSvg}
|
||||
</text>
|
||||
${descriptionSvg}
|
||||
|
||||
<g transform="translate(30, ${height - 75})">
|
||||
${starAndForkCount}
|
||||
|
||||
@@ -8,19 +8,23 @@ import { encodeHTML } from "../common/html.js";
|
||||
import { icons } from "../common/icons.js";
|
||||
import { buildSearchFilter, clampValue, parseEmojis } from "../common/ops.js";
|
||||
import {
|
||||
countWrappedLines,
|
||||
createLanguageNode,
|
||||
flexLayout,
|
||||
iconWithLabel,
|
||||
measureText,
|
||||
wrappedTextNode,
|
||||
wrappedTextStyles,
|
||||
} from "../common/render.js";
|
||||
import { repoCardLocales } from "../translations.js";
|
||||
|
||||
import { createTextNode } from "./stats.js";
|
||||
|
||||
const ICON_SIZE = 16;
|
||||
const DESCRIPTION_LINE_WIDTH = 59;
|
||||
const CARD_DEFAULT_WIDTH = 400;
|
||||
const X_OFFSET = 25;
|
||||
const DESCRIPTION_FONT_SIZE = 13;
|
||||
const DESCRIPTION_LINE_HEIGHT_PX = 16;
|
||||
const DESCRIPTION_MAX_LINES = 3;
|
||||
|
||||
/**
|
||||
@@ -81,6 +85,7 @@ const renderRepoCard = (repo, options = {}) => {
|
||||
bg_color,
|
||||
card_width_input,
|
||||
show_owner = false,
|
||||
browser_rendering = false,
|
||||
show = [],
|
||||
show_icons = true,
|
||||
number_format = "short",
|
||||
@@ -177,27 +182,56 @@ const renderRepoCard = (repo, options = {}) => {
|
||||
const header = show_owner ? nameWithOwner : name;
|
||||
const langName = (primaryLanguage && primaryLanguage.name) || "Unspecified";
|
||||
const langColor = (primaryLanguage && primaryLanguage.color) || "#333";
|
||||
const descriptionMaxLines = description_lines_count
|
||||
? clampValue(description_lines_count, 1, DESCRIPTION_MAX_LINES)
|
||||
: DESCRIPTION_MAX_LINES;
|
||||
|
||||
const desc = parseEmojis(description || "No description provided");
|
||||
const multiLineDescription = wrapTextMultiline(
|
||||
desc,
|
||||
Math.round(
|
||||
(card_width - CARD_DEFAULT_WIDTH) / 5.93 + DESCRIPTION_LINE_WIDTH,
|
||||
),
|
||||
descriptionMaxLines,
|
||||
);
|
||||
const descriptionLinesCount = description_lines_count
|
||||
? clampValue(description_lines_count, 1, DESCRIPTION_MAX_LINES)
|
||||
: multiLineDescription.length;
|
||||
const descriptionBoxWidth = card_width - 2 * X_OFFSET;
|
||||
|
||||
const descriptionSvg = multiLineDescription
|
||||
.map(
|
||||
(line) => `<tspan dy="1.2em" x="${X_OFFSET}">${encodeHTML(line)}</tspan>`,
|
||||
)
|
||||
.join("");
|
||||
let descriptionLinesCount, descriptionSvg;
|
||||
if (browser_rendering) {
|
||||
// The browser performs the actual text wrapping inside the foreignObject;
|
||||
// we only estimate the line count server-side so the SVG can reserve enough
|
||||
// height. The estimate uses measureText for font-aware widths instead of a
|
||||
// fixed character count.
|
||||
descriptionLinesCount = description_lines_count
|
||||
? clampValue(description_lines_count, 1, DESCRIPTION_MAX_LINES)
|
||||
: countWrappedLines(
|
||||
desc,
|
||||
DESCRIPTION_FONT_SIZE,
|
||||
descriptionBoxWidth,
|
||||
DESCRIPTION_MAX_LINES,
|
||||
);
|
||||
descriptionSvg = wrappedTextNode({
|
||||
text: desc,
|
||||
x: X_OFFSET,
|
||||
y: -3,
|
||||
width: descriptionBoxWidth,
|
||||
height: descriptionLinesCount * DESCRIPTION_LINE_HEIGHT_PX + 10, // 10px extra for "descenders" like g, j, q, p, y
|
||||
lineCount: descriptionLinesCount,
|
||||
className: "description",
|
||||
testId: "description-text",
|
||||
});
|
||||
} else {
|
||||
const descriptionMaxLines = description_lines_count
|
||||
? clampValue(description_lines_count, 1, DESCRIPTION_MAX_LINES)
|
||||
: DESCRIPTION_MAX_LINES;
|
||||
const multiLineDescription = wrapTextMultiline(
|
||||
desc,
|
||||
descriptionBoxWidth,
|
||||
DESCRIPTION_FONT_SIZE,
|
||||
descriptionMaxLines,
|
||||
);
|
||||
descriptionLinesCount = description_lines_count
|
||||
? clampValue(description_lines_count, 1, DESCRIPTION_MAX_LINES)
|
||||
: multiLineDescription.length;
|
||||
descriptionSvg = multiLineDescription
|
||||
.map(
|
||||
(line) =>
|
||||
`<tspan dy="1.2em" x="${X_OFFSET}">${encodeHTML(line)}</tspan>`,
|
||||
)
|
||||
.join("");
|
||||
descriptionSvg = `<text class="description" x="${X_OFFSET}" y="-5">
|
||||
${descriptionSvg}
|
||||
</text>`;
|
||||
}
|
||||
|
||||
const extraHeight = Object.keys(STATS).length
|
||||
? -7 + (Math.ceil(statItems.length / 2) + 1) * extraLHeight
|
||||
@@ -279,11 +313,14 @@ const renderRepoCard = (repo, options = {}) => {
|
||||
card.setHideBorder(hide_border);
|
||||
card.setHideTitle(false);
|
||||
card.setCSS(`
|
||||
.description { font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${colors.textColor} }
|
||||
.description {
|
||||
font: 400 ${DESCRIPTION_FONT_SIZE}px 'Segoe UI', Ubuntu, Sans-Serif;fill: ${colors.textColor};
|
||||
${browser_rendering ? wrappedTextStyles(colors.textColor) : ""}
|
||||
}
|
||||
.gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${colors.textColor} }
|
||||
.badge { font: 600 11px 'Segoe UI', Ubuntu, Sans-Serif; }
|
||||
.badge rect { opacity: 0.2 }
|
||||
|
||||
|
||||
.stat { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${colors.textColor} }
|
||||
.stagger {
|
||||
opacity: 0;
|
||||
@@ -316,9 +353,7 @@ const renderRepoCard = (repo, options = {}) => {
|
||||
: ""
|
||||
}
|
||||
|
||||
<text class="description" x="${X_OFFSET}" y="-5">
|
||||
${descriptionSvg}
|
||||
</text>
|
||||
${descriptionSvg}
|
||||
|
||||
<g transform="translate(30, ${height - 75 - extraHeight})">
|
||||
${starAndForkCount}
|
||||
|
||||
Vendored
+2
@@ -34,6 +34,7 @@ export type StatCardOptions = CommonOptions & {
|
||||
|
||||
export type RepoCardOptions = CommonOptions & {
|
||||
show_owner: boolean;
|
||||
browser_rendering: boolean;
|
||||
description_lines_count: number;
|
||||
card_width_input;
|
||||
show: Array<string>;
|
||||
@@ -73,4 +74,5 @@ export type WakaTimeOptions = CommonOptions & {
|
||||
|
||||
export type GistCardOptions = CommonOptions & {
|
||||
show_owner: boolean;
|
||||
browser_rendering: boolean;
|
||||
};
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// @ts-check
|
||||
|
||||
import wrap from "word-wrap";
|
||||
|
||||
import { encodeHTML } from "./html.js";
|
||||
import { splitWrappedText } from "./render.js";
|
||||
|
||||
/**
|
||||
* Retrieves num with suffix k(thousands) precise to given decimal places.
|
||||
@@ -57,26 +56,16 @@ const formatBytes = (bytes) => {
|
||||
* Split text over multiple lines based on the card width.
|
||||
*
|
||||
* @param {string} text Text to split.
|
||||
* @param {number} width Line width in number of characters.
|
||||
* @param {number} width Available wrap width in px.
|
||||
* @param {number} fontSize Font size in px.
|
||||
* @param {number} maxLines Maximum number of lines.
|
||||
* @returns {string[]} Array of lines.
|
||||
*/
|
||||
const wrapTextMultiline = (text, width = 59, maxLines = 3) => {
|
||||
const fullWidthComma = ",";
|
||||
const encoded = encodeHTML(text);
|
||||
const isChinese = encoded.includes(fullWidthComma);
|
||||
|
||||
let wrapped;
|
||||
|
||||
if (isChinese) {
|
||||
wrapped = encoded.split(fullWidthComma); // Chinese full punctuation
|
||||
} else {
|
||||
wrapped = wrap(encoded, {
|
||||
width,
|
||||
}).split("\n"); // Split wrapped lines to get an array of lines
|
||||
}
|
||||
|
||||
const lines = wrapped.map((line) => line.trim()).slice(0, maxLines); // Only consider maxLines lines
|
||||
const wrapTextMultiline = (text, width, fontSize, maxLines = 3) => {
|
||||
const wrapped = splitWrappedText(text, fontSize, width);
|
||||
const lines = wrapped
|
||||
.map((line) => encodeHTML(line.trim()))
|
||||
.slice(0, maxLines); // Only consider maxLines lines
|
||||
|
||||
// Add "..." to the last line if the text exceeds maxLines
|
||||
if (wrapped.length > maxLines) {
|
||||
|
||||
@@ -254,6 +254,7 @@
|
||||
"Hy": "#7790B2",
|
||||
"IDL": "#a3522f",
|
||||
"IGOR Pro": "#0000cc",
|
||||
"IL Assembly": "#512BD4",
|
||||
"INI": "#d1dbe0",
|
||||
"ISPC": "#2D68B1",
|
||||
"Idris": "#b30000",
|
||||
@@ -467,6 +468,7 @@
|
||||
"Quake": "#882233",
|
||||
"QuakeC": "#975777",
|
||||
"QuickBASIC": "#008080",
|
||||
"Quint": "#9d6ce5",
|
||||
"R": "#198CE7",
|
||||
"RAML": "#77d9fb",
|
||||
"RAScript": "#2C97FA",
|
||||
|
||||
@@ -86,6 +86,66 @@ const createProgressNode = ({
|
||||
`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders multi-line text via a `foreignObject` so the browser performs
|
||||
* native, font-aware wrapping. Content overflowing `lineCount` lines is
|
||||
* clipped (with an ellipsis on the last visible line) by CSS line-clamp.
|
||||
*
|
||||
* @param {object} props Function properties.
|
||||
* @param {string} props.text Text to render (will be HTML-encoded).
|
||||
* @param {number} props.x X position of the foreignObject.
|
||||
* @param {number} props.y Y position of the foreignObject.
|
||||
* @param {number} props.width Width of the wrap box.
|
||||
* @param {number} props.height Height of the wrap box.
|
||||
* @param {number} props.lineCount Maximum number of lines to display.
|
||||
* @param {string} props.className CSS class applied to the inner element.
|
||||
* @param {string=} props.testId Optional test id for the inner element.
|
||||
* @returns {string} foreignObject SVG node.
|
||||
*/
|
||||
const wrappedTextNode = ({
|
||||
text,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
lineCount,
|
||||
className,
|
||||
testId,
|
||||
}) => {
|
||||
const testIdAttr = testId ? ` data-testid="${testId}"` : "";
|
||||
return `
|
||||
<foreignObject x="${x}" y="${y}" width="${width}" height="${height}">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" class="${className}" style="--lines: ${lineCount};"${testIdAttr}>${encodeHTML(
|
||||
text,
|
||||
)}</div>
|
||||
</foreignObject>
|
||||
`;
|
||||
};
|
||||
|
||||
/**
|
||||
* CSS rules used to render multi-line text inside a `foreignObject`. Apply this
|
||||
* to a CSS class (e.g. `.description`) shared with `wrappedTextNode` so the
|
||||
* browser handles wrapping and the line count is taken from the `--lines`
|
||||
* custom property set on the element.
|
||||
*
|
||||
* @param {string} color Text color (CSS `color` property).
|
||||
* @returns {string} CSS rules block (without the surrounding selector).
|
||||
*/
|
||||
const wrappedTextStyles = (color) => `
|
||||
color: ${color};
|
||||
margin: 0;
|
||||
line-height: 1.2;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: var(--lines);
|
||||
line-clamp: var(--lines);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding-bottom: 0.15em;
|
||||
`;
|
||||
|
||||
/**
|
||||
* Creates an icon with label to display repository/gist stats like forks, stars, etc.
|
||||
*
|
||||
@@ -187,7 +247,7 @@ const renderError = ({
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve text length.
|
||||
* Retrieve text length based on Segoe UI font.
|
||||
*
|
||||
* @see https://stackoverflow.com/a/48172630/10629172
|
||||
* @param {string} str String to measure.
|
||||
@@ -197,37 +257,177 @@ const renderError = ({
|
||||
const measureText = (str, fontSize = 10) => {
|
||||
// prettier-ignore
|
||||
const widths = [
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0.2796875, 0.2765625,
|
||||
0.3546875, 0.5546875, 0.5546875, 0.8890625, 0.665625, 0.190625,
|
||||
0.3328125, 0.3328125, 0.3890625, 0.5828125, 0.2765625, 0.3328125,
|
||||
0.2765625, 0.3015625, 0.5546875, 0.5546875, 0.5546875, 0.5546875,
|
||||
0.5546875, 0.5546875, 0.5546875, 0.5546875, 0.5546875, 0.5546875,
|
||||
0.2765625, 0.2765625, 0.584375, 0.5828125, 0.584375, 0.5546875,
|
||||
1.0140625, 0.665625, 0.665625, 0.721875, 0.721875, 0.665625,
|
||||
0.609375, 0.7765625, 0.721875, 0.2765625, 0.5, 0.665625,
|
||||
0.5546875, 0.8328125, 0.721875, 0.7765625, 0.665625, 0.7765625,
|
||||
0.721875, 0.665625, 0.609375, 0.721875, 0.665625, 0.94375,
|
||||
0.665625, 0.665625, 0.609375, 0.2765625, 0.3546875, 0.2765625,
|
||||
0.4765625, 0.5546875, 0.3328125, 0.5546875, 0.5546875, 0.5,
|
||||
0.5546875, 0.5546875, 0.2765625, 0.5546875, 0.5546875, 0.221875,
|
||||
0.240625, 0.5, 0.221875, 0.8328125, 0.5546875, 0.5546875,
|
||||
0.5546875, 0.5546875, 0.3328125, 0.5, 0.2765625, 0.5546875,
|
||||
0.5, 0.721875, 0.5, 0.5, 0.5, 0.3546875, 0.259375, 0.353125, 0.5890625,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0.2733333110809326, 0.28499999046325686,
|
||||
0.39166667461395266, 0.5900000095367431, 0.5383333206176758,
|
||||
0.8183333396911621, 0.8, 0.22999999523162842, 0.30166666507720946,
|
||||
0.30166666507720946, 0.41666665077209475, 0.6833333492279052,
|
||||
0.21666667461395264, 0.4, 0.21666667461395264, 0.3900000095367432,
|
||||
0.5383333206176758, 0.5383333206176758, 0.5383333206176758,
|
||||
0.5383333206176758, 0.5383333206176758, 0.5383333206176758,
|
||||
0.5383333206176758, 0.5383333206176758, 0.5383333206176758,
|
||||
0.5383333206176758, 0.21666667461395264, 0.21666667461395264,
|
||||
0.6833333492279052, 0.6833333492279052, 0.6833333492279052,
|
||||
0.4483333110809326, 0.9550000190734863, 0.6449999809265137,
|
||||
0.5733333110809327, 0.6183333396911621, 0.7016666889190674,
|
||||
0.5066666603088379, 0.48833332061767576, 0.6866666793823242,
|
||||
0.7099999904632568, 0.26666667461395266, 0.35666666030883787,
|
||||
0.5800000190734863, 0.4699999809265137, 0.8983333587646485,
|
||||
0.7483333110809326, 0.753333330154419, 0.5599999904632569,
|
||||
0.753333330154419, 0.5983333110809326, 0.5316666603088379,
|
||||
0.5233333110809326, 0.6866666793823242, 0.621666669845581,
|
||||
0.9333333015441895, 0.5900000095367431, 0.553333330154419,
|
||||
0.5699999809265137, 0.30166666507720946, 0.37833333015441895,
|
||||
0.30166666507720946, 0.6833333492279052, 0.41500000953674315,
|
||||
0.26833333969116213, 0.5083333492279053, 0.5883333206176757,
|
||||
0.4616666793823242, 0.5883333206176757, 0.5233333110809326,
|
||||
0.3133333444595337, 0.5883333206176757, 0.5666666507720948,
|
||||
0.24166667461395264, 0.24166667461395264, 0.49666666984558105,
|
||||
0.24166667461395264, 0.8616666793823242, 0.5666666507720948,
|
||||
0.5866666793823242, 0.5883333206176757, 0.5883333206176757,
|
||||
0.3483333349227905, 0.425, 0.33833334445953367, 0.5666666507720948,
|
||||
0.4783333301544189, 0.7233333110809326, 0.45833334922790525,
|
||||
0.4833333492279053, 0.45166668891906736, 0.30166666507720946,
|
||||
0.24000000953674316, 0.30166666507720946, 0.6833333492279052,
|
||||
];
|
||||
|
||||
const avg = 0.5279276315789471;
|
||||
const avg = 0.5131403493881227;
|
||||
// CJK character range: U+3000–U+9FFF (CJK Symbols/Punctuation, Hiragana,
|
||||
// Katakana, CJK Unified Ideographs incl. Extension A) plus U+FF00–U+FFEF
|
||||
// (Halfwidth/Fullwidth Forms — fullwidth ASCII, fullwidth punctuation).
|
||||
const cjkRange = /[\u3000-\u9FFF\uFF00-\uFFEF]/;
|
||||
|
||||
return (
|
||||
str
|
||||
.split("")
|
||||
.map((c) =>
|
||||
c.charCodeAt(0) < widths.length ? widths[c.charCodeAt(0)] : avg,
|
||||
)
|
||||
.map((c) => {
|
||||
if (cjkRange.test(c) || c === "\u3000") {
|
||||
// CJK glyphs and U+3000 IDEOGRAPHIC SPACE are full-width by default;
|
||||
return 1;
|
||||
}
|
||||
if (c.charCodeAt(0) < widths.length) {
|
||||
return widths[c.charCodeAt(0)];
|
||||
} else {
|
||||
return avg;
|
||||
}
|
||||
})
|
||||
.reduce((cur, acc) => acc + cur) * fontSize
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Split text into the lines it would wrap to when laid out greedily at the
|
||||
* given font size inside a box of width `maxWidth`. Uses `measureText` so the
|
||||
* estimate reflects actual font metrics rather than a fixed character count.
|
||||
* The browser still does the real wrap inside the foreignObject; this is only
|
||||
* used to size the SVG.
|
||||
*
|
||||
* @param {string} text Text to split.
|
||||
* @param {number} fontSize Font size in px (matches `measureText`).
|
||||
* @param {number} maxWidth Available wrap width in px.
|
||||
* @returns {string[]} Estimated wrapped lines.
|
||||
*/
|
||||
const splitWrappedText = (text, fontSize, maxWidth) => {
|
||||
if (!text) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Tokenize the text into atoms representing line-break opportunities:
|
||||
// - ASCII whitespace runs (collapsed/dropped at line edges per CSS rules);
|
||||
// - non-ASCII whitespaces
|
||||
// - a single CJK codepoint (browsers break between any two CJK chars,
|
||||
// punctuation or not, so each one is its own atom and ~1 em wide);
|
||||
// - a run of non-whitespace non-CJK characters (a "word" that can only
|
||||
// break at its boundaries, like Latin script).
|
||||
// Korean Hangul (U+AC00–U+D7AF) is intentionally NOT in the CJK range
|
||||
// because Korean wraps at word boundaries by default in HTML.
|
||||
// ASCII whitespace is collapsed to a single space per CSS `white-space: normal;`
|
||||
text = text.replace(/[\t\n\r ]+/g, " ");
|
||||
const tokens = text.match(
|
||||
/\s|[\u3000-\u9FFF\uFF00-\uFFEF]|[^\s\u3000-\u9FFF\uFF00-\uFFEF]+/g,
|
||||
);
|
||||
if (!tokens) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const takeFittingSegment = (token, availableWidth) => {
|
||||
const characters = token.split("");
|
||||
let segment = "";
|
||||
let width = 0;
|
||||
|
||||
for (const character of characters) {
|
||||
const characterWidth = measureText(character, fontSize);
|
||||
if (segment && width + characterWidth > availableWidth) {
|
||||
break;
|
||||
}
|
||||
segment += character;
|
||||
width += characterWidth;
|
||||
}
|
||||
|
||||
return {
|
||||
segment,
|
||||
width,
|
||||
};
|
||||
};
|
||||
|
||||
const lines = [""];
|
||||
let currentWidth = 0;
|
||||
|
||||
for (const token of tokens) {
|
||||
if (token === " ") {
|
||||
// Whitespace at the start of a line is dropped by browsers.
|
||||
if (currentWidth === 0) {
|
||||
continue;
|
||||
}
|
||||
lines[lines.length - 1] += token;
|
||||
currentWidth += measureText(token, fontSize);
|
||||
continue;
|
||||
}
|
||||
|
||||
let remaining = token;
|
||||
|
||||
while (remaining) {
|
||||
const w = measureText(remaining, fontSize);
|
||||
if (currentWidth + w <= maxWidth) {
|
||||
lines[lines.length - 1] += remaining;
|
||||
currentWidth += w;
|
||||
break;
|
||||
}
|
||||
|
||||
if (currentWidth > 0) {
|
||||
lines.push("");
|
||||
currentWidth = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
// An atom wider than the box wraps mid-glyph (overflow-wrap: anywhere).
|
||||
const { segment, width } = takeFittingSegment(remaining, maxWidth);
|
||||
lines[lines.length - 1] += segment;
|
||||
currentWidth = width;
|
||||
remaining = remaining.slice(segment.length);
|
||||
}
|
||||
}
|
||||
|
||||
return lines.map((line) => line.replace(/[\t\n\r ]+$/, ""));
|
||||
};
|
||||
|
||||
/**
|
||||
* Estimate how many lines a string will wrap to when laid out greedily at the
|
||||
* given font size inside a box of width `maxWidth`, capped at `maxLines`.
|
||||
*
|
||||
* @param {string} text Text to estimate.
|
||||
* @param {number} fontSize Font size in px (matches `measureText`).
|
||||
* @param {number} maxWidth Available wrap width in px.
|
||||
* @param {number} maxLines Cap on the returned line count.
|
||||
* @returns {number} Estimated line count, at least 1, at most `maxLines`.
|
||||
*/
|
||||
const countWrappedLines = (text, fontSize, maxWidth, maxLines) => {
|
||||
return Math.min(
|
||||
Math.max(1, splitWrappedText(text, fontSize, maxWidth).length),
|
||||
maxLines,
|
||||
);
|
||||
};
|
||||
|
||||
export {
|
||||
renderError,
|
||||
createLanguageNode,
|
||||
@@ -235,4 +435,8 @@ export {
|
||||
iconWithLabel,
|
||||
flexLayout,
|
||||
measureText,
|
||||
splitWrappedText,
|
||||
countWrappedLines,
|
||||
wrappedTextNode,
|
||||
wrappedTextStyles,
|
||||
};
|
||||
|
||||
@@ -388,7 +388,7 @@ const fetchStats = async (
|
||||
}
|
||||
if (res.data.errors[0].message) {
|
||||
throw new CustomError(
|
||||
wrapTextMultiline(res.data.errors[0].message, 90, 1)[0],
|
||||
wrapTextMultiline(res.data.errors[0].message, 525, 12)[0],
|
||||
res.statusText,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ const fetchTopLanguages = async (
|
||||
}
|
||||
if (res.data.errors[0].message) {
|
||||
throw new CustomError(
|
||||
wrapTextMultiline(res.data.errors[0].message, 90, 1)[0],
|
||||
wrapTextMultiline(res.data.errors[0].message, 525, 12)[0],
|
||||
res.statusText,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -72,7 +72,12 @@ describe("Test fmt.js", () => {
|
||||
|
||||
it("wrapTextMultiline: should not wrap small texts", () => {
|
||||
{
|
||||
let multiLineText = wrapTextMultiline("Small text should not wrap");
|
||||
let multiLineText = wrapTextMultiline(
|
||||
"Small text should not wrap",
|
||||
130,
|
||||
11,
|
||||
3,
|
||||
);
|
||||
expect(multiLineText).toEqual(["Small text should not wrap"]);
|
||||
}
|
||||
});
|
||||
@@ -80,26 +85,31 @@ describe("Test fmt.js", () => {
|
||||
it("wrapTextMultiline: should wrap large texts", () => {
|
||||
let multiLineText = wrapTextMultiline(
|
||||
"Hello world long long long text",
|
||||
20,
|
||||
130,
|
||||
11,
|
||||
3,
|
||||
);
|
||||
expect(multiLineText).toEqual(["Hello world long", "long long text"]);
|
||||
expect(multiLineText).toEqual(["Hello world long long", "long text"]);
|
||||
});
|
||||
|
||||
it("wrapTextMultiline: should wrap large texts and limit max lines", () => {
|
||||
let multiLineText = wrapTextMultiline(
|
||||
"Hello world long long long text",
|
||||
10,
|
||||
53,
|
||||
11,
|
||||
2,
|
||||
);
|
||||
expect(multiLineText).toEqual(["Hello", "world long..."]);
|
||||
});
|
||||
|
||||
it("wrapTextMultiline: should wrap chinese by punctuation", () => {
|
||||
it("wrapTextMultiline: should handle chinese characters", () => {
|
||||
let multiLineText = wrapTextMultiline(
|
||||
"专门为刚开始刷题的同学准备的算法基地,没有最细只有更细,立志用动画将晦涩难懂的算法说的通俗易懂!",
|
||||
130,
|
||||
11,
|
||||
3,
|
||||
);
|
||||
expect(multiLineText.length).toEqual(3);
|
||||
expect(multiLineText[0].length).toEqual(18 * 8); // &#xxxxx; x 8
|
||||
expect(multiLineText[0].length).toEqual(11 * 8); // &#xxxxx; x 8
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,10 +3,80 @@
|
||||
import { queryByTestId } from "@testing-library/dom";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { renderError } from "../src/common/render.js";
|
||||
import {
|
||||
countWrappedLines,
|
||||
renderError,
|
||||
splitWrappedText,
|
||||
} from "../src/common/render.js";
|
||||
|
||||
describe("Test render.js", () => {
|
||||
it("should test renderError", () => {
|
||||
describe("Test splitWrappedText", () => {
|
||||
it("should return an empty array for empty text", () => {
|
||||
expect(splitWrappedText("", 10, 200)).toEqual([]);
|
||||
});
|
||||
|
||||
it("should split a two-word string across lines", () => {
|
||||
expect(splitWrappedText("hello world", 10, 25)).toEqual(["hello", "world"]);
|
||||
});
|
||||
|
||||
it("should split a word wider than maxWidth", () => {
|
||||
expect(splitWrappedText("aaaa", 10, 15)).toEqual(["aa", "aa"]);
|
||||
});
|
||||
|
||||
it("should handle mix of short and long words", () => {
|
||||
expect(splitWrappedText("short looooong", 10, 40)).toEqual([
|
||||
"short",
|
||||
"looooon",
|
||||
"g",
|
||||
]);
|
||||
});
|
||||
|
||||
it("should handle complex whitespace characters", () => {
|
||||
expect(splitWrappedText("One two three", 10, 25)).toEqual([
|
||||
"One",
|
||||
" ",
|
||||
" ",
|
||||
"two",
|
||||
"three",
|
||||
]);
|
||||
});
|
||||
|
||||
it("trailing spaces should not cause line breaks", () => {
|
||||
expect(splitWrappedText("hi hi ", 10, 12)).toEqual(["hi", "hi"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test countWrappedLines", () => {
|
||||
it("should return 1 for empty text", () => {
|
||||
expect(countWrappedLines("", 10, 200, 10)).toBe(1);
|
||||
});
|
||||
|
||||
it("should return 1 when all text fits on a single line", () => {
|
||||
expect(countWrappedLines("hi", 10, 200, 10)).toBe(1);
|
||||
});
|
||||
|
||||
it("should return 2 when a two-word string wraps", () => {
|
||||
expect(countWrappedLines("hello world", 10, 25, 10)).toBe(2);
|
||||
});
|
||||
|
||||
it("should split a word wider than maxWidth (overflow-wrap: anywhere)", () => {
|
||||
expect(countWrappedLines("aaaa", 10, 15, 10)).toBe(2);
|
||||
});
|
||||
|
||||
it("should cap the result at maxLines", () => {
|
||||
expect(countWrappedLines("word ".repeat(10), 10, 25, 3)).toBe(3);
|
||||
});
|
||||
|
||||
it("should handle complex whitespace characters", () => {
|
||||
expect(countWrappedLines("One two three", 10, 25, 10)).toBe(5);
|
||||
});
|
||||
|
||||
it("trailing spaces should not cause line breaks", () => {
|
||||
expect(countWrappedLines("hi hi ", 10, 12, 10)).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test renderError", () => {
|
||||
it("should contain error messages", () => {
|
||||
document.body.innerHTML = renderError({ message: "Something went wrong" });
|
||||
expect(
|
||||
queryByTestId(document.body, "message")?.children[0],
|
||||
|
||||
@@ -54,7 +54,7 @@ describe("test renderGistCard", () => {
|
||||
expect(header).toHaveTextContent("some-really-long-repo-name-for-test...");
|
||||
});
|
||||
|
||||
it("should trim description if description os too long", () => {
|
||||
it("should trim description if description is too long", () => {
|
||||
document.body.innerHTML = renderGistCard({
|
||||
...data,
|
||||
description:
|
||||
@@ -66,7 +66,27 @@ describe("test renderGistCard", () => {
|
||||
|
||||
expect(
|
||||
document.getElementsByClassName("description")[0].children[1].textContent,
|
||||
).toBe("English-language pangram—a sentence that contains all");
|
||||
).toBe("English-language pangram—a sentence that contains all of");
|
||||
});
|
||||
|
||||
it("should respect browser_rendering=true", () => {
|
||||
document.body.innerHTML = renderGistCard(
|
||||
{
|
||||
...data,
|
||||
description:
|
||||
"The quick brown fox jumps over the lazy dog is an English-language pangram—a sentence that contains all of the letters of the English alphabet",
|
||||
},
|
||||
{ browser_rendering: true },
|
||||
);
|
||||
// The full description stays in the DOM; the CSS line-clamp on the
|
||||
// foreignObject's inner div is what visually truncates the overflow.
|
||||
const description = document.getElementsByClassName("description")[0];
|
||||
expect(description).toHaveTextContent(
|
||||
"The quick brown fox jumps over the lazy dog is an English-language pangram—a sentence that contains all of the letters of the English alphabet",
|
||||
);
|
||||
expect(
|
||||
Number(description.style.getPropertyValue("--lines")),
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should not trim description if it is short", () => {
|
||||
|
||||
@@ -29,7 +29,8 @@ describe("Test renderRepoCard", () => {
|
||||
expect(header).toHaveTextContent("convoychat");
|
||||
expect(header).not.toHaveTextContent("anuraghazra");
|
||||
expect(document.getElementsByClassName("description")[0]).toHaveTextContent(
|
||||
"Help us take over the world! React + TS + GraphQL Chat App",
|
||||
// no space between "Chat" and "App" because there's a line break there
|
||||
"Help us take over the world! React + TS + GraphQL ChatApp",
|
||||
);
|
||||
expect(queryByTestId(document.body, "stargazers")).toHaveTextContent("38k");
|
||||
expect(queryByTestId(document.body, "forkcount")).toHaveTextContent("100");
|
||||
@@ -75,7 +76,7 @@ describe("Test renderRepoCard", () => {
|
||||
|
||||
expect(
|
||||
document.getElementsByClassName("description")[0].children[1].textContent,
|
||||
).toBe("English-language pangram—a sentence that contains all");
|
||||
).toBe("English-language pangram—a sentence that contains all of");
|
||||
|
||||
// Should not trim
|
||||
document.body.innerHTML = renderRepoCard({
|
||||
@@ -88,6 +89,36 @@ describe("Test renderRepoCard", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should respect browser_rendering=true", () => {
|
||||
document.body.innerHTML = renderRepoCard(
|
||||
{
|
||||
...data_repo.repository,
|
||||
description:
|
||||
"The quick brown fox jumps over the lazy dog is an English-language pangram—a sentence that contains all of the letters of the English alphabet",
|
||||
},
|
||||
{ browser_rendering: true },
|
||||
);
|
||||
|
||||
// Browser-side wrapping inside the foreignObject keeps the full text in
|
||||
// the DOM; the CSS line-clamp truncates whatever exceeds the line budget
|
||||
// at render time.
|
||||
const description = document.getElementsByClassName("description")[0];
|
||||
expect(description).toHaveTextContent(
|
||||
"The quick brown fox jumps over the lazy dog is an English-language pangram—a sentence that contains all of the letters of the English alphabet",
|
||||
);
|
||||
expect(description.style.getPropertyValue("--lines")).toBe("3");
|
||||
|
||||
// Short descriptions should leave the full text visible without clamping.
|
||||
document.body.innerHTML = renderRepoCard({
|
||||
...data_repo.repository,
|
||||
description: "Small text should not trim",
|
||||
});
|
||||
|
||||
expect(document.getElementsByClassName("description")[0]).toHaveTextContent(
|
||||
"Small text should not trim",
|
||||
);
|
||||
});
|
||||
|
||||
it("should render emojis", () => {
|
||||
document.body.innerHTML = renderRepoCard({
|
||||
...data_repo.repository,
|
||||
|
||||
@@ -350,7 +350,7 @@ describe("Test renderStatsCard", () => {
|
||||
|
||||
expect(
|
||||
document.body.getElementsByTagName("svg")[0].getAttribute("width"),
|
||||
).toBe("305.81250000000006");
|
||||
).toBe("299.9666657447815");
|
||||
});
|
||||
|
||||
it("should auto resize if hide_rank is true & custom_title is set", () => {
|
||||
|
||||
Generated
+860
-854
File diff suppressed because it is too large
Load Diff
+5
-3
@@ -3,9 +3,11 @@ packages:
|
||||
- packages/*
|
||||
|
||||
catalog:
|
||||
"@vitest/coverage-v8": 4.1.4
|
||||
vite: 8.0.10
|
||||
vitest: 4.1.4
|
||||
"@vitest/coverage-v8": 4.1.8
|
||||
axios: ^1.17.0
|
||||
jsdom: 29.1.1
|
||||
vite: 8.0.16
|
||||
vitest: 4.1.8
|
||||
|
||||
minimumReleaseAge: 10080
|
||||
|
||||
|
||||
Reference in New Issue
Block a user