From f8a98841d7ae8f618ba509c9c35f9379b4a62a94 Mon Sep 17 00:00:00 2001
From: Osama Mabkhot <99215291+O2sa@users.noreply.github.com>
Date: Mon, 27 Apr 2026 00:10:05 +0300
Subject: [PATCH 1/3] fix(compare-form): Type error: Cannot find name 'data'
---
app/page.tsx | 2 +-
components/compare-form.tsx | 5 +++--
2 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/app/page.tsx b/app/page.tsx
index 45e5e38..ebe792a 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -163,7 +163,7 @@ function HomePageInner() {
loading={loading}
reset={reset}
swapUsers={swapUsers}
- data={Boolean(data)}
+ hasData={Boolean(data)}
/>
{loading && skeleton}
diff --git a/components/compare-form.tsx b/components/compare-form.tsx
index 560bff0..f5aa07a 100644
--- a/components/compare-form.tsx
+++ b/components/compare-form.tsx
@@ -16,7 +16,7 @@ type CompareFormProps = {
username2: string;
setUsername1: (value: string) => void;
setUsername2: (value: string) => void;
- data?: boolean;
+ hasData?: boolean;
onSubmit: (u1: string, u2: string) => void;
loading?: boolean;
reset?: () => void;
@@ -29,6 +29,7 @@ export function CompareForm({
username2,
setUsername1,
setUsername2,
+ hasData,
onSubmit,
loading,
swapUsers,
@@ -44,7 +45,7 @@ export function CompareForm({
}, []);
const canSubmit = Boolean(username1.trim() && username2.trim() && !loading);
- const isEmpty = (!username1.trim() && !username2.trim()) && !data;
+ const isEmpty = (!username1.trim() && !username2.trim()) && !hasData;
const handleSwap = () => {
if (swapUsers) swapUsers();
From fdf76f7a0d2ca9b2ba7511ec85cd27ad2c3d8c67 Mon Sep 17 00:00:00 2001
From: Osama Mabkhot <99215291+O2sa@users.noreply.github.com>
Date: Mon, 11 May 2026 01:15:12 +0300
Subject: [PATCH 2/3] style: enhance the loading state
---
app/page.tsx | 130 ++++++++++++++++++++++++++++++++++++++++-----------
1 file changed, 104 insertions(+), 26 deletions(-)
diff --git a/app/page.tsx b/app/page.tsx
index 9f83c9a..d474712 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -11,6 +11,14 @@ import { AppHeader } from "@/components/app-header";
import { AppFooter } from "@/components/app-footer";
import { useTranslation } from "@/components/language-provider";
import { ApiResponse } from "@/types/api-response";
+import { cn } from "@/lib/utils";
+
+type ComparisonData = {
+ user1: UserResult;
+ user2: UserResult;
+};
+
+const EXIT_ANIMATION_MS = 240;
function HomePageInner() {
const { t } = useTranslation();
@@ -23,13 +31,12 @@ function HomePageInner() {
const [error, setError] = useState(null);
const [username1, setUsername1] = useState(initialUsername1);
const [username2, setUsername2] = useState(initialUsername2);
- const [data, setData] = useState<{
- user1: UserResult;
- user2: UserResult;
- } | null>(null);
+ const [data, setData] = useState(null);
+ const [displayData, setDisplayData] = useState(null);
// Track the URL pair we last fetched against so back/forward navigation
// can resync the form and results without re-fetching identical pairs.
const lastFetchedPairRef = useRef<[string, string] | null>(null);
+ const hideTimerRef = useRef(null);
const localizeErrorMessage = (message?: string) => {
switch (message) {
@@ -56,7 +63,6 @@ function HomePageInner() {
);
setLoading(true);
setError(null);
- setData(null);
try {
const params = new URLSearchParams();
@@ -71,19 +77,26 @@ function HomePageInner() {
}
if (body.users[0].finalScore > body.users[1].finalScore) {
- setData({
+ const nextData = {
user1: { ...body.users[0], isWinner: true },
user2: body.users[1],
- });
+ };
+ setData(nextData);
+ setDisplayData(nextData);
} else if (body.users[1].finalScore > body.users[0].finalScore) {
- setData({
+ const nextData = {
user1: body.users[0],
user2: { ...body.users[1], isWinner: true },
- });
+ };
+ setData(nextData);
+ setDisplayData(nextData);
} else {
- setData({ user1: body.users[0], user2: body.users[1] });
+ const nextData = { user1: body.users[0], user2: body.users[1] };
+ setData(nextData);
+ setDisplayData(nextData);
}
} catch (err: unknown) {
+ setData(null);
setError(localizeErrorMessage(err instanceof Error ? err.message : undefined));
} finally {
setLoading(false);
@@ -119,9 +132,44 @@ function HomePageInner() {
syncToUrl(params[0] ?? "", params[1] ?? "");
}, [searchParams]);
+ useEffect(() => {
+ if (hideTimerRef.current !== null) {
+ window.clearTimeout(hideTimerRef.current);
+ hideTimerRef.current = null;
+ }
+
+ if (data) {
+ setDisplayData(data);
+ return;
+ }
+
+ if (loading) {
+ return;
+ }
+
+ if (!displayData) {
+ return;
+ }
+
+ hideTimerRef.current = window.setTimeout(() => {
+ setDisplayData(null);
+ hideTimerRef.current = null;
+ }, EXIT_ANIMATION_MS);
+
+ return () => {
+ if (hideTimerRef.current !== null) {
+ window.clearTimeout(hideTimerRef.current);
+ hideTimerRef.current = null;
+ }
+ };
+ }, [data, displayData, loading]);
+
const skeleton = useMemo(() => , []);
+ const isRefreshing = loading && Boolean(displayData);
+ const isExiting = !loading && !data && Boolean(displayData);
const reset = () => {
+ setLoading(false);
setData(null);
setError(null);
setUsername1("");
@@ -161,22 +209,52 @@ function HomePageInner() {
hasData={Boolean(data)}
/>
- {loading && skeleton}
- {error && (
-
- {error}
-
- )}
-
- {data && }
-
- {!loading && !error && !data && (
-
-
-
{t("page.empty.title")}
-
{t("page.empty.description")}
-
- )}
+
+ {displayData ? (
+
+
+
+ ) : loading ? (
+
+ {skeleton}
+
+ ) : null}
+
+ {loading && displayData && (
+
+
+ {t("form.compare.ing")}
+
+
+ )}
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {!loading && !error && !displayData && (
+
+
+
{t("page.empty.title")}
+
{t("page.empty.description")}
+
+ )}
+
From 33a3a1bdc60f34e5eb6ce639376e19d0c05ad77d Mon Sep 17 00:00:00 2001
From: Osama Mabkhot <99215291+O2sa@users.noreply.github.com>
Date: Mon, 11 May 2026 01:16:25 +0300
Subject: [PATCH 3/3] feat: enahance the scoring alog
Co-authored-by: Copilot
---
.gitignore | 1 +
algorithm.md | 137 +++
app/api/compare/route.ts | 227 ++++-
components/app-footer.tsx | 2 +-
components/brand-logo.tsx | 3 +-
components/github-link.tsx | 46 +-
components/theme-toggle.tsx | 7 +-
github-issues.json | 8 +
lib/github.ts | 310 ++++++-
lib/i18n.ts | 22 +-
lib/score.ts | 845 ++++++++++++++++--
lib/scoring/languageScoring.ts | 118 +++
package.json | 7 +-
pnpm-lock.yaml | 677 +++++++++++++-
public/screenshots/screenshot.png | Bin 292375 -> 1673976 bytes
test/fixtures/github.ts | 175 ++++
test/helpers/score.ts | 220 +++++
.../calculateUserScore.contribution.test.ts | 333 +++++++
.../calculateUserScore.language.test.ts | 333 +++++++
test/scoring/calculateUserScore.pr.test.ts | 263 ++++++
test/scoring/calculateUserScore.repo.test.ts | 163 ++++
.../calculateUserScore.scenario.test.ts | 126 +++
test/scoring/languageScoring.helpers.test.ts | 85 ++
types/api-response.ts | 13 +-
types/github.ts | 59 +-
types/score.ts | 41 +-
types/user-result.ts | 56 ++
vitest.config.ts | 16 +
28 files changed, 4158 insertions(+), 135 deletions(-)
create mode 100644 algorithm.md
create mode 100644 github-issues.json
create mode 100644 lib/scoring/languageScoring.ts
create mode 100644 test/fixtures/github.ts
create mode 100644 test/helpers/score.ts
create mode 100644 test/scoring/calculateUserScore.contribution.test.ts
create mode 100644 test/scoring/calculateUserScore.language.test.ts
create mode 100644 test/scoring/calculateUserScore.pr.test.ts
create mode 100644 test/scoring/calculateUserScore.repo.test.ts
create mode 100644 test/scoring/calculateUserScore.scenario.test.ts
create mode 100644 test/scoring/languageScoring.helpers.test.ts
create mode 100644 vitest.config.ts
diff --git a/.gitignore b/.gitignore
index 9a5aced..93bdc13 100644
--- a/.gitignore
+++ b/.gitignore
@@ -137,3 +137,4 @@ dist
# Vite logs files
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
+server/
\ No newline at end of file
diff --git a/algorithm.md b/algorithm.md
new file mode 100644
index 0000000..93891cb
--- /dev/null
+++ b/algorithm.md
@@ -0,0 +1,137 @@
+# DevImpact
+
+
+
+
+### 🧠 Main
+```
+ compareUsers(user1, user2):
+
+ score1 = calculateUserScore(user1)
+ score2 = calculateUserScore(user2)
+
+ IF score1 > score2:
+ RETURN user1 as winner
+ ELSE:
+ RETURN user2 as winner
+```
+
+### 🧠 User Score
+```
+ calculateUserScore(user):
+
+ repos = getUserRepositories(user) // get the first 100 top repos
+ prs = getUserPullRequests(user) // get the latest 100 top merged PRs that's not merged to the user repo
+ contributions = getUserContributions(user)
+
+ repoScore = calculateRepoScore(repos)
+ prScore = calculatePRScore(prs, user)
+ contributionScore = calculateContributionScore(contributions)
+
+ finalScore =
+ repoScore * 0.4 +
+ prScore * 0.4 +
+ contributionScore * 0.2
+
+ RETURN finalScore
+```
+
+### 📦 Repository Score
+```
+ calculateRepoScore(repos):
+
+ scores = []
+
+ FOR EACH repo IN repos:
+ score =
+ log(repo.stars + 1) * 5 +
+ log(repo.forks + 1) * 3 +
+ log(repo.watchers + 1) * 2
+
+ ADD score TO scores
+
+ SORT scores DESC
+
+ total = 0
+
+ FOR i FROM 0 TO length(scores)-1:
+ IF i < 5:
+ weight = 1 // top repos matter most
+ ELSE:
+ weight = 0.1 // others have low impact
+
+ total += scores[i] * weight
+
+ RETURN total
+```
+
+
+### 🔥 Pull Request Score
+```
+ calculatePRScore(prs, username):
+
+ groupedPRs = groupPRsByRepository(prs)
+
+ totalScore = 0
+
+ FOR EACH repo IN groupedPRs:
+
+ repoPRs = groupedPRs[repo]
+
+ prScores = []
+
+ FOR EACH pr IN repoPRs:
+
+ // ❌ Ignore PRs to user's own repo
+ IF pr.repoOwner == username:
+ CONTINUE
+
+ // ❌ Ignore non-merged PRs
+ IF NOT pr.isMerged:
+ CONTINUE
+
+ // ✅ Base score (only for valid PRs)
+ base =
+ log(pr.repoStars + 1) * 2
+
+ // Optional: PR size factor (recommended)
+ sizeFactor = log(pr.additions + pr.deletions + 1)
+
+ score = base * sizeFactor
+
+ ADD score TO prScores
+
+ // If no valid PRs, skip repo
+ IF length(prScores) == 0:
+ CONTINUE
+
+ SORT prScores DESC
+
+ // diminishing returns inside same repo
+ repoTotal = 0
+
+ FOR i FROM 0 TO length(prScores)-1:
+ weight = 1 / (i + 1)
+ repoTotal += prScores[i] * weight
+
+ totalScore += repoTotal
+
+ RETURN totalScore
+```
+
+
+### 🌍 Contribution Score (Activity)
+```
+ calculateContributionScore(contributions):
+
+ commits = contributions.commits // public commits
+ prs = contributions.prs
+ issues = contributions.issues // public issues
+
+ score =
+ commits * 0.5 +
+ prs * 2 +
+ issues * 0.3
+
+ RETURN score
+```
\ No newline at end of file
diff --git a/app/api/compare/route.ts b/app/api/compare/route.ts
index c7991a0..72ce25d 100644
--- a/app/api/compare/route.ts
+++ b/app/api/compare/route.ts
@@ -1,50 +1,229 @@
import { NextResponse } from "next/server";
import { fetchGitHubUserData } from "../../../lib/github";
import { calculateUserScore } from "../../../lib/score";
+import { normalizeSelectedLanguages } from "@/lib/scoring/languageScoring";
export const runtime = "nodejs";
+type CompareRequestBody = {
+ username1?: string;
+ username2?: string;
+ selectedLanguages?: string[];
+};
+
+type ComparedUserResult = {
+ username: string;
+ name: string | null;
+ avatarUrl: string;
+ repoScore: number;
+ prScore: number;
+ contributionScore: number;
+ finalScore: number;
+ normalizedRepoScore: number;
+ normalizedPRScore: number;
+ normalizedContributionScore: number;
+ normalizedFinalScore: number;
+ topRepos: ReturnType["topRepos"];
+ topPullRequests: ReturnType["topPullRequests"];
+ topCommunityContributions: ReturnType<
+ typeof calculateUserScore
+ >["topCommunityContributions"];
+ languageScores: ReturnType["languageScores"];
+ signals: ReturnType["signals"];
+ explanations: ReturnType["explanations"];
+};
+
+function parseSelectedLanguagesFromSearchParams(
+ searchParams: URLSearchParams,
+): string[] {
+ const fromRepeated = searchParams.getAll("selectedLanguage");
+ const fromCsv = searchParams
+ .get("selectedLanguages")
+ ?.split(",")
+ .map((language) => language.trim())
+ .filter(Boolean);
+
+ return normalizeSelectedLanguages([...(fromRepeated ?? []), ...(fromCsv ?? [])]);
+}
+
+function calculateWinner(users: ComparedUserResult[]): {
+ winner?: {
+ username: string;
+ finalScoreDifference: number;
+ percentageDifference: number;
+ };
+ languageWinner?: {
+ username: string;
+ finalScoreDifference: number;
+ percentageDifference: number;
+ selectedLanguages: string[];
+ };
+} {
+ if (users.length !== 2) {
+ return {};
+ }
+
+ const [userA, userB] = users;
+ const overallWinner = userA.finalScore >= userB.finalScore ? userA : userB;
+ const overallLoser = overallWinner.username === userA.username ? userB : userA;
+ const overallDifference = Math.abs(userA.finalScore - userB.finalScore);
+ const overallPercentage =
+ overallLoser.finalScore > 0
+ ? (overallDifference / overallLoser.finalScore) * 100
+ : 0;
+
+ const result: {
+ winner: {
+ username: string;
+ finalScoreDifference: number;
+ percentageDifference: number;
+ };
+ languageWinner?: {
+ username: string;
+ finalScoreDifference: number;
+ percentageDifference: number;
+ selectedLanguages: string[];
+ };
+ } = {
+ winner: {
+ username: overallWinner.username,
+ finalScoreDifference: Math.round(overallDifference),
+ percentageDifference: Math.round(overallPercentage),
+ },
+ };
+
+ if (userA.languageScores && userB.languageScores) {
+ const languageWinner =
+ userA.languageScores.finalScore >= userB.languageScores.finalScore
+ ? userA
+ : userB;
+ const languageLoser = languageWinner.username === userA.username ? userB : userA;
+ const winnerLanguageScores = languageWinner.languageScores!;
+ const loserLanguageScores = languageLoser.languageScores!;
+ const languageDifference = Math.abs(
+ winnerLanguageScores.finalScore - loserLanguageScores.finalScore,
+ );
+ const languagePercentage =
+ loserLanguageScores.finalScore > 0
+ ? (languageDifference / loserLanguageScores.finalScore) * 100
+ : 0;
+
+ result.languageWinner = {
+ username: languageWinner.username,
+ finalScoreDifference: Math.round(languageDifference),
+ percentageDifference: Math.round(languagePercentage),
+ selectedLanguages: winnerLanguageScores.selectedLanguages,
+ };
+ }
+
+ return result;
+}
+
+async function compareUsers(
+ usernames: string[],
+ selectedLanguages: string[],
+): Promise {
+ return Promise.all(
+ usernames.map(async (username) => {
+ const data = await fetchGitHubUserData(username);
+ const score = calculateUserScore(
+ {
+ ...data,
+ selectedLanguages,
+ },
+ username,
+ );
+
+ return {
+ username,
+ name: data.name,
+ avatarUrl: data.avatarUrl,
+ repoScore: Math.round(score.repoScore),
+ prScore: Math.round(score.prScore),
+ contributionScore: Math.round(score.contributionScore),
+ finalScore: Math.round(score.finalScore),
+ normalizedRepoScore: Math.round(score.normalizedRepoScore),
+ normalizedPRScore: Math.round(score.normalizedPRScore),
+ normalizedContributionScore: Math.round(score.normalizedContributionScore),
+ normalizedFinalScore: Math.round(score.normalizedFinalScore),
+ topRepos: score.topRepos,
+ topPullRequests: score.topPullRequests,
+ topCommunityContributions: score.topCommunityContributions,
+ languageScores: score.languageScores,
+ signals: score.signals,
+ explanations: score.explanations,
+ };
+ }),
+ );
+}
+
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
- const usernames = searchParams.getAll("username");
+ const usernames = searchParams
+ .getAll("username")
+ .map((username) => username.trim())
+ .filter(Boolean);
if (usernames.length === 0) {
return NextResponse.json(
{ success: false, error: "provide at least one username param" },
- { status: 400 }
+ { status: 400 },
);
}
try {
- const results = await Promise.all(
- usernames.map(async (username) => {
- const data = await fetchGitHubUserData(username);
- const score = calculateUserScore(data, username);
-
- return {
- username,
- name: data.name,
- avatarUrl: data.avatarUrl,
- repoScore: Math.round(score.repoScore),
- prScore: Math.round(score.prScore),
- contributionScore: Math.round(score.contributionScore),
- finalScore: Math.round(score.finalScore),
- topRepos: score.topRepos,
- topPullRequests: score.topPullRequests,
- };
- })
- );
-
- return NextResponse.json({ success: true, users: results });
+ const selectedLanguages = parseSelectedLanguagesFromSearchParams(searchParams);
+ const users = await compareUsers(usernames, selectedLanguages);
+ const winnerData = calculateWinner(users);
+ return NextResponse.json({ success: true, users, ...winnerData });
} catch (error: unknown) {
console.error("GitHub score error:", error);
const message =
error instanceof Error && error.message === "User not found"
? "GitHub user not found"
: "Failed to calculate score";
+ return NextResponse.json({ success: false, error: message }, { status: 500 });
+ }
+}
+
+export async function POST(request: Request) {
+ let body: CompareRequestBody;
+
+ try {
+ body = (await request.json()) as CompareRequestBody;
+ } catch {
+ return NextResponse.json(
+ { success: false, error: "Invalid JSON body" },
+ { status: 400 },
+ );
+ }
+
+ const usernames = [body.username1, body.username2]
+ .map((username) => username?.trim() ?? "")
+ .filter(Boolean);
+
+ if (usernames.length !== 2) {
return NextResponse.json(
- { success: false, error: message },
- { status: 500 }
+ {
+ success: false,
+ error: "Provide username1 and username2 in the request body",
+ },
+ { status: 400 },
);
}
+
+ const selectedLanguages = normalizeSelectedLanguages(body.selectedLanguages);
+
+ try {
+ const users = await compareUsers(usernames, selectedLanguages);
+ const winnerData = calculateWinner(users);
+ return NextResponse.json({ success: true, users, ...winnerData });
+ } catch (error: unknown) {
+ console.error("GitHub score error:", error);
+ const message =
+ error instanceof Error && error.message === "User not found"
+ ? "GitHub user not found"
+ : "Failed to calculate score";
+ return NextResponse.json({ success: false, error: message }, { status: 500 });
+ }
}
diff --git a/components/app-footer.tsx b/components/app-footer.tsx
index da386ee..0e8422d 100644
--- a/components/app-footer.tsx
+++ b/components/app-footer.tsx
@@ -56,7 +56,7 @@ export function AppFooter() {
>
{t("footer.note")}
-
+
diff --git a/components/brand-logo.tsx b/components/brand-logo.tsx
index 969b587..9f71678 100644
--- a/components/brand-logo.tsx
+++ b/components/brand-logo.tsx
@@ -17,7 +17,7 @@ const sizeClasses = {
export function BrandLogo({
size = "md",
className,
- priority = false,
+ priority = true,
}: BrandLogoProps) {
return (
);
diff --git a/components/github-link.tsx b/components/github-link.tsx
index 22ff1fb..2c209c9 100644
--- a/components/github-link.tsx
+++ b/components/github-link.tsx
@@ -1,24 +1,46 @@
"use client";
-import { Button } from "./ui/button";
import { FaGithub } from "react-icons/fa";
-export function GithubLink() {
+import { cn } from "@/lib/utils";
+
+type GithubLinkProps = {
+ variant?: "compact" | "prominent";
+};
+
+export function GithubLink({ variant = "compact" }: GithubLinkProps) {
+ const isProminent = variant === "prominent";
+
return (
-
+
+
+
+
+
+ {isProminent ? (
+
+ GitHub
+
+ ) : null}
+
);
-}
\ No newline at end of file
+}
diff --git a/components/theme-toggle.tsx b/components/theme-toggle.tsx
index 6a2cab1..915dfc2 100644
--- a/components/theme-toggle.tsx
+++ b/components/theme-toggle.tsx
@@ -44,12 +44,7 @@ export function ThemeToggle() {
>
{mounted && current === "dark" ? : }
-
-
-
+
-
-
-
);
}
diff --git a/github-issues.json b/github-issues.json
new file mode 100644
index 0000000..d75818f
--- /dev/null
+++ b/github-issues.json
@@ -0,0 +1,8 @@
+[
+
+ {
+ "title": "refactor: Extract ApiResponse type to the types directory",
+ "body": "The `ApiResponse` type is currently defined directly inside `app/page.tsx`. To improve code organization and make the type reusable across other files (such as API routes), it should be extracted into its own file within the `types` directory.\n\n### Tasks\n- [ ] Create a new file `types/api-response.ts`.\n- [ ] Move the `ApiResponse` type definition from `app/page.tsx` into this new file and `export` it.\n- [ ] Update `app/page.tsx` to import the `ApiResponse` type from the new file.",
+ "labels": "help wanted,good first issue,refactor,easy,beginner friendly"
+ }
+]
\ No newline at end of file
diff --git a/lib/github.ts b/lib/github.ts
index f836f2b..7a0c9c0 100644
--- a/lib/github.ts
+++ b/lib/github.ts
@@ -1,12 +1,76 @@
-import { ContributionTotals, GitHubUserData, PullRequestNode, RepoNode } from "@/types/github";
+import type {
+ DiscussionNode,
+ GitHubUserData,
+ IssueNode,
+ PullRequestNode,
+ ReactionSummary,
+ RepoNode,
+} from "@/types/github";
import { graphql } from "@octokit/graphql";
type GitHubRawUser = {
name: string | null;
avatarUrl: string;
- repositories: { nodes: RepoNode[] };
- pullRequests: { nodes: PullRequestNode[] };
- contributionsCollection: ContributionTotals;
+ repositories: { nodes: Array };
+ contributionsCollection: {
+ totalCommitContributions: number;
+ totalPullRequestContributions: number;
+ totalIssueContributions: number;
+ };
+};
+
+type RawIssueNode = {
+ title: string;
+ url?: string;
+ comments: { totalCount: number };
+ thumbsUp: { totalCount: number };
+ thumbsDown: { totalCount: number };
+ heart: { totalCount: number };
+ hooray: { totalCount: number };
+ rocket: { totalCount: number };
+ eyes: { totalCount: number };
+ confused: { totalCount: number };
+ laugh: { totalCount: number };
+ repository: {
+ nameWithOwner: string;
+ stargazerCount: number;
+ owner: { login: string };
+ languages?: RepoNode["languages"];
+ };
+};
+
+type RawDiscussionNode = {
+ title: string;
+ url?: string;
+ comments: { totalCount: number };
+ thumbsUp: { totalCount: number };
+ thumbsDown: { totalCount: number };
+ heart: { totalCount: number };
+ hooray: { totalCount: number };
+ rocket: { totalCount: number };
+ eyes: { totalCount: number };
+ confused: { totalCount: number };
+ laugh: { totalCount: number };
+ repository: {
+ nameWithOwner: string;
+ stargazerCount: number;
+ owner: { login: string };
+ languages?: RepoNode["languages"];
+ };
+};
+
+type FetchUserDataResponse = {
+ user: GitHubRawUser | null;
+ pullRequests: { nodes: Array };
+ issues: { nodes: Array };
+ discussions: { nodes: Array };
+ rateLimit: {
+ limit: number;
+ remaining: number;
+ used: number;
+ resetAt: string;
+ cost: number;
+ };
};
if (!process.env.GITHUB_TOKEN) {
@@ -19,12 +83,21 @@ const client = graphql.defaults({
},
});
-
const QUERY = /* GraphQL */ `
- query FetchUserData($login: String!, $repoCount: Int = 100, $prCount: Int = 100) {
+ query FetchUserData(
+ $login: String!
+ $repoCount: Int = 100
+ $prCount: Int = 100
+ $issueCount: Int = 100
+ $discussionCount: Int = 100
+ $externalPrQuery: String!
+ $externalIssueQuery: String!
+ $externalDiscussionQuery: String!
+ ) {
user(login: $login) {
name
avatarUrl(size: 80)
+
repositories(
first: $repoCount
privacy: PUBLIC
@@ -33,46 +106,239 @@ const QUERY = /* GraphQL */ `
) {
nodes {
name
+ nameWithOwner
+ url
+ isFork
stargazerCount
forkCount
+ pushedAt
watchers {
totalCount
}
+ languages(first: 10, orderBy: { field: SIZE, direction: DESC }) {
+ edges {
+ size
+ node {
+ name
+ }
+ }
+ }
}
}
- pullRequests(
- first: $prCount
- states: [MERGED]
- orderBy: { field: CREATED_AT, direction: DESC }
- ) {
- nodes {
+
+ contributionsCollection {
+ totalCommitContributions
+ totalPullRequestContributions
+ totalIssueContributions
+ }
+ }
+
+ pullRequests: search(query: $externalPrQuery, type: ISSUE, first: $prCount) {
+ nodes {
+ ... on PullRequest {
merged
additions
deletions
title
url
+
repository {
nameWithOwner
+ url
stargazerCount
+ pushedAt
owner {
login
}
+ languages(first: 10, orderBy: { field: SIZE, direction: DESC }) {
+ edges {
+ size
+ node {
+ name
+ }
+ }
+ }
}
}
}
- contributionsCollection {
- totalCommitContributions
- totalPullRequestContributions
- totalIssueContributions
+ }
+
+ issues: search(query: $externalIssueQuery, type: ISSUE, first: $issueCount) {
+ nodes {
+ ... on Issue {
+ title
+ url
+ comments {
+ totalCount
+ }
+ thumbsUp: reactions(content: THUMBS_UP) {
+ totalCount
+ }
+ thumbsDown: reactions(content: THUMBS_DOWN) {
+ totalCount
+ }
+ heart: reactions(content: HEART) {
+ totalCount
+ }
+ hooray: reactions(content: HOORAY) {
+ totalCount
+ }
+ rocket: reactions(content: ROCKET) {
+ totalCount
+ }
+ eyes: reactions(content: EYES) {
+ totalCount
+ }
+ confused: reactions(content: CONFUSED) {
+ totalCount
+ }
+ laugh: reactions(content: LAUGH) {
+ totalCount
+ }
+ repository {
+ nameWithOwner
+ stargazerCount
+ owner {
+ login
+ }
+ languages(first: 10, orderBy: { field: SIZE, direction: DESC }) {
+ edges {
+ size
+ node {
+ name
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ discussions: search(
+ query: $externalDiscussionQuery
+ type: DISCUSSION
+ first: $discussionCount
+ ) {
+ nodes {
+ ... on Discussion {
+ title
+ url
+ comments {
+ totalCount
+ }
+ thumbsUp: reactions(content: THUMBS_UP) {
+ totalCount
+ }
+ thumbsDown: reactions(content: THUMBS_DOWN) {
+ totalCount
+ }
+ heart: reactions(content: HEART) {
+ totalCount
+ }
+ hooray: reactions(content: HOORAY) {
+ totalCount
+ }
+ rocket: reactions(content: ROCKET) {
+ totalCount
+ }
+ eyes: reactions(content: EYES) {
+ totalCount
+ }
+ confused: reactions(content: CONFUSED) {
+ totalCount
+ }
+ laugh: reactions(content: LAUGH) {
+ totalCount
+ }
+ repository {
+ nameWithOwner
+ stargazerCount
+ owner {
+ login
+ }
+ languages(first: 10, orderBy: { field: SIZE, direction: DESC }) {
+ edges {
+ size
+ node {
+ name
+ }
+ }
+ }
+ }
+ }
}
}
+
+ rateLimit {
+ limit
+ remaining
+ used
+ resetAt
+ cost
+ }
}
`;
+function isDefined(value: T | null | undefined): value is T {
+ return value !== null && value !== undefined;
+}
+
+function toReactionSummary(item: RawIssueNode): ReactionSummary {
+ return {
+ thumbsUp: item.thumbsUp.totalCount,
+ thumbsDown: item.thumbsDown.totalCount,
+ heart: item.heart.totalCount,
+ hooray: item.hooray.totalCount,
+ rocket: item.rocket.totalCount,
+ eyes: item.eyes.totalCount,
+ confused: item.confused.totalCount,
+ laugh: item.laugh.totalCount,
+ };
+}
+
+function toIssueNode(item: RawIssueNode): IssueNode {
+ return {
+ title: item.title,
+ url: item.url,
+ comments: item.comments,
+ reactions: toReactionSummary(item),
+ repository: item.repository,
+ };
+}
+
+function toDiscussionNode(item: RawDiscussionNode): DiscussionNode {
+ return {
+ title: item.title,
+ url: item.url,
+ comments: item.comments,
+ reactions: toReactionSummary(item),
+ repository: item.repository,
+ };
+}
+
export async function fetchGitHubUserData(
- username: string
+ username: string,
): Promise {
- const { user } = await client<{ user: GitHubRawUser | null }>(QUERY, { login: username });
+ const externalPrQuery = `type:pr is:merged author:${username} -user:${username}`;
+ const externalIssueQuery = `type:issue author:${username} -user:${username}`;
+ const externalDiscussionQuery = `author:${username} -user:${username}`;
+
+ const response = await client(QUERY, {
+ login: username,
+ repoCount: 100,
+ prCount: 100,
+ issueCount: 100,
+ discussionCount: 100,
+ externalPrQuery,
+ externalIssueQuery,
+ externalDiscussionQuery,
+ headers: {
+ authorization: `bearer ${process.env.GITHUB_TOKEN}`,
+ },
+ });
+
+ const user = response.user;
+ console.log(response.rateLimit);
if (!user) {
throw new Error("User not found");
@@ -81,8 +347,12 @@ export async function fetchGitHubUserData(
return {
name: user.name,
avatarUrl: user.avatarUrl,
- repos: user.repositories.nodes,
- pullRequests: user.pullRequests.nodes,
+ repos: user.repositories.nodes.filter(isDefined),
+ pullRequests: response.pullRequests.nodes.filter(isDefined),
contributions: user.contributionsCollection,
+ issues: response.issues.nodes.filter(isDefined).map(toIssueNode),
+ discussions: response.discussions.nodes
+ .filter(isDefined)
+ .map(toDiscussionNode),
};
}
diff --git a/lib/i18n.ts b/lib/i18n.ts
index b6b4ee8..421e82b 100644
--- a/lib/i18n.ts
+++ b/lib/i18n.ts
@@ -40,8 +40,17 @@ function persistLocale(locale: Locale) {
}
export function useI18nProvider(initialLocale: Locale = DEFAULT_LOCALE) {
- const [locale, setLocaleState] = useState(initialLocale);
- const [messages, setMessages] = useState(() => messagesByLocale[initialLocale]);
+ const getInitialLocale = () => {
+ if (typeof window !== "undefined") {
+ const stored = window.localStorage.getItem(LOCALE_COOKIE);
+ if (isSupportedLocale(stored)) return stored;
+ }
+ return initialLocale;
+ };
+
+ const initialLoc = getInitialLocale();
+ const [locale, setLocaleState] = useState(initialLoc);
+ const [messages, setMessages] = useState(() => messagesByLocale[initialLoc]);
const [ready, setReady] = useState(true);
const changeLocale = useCallback((next: Locale) => {
@@ -61,15 +70,6 @@ export function useI18nProvider(initialLocale: Locale = DEFAULT_LOCALE) {
});
}, []);
- useEffect(() => {
- if (typeof window === "undefined") return;
-
- const stored = window.localStorage.getItem(LOCALE_COOKIE);
- if (isSupportedLocale(stored) && stored !== locale) {
- changeLocale(stored);
- }
- }, [changeLocale, locale]);
-
useEffect(() => {
if (typeof document === "undefined") return;
const meta = localeMeta[locale];
diff --git a/lib/score.ts b/lib/score.ts
index 237005d..66fc209 100644
--- a/lib/score.ts
+++ b/lib/score.ts
@@ -1,125 +1,868 @@
import type {
ContributionTotals,
+ DiscussionNode,
+ IssueNode,
PullRequestNode,
+ ReactionSummary,
RepoNode,
} from "@/types/github";
-import { PullRequestScoreDetail, RepoScoreDetail } from "@/types/score";
+import type {
+ CommunityContributionDetail,
+ PullRequestScoreDetail,
+ RepoScoreDetail,
+ ScoringExplanations,
+ ScoringSignals,
+} from "@/types/score";
+import {
+ getLanguageDistribution,
+ getLanguageFactor,
+ getLanguageMatch,
+ getTopLanguages,
+ normalizeSelectedLanguages,
+} from "@/lib/scoring/languageScoring";
+
+const MS_PER_DAY = 86_400_000;
+const FALLBACK_REFERENCE_DATE = "2026-01-01T00:00:00.000Z";
+
+export function safeLog(value: number): number {
+ return Math.log(Math.max(0, value) + 1);
+}
+
+export function roundScore(value: number): number {
+ return Number.isFinite(value) ? Math.round(value) : 0;
+}
+
+export function normalizeScore(score: number, k: number): number {
+ const sanitizedScore = sanitizeNumber(score);
+ const sanitizedK = Math.max(0, sanitizeNumber(k));
+ const denominator = sanitizedScore + sanitizedK;
+
+ if (denominator <= 0) {
+ return 0;
+ }
+
+ return (100 * sanitizedScore) / denominator;
+}
+
+export function getDiminishingWeight(index: number): number {
+ const safeIndex = Math.max(0, index);
+ return 1 / (safeIndex + 1);
+}
-const LOG = Math.log;
+export function getRepoRankWeight(index: number): number {
+ return index < 5 ? 1 : 0.1;
+}
+
+function sanitizeNumber(value: number): number {
+ return Number.isFinite(value) ? value : 0;
+}
+
+function parseDate(value?: string): Date | null {
+ if (!value) {
+ return null;
+ }
+
+ const parsed = new Date(value);
+ if (Number.isNaN(parsed.getTime())) {
+ return null;
+ }
+
+ return parsed;
+}
+
+function resolveReferenceDate(data: {
+ repos: RepoNode[];
+ pullRequests: PullRequestNode[];
+ referenceDate?: string;
+}): Date {
+ const timestamps: number[] = [];
+
+ const explicitReference = parseDate(data.referenceDate);
+ if (explicitReference) {
+ timestamps.push(explicitReference.getTime());
+ }
+ for (const repo of data.repos) {
+ const parsed = parseDate(repo.pushedAt);
+ if (parsed) {
+ timestamps.push(parsed.getTime());
+ }
+ }
+
+ for (const pr of data.pullRequests) {
+ const parsed = parseDate(pr.repository.pushedAt);
+ if (parsed) {
+ timestamps.push(parsed.getTime());
+ }
+ }
+
+ if (timestamps.length === 0) {
+ return new Date(FALLBACK_REFERENCE_DATE);
+ }
+
+ return new Date(Math.max(...timestamps));
+}
+
+function getDaysSince(dateValue: string, referenceDate: Date): number | null {
+ const date = parseDate(dateValue);
+ if (!date) {
+ return null;
+ }
+
+ const diff = referenceDate.getTime() - date.getTime();
+ return Math.max(0, diff / MS_PER_DAY);
+}
+
+function getRepoActivityFactor(
+ pushedAt: string | undefined,
+ referenceDate: Date,
+): number {
+ if (!pushedAt) {
+ return 0.8;
+ }
+
+ const daysSincePush = getDaysSince(pushedAt, referenceDate);
+ if (daysSincePush === null) {
+ return 0.8;
+ }
+
+ if (daysSincePush <= 90) {
+ return 1.2;
+ }
+ if (daysSincePush <= 365) {
+ return 1.0;
+ }
+ if (daysSincePush <= 730) {
+ return 0.7;
+ }
+ return 0.4;
+}
+
+function getPullRequestRepoActivityFactor(
+ pushedAt: string | undefined,
+ referenceDate: Date,
+): number {
+ if (!pushedAt) {
+ return 0.9;
+ }
+
+ const daysSincePush = getDaysSince(pushedAt, referenceDate);
+ if (daysSincePush === null) {
+ return 0.9;
+ }
+ if (daysSincePush <= 90) {
+ return 1.1;
+ }
+ if (daysSincePush <= 365) {
+ return 1.0;
+ }
+ if (daysSincePush <= 730) {
+ return 0.85;
+ }
+ return 0.7;
+}
function calculateRepoScore(
- repos: RepoNode[]
+ repos: RepoNode[],
+ referenceDate: Date,
): { total: number; details: RepoScoreDetail[] } {
const details = repos.map((repo) => {
+ const baseRepoScore =
+ safeLog(repo.stargazerCount) * 5 +
+ safeLog(repo.forkCount) * 3 +
+ safeLog(repo.watchers.totalCount) * 2;
+
+ let score = baseRepoScore;
+
+ if (repo.isFork === true) {
+ score *= 0.2;
+ }
+
+ score *= getRepoActivityFactor(repo.pushedAt, referenceDate);
- const score = LOG(repo.stargazerCount + 1) * 5 +
- LOG(repo.forkCount + 1) * 3 +
- LOG(repo.watchers.totalCount + 1) * 2
- return { repo, score: Math.round(score) };
+ return { repo, score: sanitizeNumber(score) };
});
details.sort((a, b) => b.score - a.score);
const total = details.reduce((sum, { score }, index) => {
- const weight = index < 5 ? 1 : 0.1;
- return sum + score * weight;
+ return sum + score * getRepoRankWeight(index);
}, 0);
- return { total, details };
+ return { total: sanitizeNumber(total), details };
}
+type PRScoreResult = {
+ total: number;
+ details: PullRequestScoreDetail[];
+ mergedExternalPRs: number;
+ ownRepoPRsIgnored: number;
+ unmergedPRsIgnored: number;
+ uniqueExternalPRRepos: number;
+};
+
function calculatePRScore(
prs: PullRequestNode[],
- username: string
-): { total: number; details: PullRequestScoreDetail[] } {
- const grouped: Record = {};
+ username: string,
+ referenceDate: Date,
+): PRScoreResult {
+ const grouped = new Map();
+ const normalizedUsername = username.toLowerCase();
+
+ let mergedExternalPRs = 0;
+ let ownRepoPRsIgnored = 0;
+ let unmergedPRsIgnored = 0;
for (const pr of prs) {
const repoOwner = pr.repository.owner.login.toLowerCase();
- if (repoOwner === username.toLowerCase()) continue; // ignore own repos
- if (!pr.merged) continue; // only merged PRs
- const base = LOG(pr.repository.stargazerCount + 1) * 2;
- const sizeFactor = LOG(pr.additions + pr.deletions + 1);
- const score = base * sizeFactor;
+ if (!pr.merged) {
+ unmergedPRsIgnored += 1;
+ continue;
+ }
+
+ if (repoOwner === normalizedUsername) {
+ ownRepoPRsIgnored += 1;
+ continue;
+ }
+
+ const changedLines = Math.max(0, pr.additions) + Math.max(0, pr.deletions);
+ const base = safeLog(pr.repository.stargazerCount) * 2;
+ const sizeFactor = Math.min(safeLog(changedLines), 5);
+
+ let score = base * sizeFactor;
+
+ if (changedLines < 5) {
+ score *= 0.25;
+ }
+
+ if (changedLines > 5000) {
+ score *= 0.6;
+ }
+
+ score *= getPullRequestRepoActivityFactor(pr.repository.pushedAt, referenceDate);
+ score = sanitizeNumber(score);
const repoKey = pr.repository.nameWithOwner;
- grouped[repoKey] ||= [];
- grouped[repoKey].push({ pr, score: Math.round(score) });
+ const existingScores = grouped.get(repoKey) ?? [];
+ existingScores.push({ pr, score });
+ grouped.set(repoKey, existingScores);
+ mergedExternalPRs += 1;
}
let total = 0;
const allDetails: PullRequestScoreDetail[] = [];
- for (const repoScores of Object.values(grouped)) {
+ for (const repoScores of grouped.values()) {
repoScores.sort((a, b) => b.score - a.score);
- const repoTotal = repoScores.reduce(
- (sum, item, i) => sum + item.score * (1 / (i + 1)),
- 0
- );
+
+ const repoTotal = repoScores.reduce((sum, item, index) => {
+ return sum + item.score * getDiminishingWeight(index);
+ }, 0);
+
total += repoTotal;
allDetails.push(...repoScores);
}
allDetails.sort((a, b) => b.score - a.score);
- return { total, details: allDetails };
+ return {
+ total: sanitizeNumber(total),
+ details: allDetails,
+ mergedExternalPRs,
+ ownRepoPRsIgnored,
+ unmergedPRsIgnored,
+ uniqueExternalPRRepos: grouped.size,
+ };
+}
+
+function readReactionTotals(
+ reactions?: ReactionSummary,
+): { positive: number; neutral: number; negative: number } {
+ if (!reactions) {
+ return { positive: 0, neutral: 0, negative: 0 };
+ }
+
+ const positive =
+ reactions.thumbsUp * 1 +
+ reactions.heart * 1.2 +
+ reactions.hooray * 1.2 +
+ reactions.rocket * 1.3;
+ const neutral = reactions.eyes * 0.4 + reactions.laugh * 0.2;
+ const negative = reactions.thumbsDown * 1.5 + reactions.confused * 1;
+
+ return {
+ positive: sanitizeNumber(positive),
+ neutral: sanitizeNumber(neutral),
+ negative: sanitizeNumber(negative),
+ };
+}
+
+function calculateCommunityItemScore(
+ item: IssueNode | DiscussionNode,
+): { score: number; reactionQuality: number; negativeRatio: number } {
+ const repoStars = Math.max(0, item.repository.stargazerCount);
+ const comments = Math.max(0, item.comments.totalCount);
+ const { positive, neutral, negative } = readReactionTotals(item.reactions);
+
+ const reactionQuality = Math.max(0, positive + neutral - negative);
+ const reactionTotal = positive + neutral + negative;
+ const negativeRatio = reactionTotal > 0 ? negative / reactionTotal : 0;
+
+ let score =
+ safeLog(repoStars) *
+ safeLog(Math.max(0, comments + reactionQuality));
+
+ if (comments === 0 && reactionQuality === 0) {
+ score *= 0.2;
+ }
+
+ if (negativeRatio > 0.5) {
+ score *= 0.2;
+ } else if (negativeRatio > 0.3) {
+ score *= 0.6;
+ }
+
+ return {
+ score: sanitizeNumber(score),
+ reactionQuality: sanitizeNumber(reactionQuality),
+ negativeRatio: sanitizeNumber(negativeRatio),
+ };
+}
+
+type CommunityScoreResult = {
+ total: number;
+ details: CommunityContributionDetail[];
+ issuesAnalyzed: number;
+ externalIssuesCounted: number;
+ discussionsAnalyzed: number;
+ externalDiscussionsCounted: number;
+};
+
+function calculateContributionScore(
+ _contrib: ContributionTotals | undefined,
+ issues: IssueNode[],
+ discussions: DiscussionNode[],
+ username: string,
+): CommunityScoreResult {
+ const normalizedUsername = username.toLowerCase();
+ const details: CommunityContributionDetail[] = [];
+
+ let externalIssuesCounted = 0;
+ let externalDiscussionsCounted = 0;
+
+ for (const issue of issues) {
+ if (issue.repository.owner.login.toLowerCase() === normalizedUsername) {
+ continue;
+ }
+
+ const { score } = calculateCommunityItemScore(issue);
+ details.push({
+ type: "issue",
+ item: issue,
+ score,
+ });
+ externalIssuesCounted += 1;
+ }
+
+ for (const discussion of discussions) {
+ if (
+ discussion.repository.owner.login.toLowerCase() === normalizedUsername
+ ) {
+ continue;
+ }
+
+ const { score } = calculateCommunityItemScore(discussion);
+ details.push({
+ type: "discussion",
+ item: discussion,
+ score,
+ });
+ externalDiscussionsCounted += 1;
+ }
+
+ details.sort((a, b) => b.score - a.score);
+
+ const total = details.reduce((sum, detail, index) => {
+ return sum + detail.score * getDiminishingWeight(index);
+ }, 0);
+
+ return {
+ total: sanitizeNumber(total),
+ details,
+ issuesAnalyzed: issues.length,
+ externalIssuesCounted,
+ discussionsAnalyzed: discussions.length,
+ externalDiscussionsCounted,
+ };
}
-function calculateContributionScore(contrib: ContributionTotals): number {
- return (
- contrib.totalCommitContributions * 0.5 +
- contrib.totalPullRequestContributions * 2 +
- contrib.totalIssueContributions * 0.3
+function hasLanguageData(languages: RepoNode["languages"] | undefined): boolean {
+ return Object.keys(getLanguageDistribution(languages)).length > 0;
+}
+
+function calculateLanguageRepoScore(
+ repoDetails: RepoScoreDetail[],
+ selectedLanguages: string[],
+): {
+ total: number;
+ details: Array<{
+ repo: RepoNode;
+ score: number;
+ languageMatch: number;
+ }>;
+ reposWithLanguageData: number;
+ averageLanguageMatch: number;
+} {
+ const details = repoDetails.map((item) => {
+ const languageMatch = getLanguageMatch(item.repo.languages, selectedLanguages);
+ const languageFactor = getLanguageFactor(languageMatch);
+ return {
+ repo: item.repo,
+ score: sanitizeNumber(item.score * languageFactor),
+ languageMatch,
+ };
+ });
+
+ details.sort((a, b) => b.score - a.score);
+
+ const total = details.reduce((sum, detail, index) => {
+ return sum + detail.score * getRepoRankWeight(index);
+ }, 0);
+
+ const reposWithLanguageData = details.reduce((count, detail) => {
+ return count + (hasLanguageData(detail.repo.languages) ? 1 : 0);
+ }, 0);
+
+ const averageLanguageMatch =
+ details.length > 0
+ ? details.reduce((sum, detail) => sum + detail.languageMatch, 0) / details.length
+ : 0;
+
+ return {
+ total: sanitizeNumber(total),
+ details,
+ reposWithLanguageData,
+ averageLanguageMatch: sanitizeNumber(averageLanguageMatch),
+ };
+}
+
+function calculateLanguagePRScore(
+ prDetails: PullRequestScoreDetail[],
+ selectedLanguages: string[],
+): {
+ total: number;
+ details: Array<{
+ pr: PullRequestNode;
+ score: number;
+ languageMatch: number;
+ }>;
+ prsWithLanguageData: number;
+ averageLanguageMatch: number;
+} {
+ const grouped = new Map<
+ string,
+ Array<{ pr: PullRequestNode; score: number; languageMatch: number }>
+ >();
+
+ for (const item of prDetails) {
+ const languageMatch = getLanguageMatch(
+ item.pr.repository.languages,
+ selectedLanguages,
+ );
+ const languageFactor = getLanguageFactor(languageMatch);
+ const score = sanitizeNumber(item.score * languageFactor);
+ const key = item.pr.repository.nameWithOwner;
+ const current = grouped.get(key) ?? [];
+ current.push({ pr: item.pr, score, languageMatch });
+ grouped.set(key, current);
+ }
+
+ let total = 0;
+ const details: Array<{ pr: PullRequestNode; score: number; languageMatch: number }> = [];
+
+ for (const repoScores of grouped.values()) {
+ repoScores.sort((a, b) => b.score - a.score);
+ const repoTotal = repoScores.reduce((sum, item, index) => {
+ return sum + item.score * getDiminishingWeight(index);
+ }, 0);
+ total += repoTotal;
+ details.push(...repoScores);
+ }
+
+ details.sort((a, b) => b.score - a.score);
+
+ const prsWithLanguageData = details.reduce((count, detail) => {
+ return count + (hasLanguageData(detail.pr.repository.languages) ? 1 : 0);
+ }, 0);
+
+ const averageLanguageMatch =
+ details.length > 0
+ ? details.reduce((sum, detail) => sum + detail.languageMatch, 0) / details.length
+ : 0;
+
+ return {
+ total: sanitizeNumber(total),
+ details,
+ prsWithLanguageData,
+ averageLanguageMatch: sanitizeNumber(averageLanguageMatch),
+ };
+}
+
+function calculateLanguageContributionScore(
+ communityDetails: CommunityContributionDetail[],
+ selectedLanguages: string[],
+): number {
+ const allHaveNoLanguageData = communityDetails.every(
+ (detail) => !hasLanguageData(detail.item.repository.languages),
);
+
+ if (allHaveNoLanguageData) {
+ return communityDetails.reduce((sum, detail, index) => {
+ return sum + detail.score * getDiminishingWeight(index);
+ }, 0);
+ }
+
+ const adjusted = communityDetails
+ .map((detail) => {
+ const languageMatch = getLanguageMatch(
+ detail.item.repository.languages,
+ selectedLanguages,
+ );
+ return sanitizeNumber(detail.score * getLanguageFactor(languageMatch));
+ })
+ .sort((a, b) => b - a);
+
+ return adjusted.reduce((sum, score, index) => {
+ return sum + score * getDiminishingWeight(index);
+ }, 0);
}
+type TopRepo = {
+ name: string;
+ url?: string;
+ stars: number;
+ forks: number;
+ watchers: number;
+ score: number;
+};
+
+type TopPullRequest = {
+ repo: string;
+ title: string;
+ url?: string;
+ stars: number;
+ score: number;
+ additions: number;
+ deletions: number;
+};
+
+type TopCommunityContribution = {
+ type: "issue" | "discussion";
+ title: string;
+ url?: string;
+ repo: string;
+ stars: number;
+ comments: number;
+ score: number;
+};
+
+type TopLanguageRepo = TopRepo & {
+ languageMatch: number;
+ topLanguages: {
+ name: string;
+ percentage: number;
+ }[];
+};
+
+type TopLanguagePullRequest = TopPullRequest & {
+ languageMatch: number;
+ topLanguages: {
+ name: string;
+ percentage: number;
+ }[];
+};
+
+type LanguageScores = {
+ selectedLanguages: string[];
+ repoScore: number;
+ prScore: number;
+ contributionScore: number;
+ finalScore: number;
+ normalizedRepoScore: number;
+ normalizedPRScore: number;
+ normalizedContributionScore: number;
+ normalizedFinalScore: number;
+ topRepos: TopLanguageRepo[];
+ topPullRequests: TopLanguagePullRequest[];
+};
+
+export type CalculateUserScoreResult = {
+ username: string;
+ repoScore: number;
+ prScore: number;
+ contributionScore: number;
+ finalScore: number;
+ normalizedRepoScore: number;
+ normalizedPRScore: number;
+ normalizedContributionScore: number;
+ normalizedFinalScore: number;
+ scores: {
+ repoScore: number;
+ prScore: number;
+ contributionScore: number;
+ finalScore: number;
+ normalizedRepoScore: number;
+ normalizedPRScore: number;
+ normalizedContributionScore: number;
+ normalizedFinalScore: number;
+ };
+ topRepos: TopRepo[];
+ topPullRequests: TopPullRequest[];
+ topCommunityContributions: TopCommunityContribution[];
+ languageScores?: LanguageScores;
+ signals: ScoringSignals;
+ explanations: ScoringExplanations;
+};
+
+const scoringExplanations: ScoringExplanations = {
+ repo: [
+ "Repository score is based on stars, forks, watchers, and activity.",
+ "Forked repositories are heavily reduced.",
+ "Top repositories contribute most to the repository score.",
+ ],
+ pr: [
+ "Only merged pull requests are counted.",
+ "Pull requests to the user's own repositories are ignored.",
+ "Repeated pull requests to the same repository use diminishing returns.",
+ "Tiny PRs and huge generated PRs are reduced.",
+ ],
+ contribution: [
+ "Contribution score is based on external issues and discussions only.",
+ "Commits and pull requests are excluded to avoid double-counting.",
+ "Negative reactions reduce issue and discussion impact.",
+ "Contribution score is capped so it cannot dominate the final score.",
+ ],
+ overall: [
+ "Final score is weighted 45% repository impact, 45% pull request impact, and 10% community contribution impact.",
+ ],
+};
+
export function calculateUserScore(
data: {
repos: RepoNode[];
pullRequests: PullRequestNode[];
- contributions: ContributionTotals;
+ contributions?: ContributionTotals;
+ issues?: IssueNode[];
+ discussions?: DiscussionNode[];
+ referenceDate?: string;
+ selectedLanguages?: string[];
},
- username: string
-): {
- repoScore: number;
- prScore: number;
- contributionScore: number;
- finalScore: number;
- topRepos: { name: string; stars: number; forks: number; score: number }[];
- topPullRequests: { repo: string; stars: number; score: number }[];
-} {
- const repoScore = calculateRepoScore(data.repos);
- const prScore = calculatePRScore(data.pullRequests, username);
- let contributionScore = calculateContributionScore(data.contributions);
- contributionScore = Math.min(contributionScore, 0.3 * (repoScore.total + prScore.total))
+ username: string,
+): CalculateUserScoreResult {
+ const selectedLanguages = normalizeSelectedLanguages(data.selectedLanguages);
+ const hasSelectedLanguages = selectedLanguages.length > 0;
+ const referenceDate = resolveReferenceDate({
+ repos: data.repos,
+ pullRequests: data.pullRequests,
+ referenceDate: data.referenceDate,
+ });
+
+ const repoScore = calculateRepoScore(data.repos, referenceDate);
+ const prScore = calculatePRScore(data.pullRequests, username, referenceDate);
+ const communityScore = calculateContributionScore(
+ data.contributions,
+ data.issues ?? [],
+ data.discussions ?? [],
+ username,
+ );
+
+ let contributionScore = communityScore.total;
+ contributionScore = Math.min(
+ contributionScore,
+ 0.3 * (repoScore.total + prScore.total),
+ );
+ contributionScore = sanitizeNumber(contributionScore);
const finalScore =
- repoScore.total * 0.4 + prScore.total * 0.4 + contributionScore * 0.2;
+ repoScore.total * 0.45 + prScore.total * 0.45 + contributionScore * 0.1;
+
+ const normalizedRepoScore = normalizeScore(repoScore.total, 100);
+ const normalizedPRScore = normalizeScore(prScore.total, 300);
+ const normalizedContributionScore = normalizeScore(contributionScore, 100);
+ const normalizedFinalScore =
+ normalizedRepoScore * 0.45 +
+ normalizedPRScore * 0.45 +
+ normalizedContributionScore * 0.1;
+
+ let languageScores: LanguageScores | undefined;
+ let languageRepoSignals: Pick<
+ ScoringSignals,
+ "reposWithLanguageData" | "averageRepoLanguageMatch"
+ > = {};
+ let languagePRSignals: Pick<
+ ScoringSignals,
+ "prsWithLanguageData" | "averagePRLanguageMatch"
+ > = {};
+
+ if (hasSelectedLanguages) {
+ const languageRepoScore = calculateLanguageRepoScore(
+ repoScore.details,
+ selectedLanguages,
+ );
+ const languagePRScore = calculateLanguagePRScore(
+ prScore.details,
+ selectedLanguages,
+ );
+
+ let languageContributionScore = sanitizeNumber(
+ calculateLanguageContributionScore(communityScore.details, selectedLanguages),
+ );
+ languageContributionScore = Math.min(
+ languageContributionScore,
+ 0.3 * (languageRepoScore.total + languagePRScore.total),
+ );
+ languageContributionScore = sanitizeNumber(languageContributionScore);
+
+ const languageFinalScore =
+ languageRepoScore.total * 0.45 +
+ languagePRScore.total * 0.45 +
+ languageContributionScore * 0.1;
+
+ const normalizedLanguageRepoScore = normalizeScore(languageRepoScore.total, 100);
+ const normalizedLanguagePRScore = normalizeScore(languagePRScore.total, 300);
+ const normalizedLanguageContributionScore = normalizeScore(
+ languageContributionScore,
+ 100,
+ );
+ const normalizedLanguageFinalScore =
+ normalizedLanguageRepoScore * 0.45 +
+ normalizedLanguagePRScore * 0.45 +
+ normalizedLanguageContributionScore * 0.1;
+
+ languageScores = {
+ selectedLanguages,
+ repoScore: sanitizeNumber(languageRepoScore.total),
+ prScore: sanitizeNumber(languagePRScore.total),
+ contributionScore: languageContributionScore,
+ finalScore: sanitizeNumber(languageFinalScore),
+ normalizedRepoScore: sanitizeNumber(normalizedLanguageRepoScore),
+ normalizedPRScore: sanitizeNumber(normalizedLanguagePRScore),
+ normalizedContributionScore: sanitizeNumber(normalizedLanguageContributionScore),
+ normalizedFinalScore: sanitizeNumber(normalizedLanguageFinalScore),
+ topRepos: languageRepoScore.details.slice(0, 3).map((item) => ({
+ name: item.repo.name,
+ url: item.repo.url,
+ stars: item.repo.stargazerCount,
+ forks: item.repo.forkCount,
+ watchers: item.repo.watchers.totalCount,
+ score: roundScore(item.score),
+ languageMatch: sanitizeNumber(item.languageMatch),
+ topLanguages: getTopLanguages(item.repo.languages, 3),
+ })),
+ topPullRequests: languagePRScore.details.slice(0, 3).map((item) => ({
+ repo: item.pr.repository.nameWithOwner,
+ title: item.pr.title,
+ url: item.pr.url,
+ stars: item.pr.repository.stargazerCount,
+ score: roundScore(item.score),
+ additions: item.pr.additions,
+ deletions: item.pr.deletions,
+ languageMatch: sanitizeNumber(item.languageMatch),
+ topLanguages: getTopLanguages(item.pr.repository.languages, 3),
+ })),
+ };
+
+ languageRepoSignals = {
+ reposWithLanguageData: languageRepoScore.reposWithLanguageData,
+ averageRepoLanguageMatch: languageRepoScore.averageLanguageMatch,
+ };
+
+ languagePRSignals = {
+ prsWithLanguageData: languagePRScore.prsWithLanguageData,
+ averagePRLanguageMatch: languagePRScore.averageLanguageMatch,
+ };
+ }
+
+ const explanations: ScoringExplanations = {
+ ...scoringExplanations,
+ };
+
+ if (hasSelectedLanguages) {
+ explanations.language = [
+ "Language-focused score is optional and does not replace the overall score.",
+ "Repository language match is calculated from GitHub repository language byte distribution.",
+ "Pull request language match uses the target repository language distribution as an approximation.",
+ "Non-matching repositories are softly reduced instead of fully ignored.",
+ "Repositories with missing language data use a neutral language factor.",
+ "Community contribution language matching uses repository language data when available.",
+ ];
+ }
return {
- repoScore: repoScore.total,
- prScore: prScore.total,
+ username,
+ repoScore: sanitizeNumber(repoScore.total),
+ prScore: sanitizeNumber(prScore.total),
contributionScore,
- finalScore,
+ finalScore: sanitizeNumber(finalScore),
+ normalizedRepoScore: sanitizeNumber(normalizedRepoScore),
+ normalizedPRScore: sanitizeNumber(normalizedPRScore),
+ normalizedContributionScore: sanitizeNumber(normalizedContributionScore),
+ normalizedFinalScore: sanitizeNumber(normalizedFinalScore),
+ scores: {
+ repoScore: sanitizeNumber(repoScore.total),
+ prScore: sanitizeNumber(prScore.total),
+ contributionScore,
+ finalScore: sanitizeNumber(finalScore),
+ normalizedRepoScore: sanitizeNumber(normalizedRepoScore),
+ normalizedPRScore: sanitizeNumber(normalizedPRScore),
+ normalizedContributionScore: sanitizeNumber(normalizedContributionScore),
+ normalizedFinalScore: sanitizeNumber(normalizedFinalScore),
+ },
topRepos: repoScore.details.slice(0, 3).map((item) => ({
name: item.repo.name,
+ url: item.repo.url,
stars: item.repo.stargazerCount,
forks: item.repo.forkCount,
- score: item.score,
watchers: item.repo.watchers.totalCount,
+ score: roundScore(item.score),
})),
topPullRequests: prScore.details.slice(0, 3).map((item) => ({
repo: item.pr.repository.nameWithOwner,
title: item.pr.title,
url: item.pr.url,
stars: item.pr.repository.stargazerCount,
- score: item.score,
+ score: roundScore(item.score),
additions: item.pr.additions,
deletions: item.pr.deletions,
})),
+ topCommunityContributions: communityScore.details.slice(0, 3).map((item) => ({
+ type: item.type,
+ title: item.item.title,
+ url: item.item.url,
+ repo: item.item.repository.nameWithOwner,
+ stars: item.item.repository.stargazerCount,
+ comments: item.item.comments.totalCount,
+ score: roundScore(item.score),
+ })),
+ languageScores,
+ signals: {
+ reposAnalyzed: data.repos.length,
+ pullRequestsAnalyzed: data.pullRequests.length,
+ mergedExternalPRs: prScore.mergedExternalPRs,
+ ownRepoPRsIgnored: prScore.ownRepoPRsIgnored,
+ unmergedPRsIgnored: prScore.unmergedPRsIgnored,
+ uniqueExternalPRRepos: prScore.uniqueExternalPRRepos,
+ issuesAnalyzed: communityScore.issuesAnalyzed,
+ externalIssuesCounted: communityScore.externalIssuesCounted,
+ discussionsAnalyzed: communityScore.discussionsAnalyzed,
+ externalDiscussionsCounted: communityScore.externalDiscussionsCounted,
+ ...(hasSelectedLanguages ? { selectedLanguages } : {}),
+ ...languageRepoSignals,
+ ...languagePRSignals,
+ },
+ explanations,
};
}
diff --git a/lib/scoring/languageScoring.ts b/lib/scoring/languageScoring.ts
new file mode 100644
index 0000000..a77be89
--- /dev/null
+++ b/lib/scoring/languageScoring.ts
@@ -0,0 +1,118 @@
+import type { RepoLanguages } from "@/types/github";
+
+const MAX_SELECTED_LANGUAGES = 5;
+
+export function normalizeLanguageName(language: string): string {
+ return language.trim().toLowerCase();
+}
+
+export function normalizeSelectedLanguages(languages?: string[]): string[] {
+ if (!languages || languages.length === 0) {
+ return [];
+ }
+
+ const unique: string[] = [];
+ const seen = new Set();
+
+ for (const language of languages) {
+ const normalized = normalizeLanguageName(language);
+ if (!normalized || seen.has(normalized)) {
+ continue;
+ }
+ unique.push(normalized);
+ seen.add(normalized);
+
+ if (unique.length >= MAX_SELECTED_LANGUAGES) {
+ break;
+ }
+ }
+
+ return unique;
+}
+
+export function getLanguageDistribution(
+ languages?: RepoLanguages,
+): Record {
+ const edges = languages?.edges ?? [];
+ if (edges.length === 0) {
+ return {};
+ }
+
+ let totalSize = 0;
+ for (const edge of edges) {
+ totalSize += Math.max(0, edge.size);
+ }
+
+ if (totalSize <= 0) {
+ return {};
+ }
+
+ const distribution: Record = {};
+ for (const edge of edges) {
+ const normalizedName = normalizeLanguageName(edge.node.name);
+ if (!normalizedName) {
+ continue;
+ }
+ const ratio = Math.max(0, edge.size) / totalSize;
+ distribution[normalizedName] = (distribution[normalizedName] ?? 0) + ratio;
+ }
+
+ return distribution;
+}
+
+export function getLanguageMatch(
+ languages: RepoLanguages | undefined,
+ selectedLanguages: string[],
+): number {
+ if (selectedLanguages.length === 0) {
+ return 1;
+ }
+
+ const distribution = getLanguageDistribution(languages);
+ const distributionKeys = Object.keys(distribution);
+ if (distributionKeys.length === 0) {
+ return 0.5;
+ }
+
+ let match = 0;
+ for (const selectedLanguage of selectedLanguages) {
+ match += distribution[selectedLanguage] ?? 0;
+ }
+
+ return Math.max(0, Math.min(1, match));
+}
+
+export function getLanguageFactor(
+ languageMatch: number,
+ minFactor = 0.25,
+): number {
+ const boundedMatch = Math.max(0, Math.min(1, languageMatch));
+ const boundedMinFactor = Math.max(0, Math.min(1, minFactor));
+ return boundedMinFactor + (1 - boundedMinFactor) * boundedMatch;
+}
+
+export function getTopLanguages(
+ languages?: RepoLanguages,
+ limit = 3,
+): { name: string; percentage: number }[] {
+ const edges = [...(languages?.edges ?? [])]
+ .map((edge) => ({
+ name: edge.node.name,
+ size: Math.max(0, edge.size),
+ }))
+ .sort((a, b) => b.size - a.size);
+
+ if (edges.length === 0 || limit <= 0) {
+ return [];
+ }
+
+ const totalSize = edges.reduce((sum, edge) => sum + edge.size, 0);
+ if (totalSize <= 0) {
+ return [];
+ }
+
+ return edges.slice(0, limit).map((edge) => ({
+ name: edge.name,
+ percentage: Math.round((edge.size / totalSize) * 100),
+ }));
+}
diff --git a/package.json b/package.json
index 4bf67a3..c39830a 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,9 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
- "lint": "eslint ."
+ "lint": "eslint .",
+ "test": "vitest",
+ "test:watch": "vitest --watch"
},
"dependencies": {
"@octokit/graphql": "^9.0.3",
@@ -31,6 +33,7 @@
"eslint-config-next": "^16.2.2",
"postcss": "^8.5.10",
"tailwindcss": "^3.4.14",
- "typescript": "^6.0.2"
+ "typescript": "^6.0.2",
+ "vitest": "^4.1.5"
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 596c811..2750770 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -72,6 +72,9 @@ importers:
typescript:
specifier: ^6.0.2
version: 6.0.2
+ vitest:
+ specifier: ^4.1.5
+ version: 4.1.5(@types/node@25.6.0)(vite@8.0.11(@types/node@25.6.0)(jiti@1.21.7))
packages:
@@ -150,9 +153,15 @@ packages:
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
engines: {node: '>=6.9.0'}
+ '@emnapi/core@1.10.0':
+ resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==}
+
'@emnapi/core@1.9.2':
resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==}
+ '@emnapi/runtime@1.10.0':
+ resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==}
+
'@emnapi/runtime@1.9.2':
resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==}
@@ -258,89 +267,105 @@ packages:
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-arm@1.2.4':
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
cpu: [arm]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-ppc64@1.2.4':
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-riscv64@1.2.4':
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.2.4':
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-x64@1.2.4':
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@img/sharp-linux-arm64@0.34.5':
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-arm@0.34.5':
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-ppc64@0.34.5':
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-riscv64@0.34.5':
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-s390x@0.34.5':
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-x64@0.34.5':
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.34.5':
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@img/sharp-linuxmusl-x64@0.34.5':
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@img/sharp-wasm32@0.34.5':
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
@@ -384,6 +409,12 @@ packages:
'@napi-rs/wasm-runtime@0.2.12':
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
+ '@napi-rs/wasm-runtime@1.1.4':
+ resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==}
+ peerDependencies:
+ '@emnapi/core': ^1.7.1
+ '@emnapi/runtime': ^1.7.1
+
'@next/env@16.2.3':
resolution: {integrity: sha512-ZWXyj4uNu4GCWQw9cjRxWlbD+33mcDszIo9iQxFnBX3Wmgq9ulaSJcl6VhuWx5pCWqqD+9W6Wfz7N0lM5lYPMA==}
@@ -407,24 +438,28 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@next/swc-linux-arm64-musl@16.2.3':
resolution: {integrity: sha512-/YV0LgjHUmfhQpn9bVoGc4x4nan64pkhWR5wyEV8yCOfwwrH630KpvRg86olQHTwHIn1z59uh6JwKvHq1h4QEw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@next/swc-linux-x64-gnu@16.2.3':
resolution: {integrity: sha512-/HiWEcp+WMZ7VajuiMEFGZ6cg0+aYZPqCJD3YJEfpVWQsKYSjXQG06vJP6F1rdA03COD9Fef4aODs3YxKx+RDQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@next/swc-linux-x64-musl@16.2.3':
resolution: {integrity: sha512-Kt44hGJfZSefebhk/7nIdivoDr3Ugp5+oNz9VvF3GUtfxutucUIHfIO0ZYO8QlOPDQloUVQn4NVC/9JvHRk9hw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@next/swc-win32-arm64-msvc@16.2.3':
resolution: {integrity: sha512-O2NZ9ie3Tq6xj5Z5CSwBT3+aWAMW2PIZ4egUi9MaWLkwaehgtB7YZjPm+UpcNpKOme0IQuqDcor7BsW6QBiQBw==}
@@ -476,6 +511,9 @@ packages:
'@octokit/types@16.0.0':
resolution: {integrity: sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==}
+ '@oxc-project/types@0.128.0':
+ resolution: {integrity: sha512-huv1Y/LzBJkBVHt3OlC7u0zHBW9qXf1FdD7sGmc1rXc2P1mTwHssYv7jyGx5KAACSCH+9B3Bhn6Z9luHRvf7pQ==}
+
'@radix-ui/number@1.1.1':
resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
@@ -1166,15 +1204,119 @@ packages:
'@radix-ui/rect@1.1.1':
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
+ '@rolldown/binding-android-arm64@1.0.0-rc.18':
+ resolution: {integrity: sha512-lIDyUAfD7U3+BWKzdxMbJcsYHuqXqmGz40aeRqvuAm3y5TkJSYTBW2RDrn65DJFPQqVjUAUqq5uz8urzQ8aBdQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [android]
+
+ '@rolldown/binding-darwin-arm64@1.0.0-rc.18':
+ resolution: {integrity: sha512-apJq2ktnGp27nSInMR5Vcj8kY6xJzDAvfdIFlpDcAK/w4cDO58qVoi1YQsES/SKiFNge/6e4CUzgjfHduYqWpQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rolldown/binding-darwin-x64@1.0.0-rc.18':
+ resolution: {integrity: sha512-5Ofot8xbs+pxRHJqm9/9N/4sTQOvdrwEsmPE9pdLEEoAbdZtG6F2LMDfO1sp6ZAtXJuJV/21ew2srq3W8NXB5g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rolldown/binding-freebsd-x64@1.0.0-rc.18':
+ resolution: {integrity: sha512-7h8eeOTT1eyqJyx64BFCnWZpNm486hGWt2sqeLLgDxA0xI1oGZ9H7gK1S85uNGmBhkdPwa/6reTxfFFKvIsebw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.18':
+ resolution: {integrity: sha512-eRcm/HVt9U/JFu5RKAEKwGQYtDCKWLiaH6wOnsSEp6NMBb/3Os8LgHZlNyzMpFVNmiiMFlfb2zEnebfzJrHFmg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.18':
+ resolution: {integrity: sha512-SOrT/cT4ukTmgnrEz/Hg3m7LBnuCLW9psDeMKrimRWY4I8DmnO7Lco8W2vtqPmMkbVu8iJ+g4GFLVLLOVjJ9DQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-arm64-musl@1.0.0-rc.18':
+ resolution: {integrity: sha512-QWjdxN1HJCpBTAcZ5N5F7wju3gVPzRzSpmGzx7na0c/1qpN9CFil+xt+l9lV/1M6/gqHSNXCiqPfwhVJPeLnug==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.18':
+ resolution: {integrity: sha512-ugCOyj7a4d9h3q9B+wXmf6g3a68UsjGh6dob5DHevHGMwDUbhsYNbSPxJsENcIttJZ9jv7qGM2UesLw5jqIhdg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.18':
+ resolution: {integrity: sha512-kKWRhbsotpXkGbcd5dllUWg5gEXcDAa8u5YnP9AV5DYNbvJHGzzuwv7dpmhc8NqKMJldl0a+x76IHbspEpEmdA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [s390x]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-x64-gnu@1.0.0-rc.18':
+ resolution: {integrity: sha512-uCo8ElcCIAMyYAZyuIZ81oFkhTSIllNvUCHCAlbhlN4ji3uC28h7IIdlXyIvGO7HsuqnV9p3rD/bpH7XhIyhRw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-x64-musl@1.0.0-rc.18':
+ resolution: {integrity: sha512-XNOQZtuE6yUIvx4rwGemwh8kpL1xvU41FXy/s9K7T/3JVcqGzo3NfKM2HrbrGgfPYGFW42f07Wk++aOC6B9NWA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@rolldown/binding-openharmony-arm64@1.0.0-rc.18':
+ resolution: {integrity: sha512-tSn/kzrfa7tNOXr7sEacDBN4YsIqTyLqh45IO0nHDwtpKIDNDJr+VFojt+4klSpChxB29JLyduSsE0MKEwa65A==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@rolldown/binding-wasm32-wasi@1.0.0-rc.18':
+ resolution: {integrity: sha512-+J9YGmc+czgqlhYmwun3S3O0FIZhsH8ep2456xwjAdIOmuJxM7xz4P4PtrxU+Bz17a/5bqPA8o3HAAoX0teUdg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [wasm32]
+
+ '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.18':
+ resolution: {integrity: sha512-zsu47DgU0FQzSwi6sU9dZoEdUv7pc1AptSEz/Z8HBg54sV0Pbs3N0+CrIbTsgiu6EyoaNN9CHboqbLaz9lhOyQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rolldown/binding-win32-x64-msvc@1.0.0-rc.18':
+ resolution: {integrity: sha512-7H+3yqGgmnlDTRRhw/xpYY9J1kf4GC681nVc4GqKhExZTDrVVrV2tsOR9kso0fvgBdcTCcQShx4SLLoHgaLwhg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [win32]
+
+ '@rolldown/pluginutils@1.0.0-rc.18':
+ resolution: {integrity: sha512-CUY5Mnhe64xQBGZEEXQ5WyZwsc1JU3vAZLIxtrsBt3LO6UOb+C8GunVKqe9sT8NeWb4lqSaoJtp2xo6GxT1MNw==}
+
'@rtsao/scc@1.1.0':
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
+ '@standard-schema/spec@1.1.0':
+ resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
+
'@swc/helpers@0.5.15':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
'@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
+ '@types/chai@5.2.3':
+ resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
+
'@types/d3-array@3.2.2':
resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==}
@@ -1202,6 +1344,9 @@ packages:
'@types/d3-timer@3.0.2':
resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
+ '@types/deep-eql@4.0.2':
+ resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
+
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
@@ -1320,41 +1465,49 @@ packages:
resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@unrs/resolver-binding-linux-arm64-musl@1.11.1':
resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
cpu: [riscv64]
os: [linux]
+ libc: [musl]
'@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@unrs/resolver-binding-linux-x64-gnu@1.11.1':
resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@unrs/resolver-binding-linux-x64-musl@1.11.1':
resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@unrs/resolver-binding-wasm32-wasi@1.11.1':
resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
@@ -1376,6 +1529,35 @@ packages:
cpu: [x64]
os: [win32]
+ '@vitest/expect@4.1.5':
+ resolution: {integrity: sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==}
+
+ '@vitest/mocker@4.1.5':
+ resolution: {integrity: sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==}
+ peerDependencies:
+ msw: ^2.4.9
+ vite: ^6.0.0 || ^7.0.0 || ^8.0.0
+ peerDependenciesMeta:
+ msw:
+ optional: true
+ vite:
+ optional: true
+
+ '@vitest/pretty-format@4.1.5':
+ resolution: {integrity: sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==}
+
+ '@vitest/runner@4.1.5':
+ resolution: {integrity: sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==}
+
+ '@vitest/snapshot@4.1.5':
+ resolution: {integrity: sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==}
+
+ '@vitest/spy@4.1.5':
+ resolution: {integrity: sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==}
+
+ '@vitest/utils@4.1.5':
+ resolution: {integrity: sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==}
+
acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
@@ -1446,6 +1628,10 @@ packages:
resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==}
engines: {node: '>= 0.4'}
+ assertion-error@2.0.1:
+ resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
+ engines: {node: '>=12'}
+
ast-types-flow@0.0.8:
resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==}
@@ -1527,6 +1713,10 @@ packages:
caniuse-lite@1.0.30001787:
resolution: {integrity: sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==}
+ chai@6.2.2:
+ resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
+ engines: {node: '>=18'}
+
chalk@4.1.2:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'}
@@ -1710,6 +1900,9 @@ packages:
resolution: {integrity: sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw==}
engines: {node: '>= 0.4'}
+ es-module-lexer@2.1.0:
+ resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==}
+
es-object-atoms@1.1.1:
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
engines: {node: '>= 0.4'}
@@ -1850,6 +2043,9 @@ packages:
resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
engines: {node: '>=4.0'}
+ estree-walker@3.0.3:
+ resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
+
esutils@2.0.3:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}
@@ -1857,6 +2053,10 @@ packages:
eventemitter3@4.0.7:
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
+ expect-type@1.3.0:
+ resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
+ engines: {node: '>=12.0.0'}
+
fast-content-type-parse@3.0.0:
resolution: {integrity: sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==}
@@ -2214,6 +2414,80 @@ packages:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
+ lightningcss-android-arm64@1.32.0:
+ resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [android]
+
+ lightningcss-darwin-arm64@1.32.0:
+ resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [darwin]
+
+ lightningcss-darwin-x64@1.32.0:
+ resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ lightningcss-freebsd-x64@1.32.0:
+ resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [freebsd]
+
+ lightningcss-linux-arm-gnueabihf@1.32.0:
+ resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm]
+ os: [linux]
+
+ lightningcss-linux-arm64-gnu@1.32.0:
+ resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ lightningcss-linux-arm64-musl@1.32.0:
+ resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ lightningcss-linux-x64-gnu@1.32.0:
+ resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ lightningcss-linux-x64-musl@1.32.0:
+ resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ lightningcss-win32-arm64-msvc@1.32.0:
+ resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [win32]
+
+ lightningcss-win32-x64-msvc@1.32.0:
+ resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [win32]
+
+ lightningcss@1.32.0:
+ resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
+ engines: {node: '>= 12.0.0'}
+
lilconfig@3.1.3:
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
engines: {node: '>=14'}
@@ -2243,6 +2517,9 @@ packages:
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ magic-string@0.30.21:
+ resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
+
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
@@ -2358,6 +2635,9 @@ packages:
resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
engines: {node: '>= 0.4'}
+ obug@2.1.1:
+ resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==}
+
optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
@@ -2389,6 +2669,9 @@ packages:
path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
+ pathe@2.0.3:
+ resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
+
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -2463,6 +2746,10 @@ packages:
resolution: {integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==}
engines: {node: ^10 || ^12 || >=14}
+ postcss@8.5.14:
+ resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==}
+ engines: {node: ^10 || ^12 || >=14}
+
prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
@@ -2598,6 +2885,11 @@ packages:
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
+ rolldown@1.0.0-rc.18:
+ resolution: {integrity: sha512-phmyKBpuBdRYDf4hgyynGAYn/rDDe+iZXKVJ7WX5b1zQzpLkP5oJRPGsfJuHdzPMlyyEO/4sPW6yfSx2gf7lVg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+
run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
@@ -2665,6 +2957,9 @@ packages:
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
engines: {node: '>= 0.4'}
+ siginfo@2.0.0:
+ resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
+
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
@@ -2672,6 +2967,12 @@ packages:
stable-hash@0.0.5:
resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==}
+ stackback@0.0.2:
+ resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
+
+ std-env@4.1.0:
+ resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==}
+
stop-iteration-iterator@1.1.0:
resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
engines: {node: '>= 0.4'}
@@ -2751,10 +3052,21 @@ packages:
tiny-invariant@1.3.3:
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
+ tinybench@2.9.0:
+ resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
+
+ tinyexec@1.1.2:
+ resolution: {integrity: sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==}
+ engines: {node: '>=18'}
+
tinyglobby@0.2.16:
resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==}
engines: {node: '>=12.0.0'}
+ tinyrainbow@3.1.0:
+ resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==}
+ engines: {node: '>=14.0.0'}
+
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
@@ -2859,6 +3171,90 @@ packages:
victory-vendor@36.9.2:
resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==}
+ vite@8.0.11:
+ resolution: {integrity: sha512-Jz1mxtUBR5xTT65VOdJZUUeoyLtqljmFkiUXhPTLZka3RDc9vpi/xXkyrnsdRcm2lIi3l3GPMnAidTsEGIj3Ow==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^20.19.0 || >=22.12.0
+ '@vitejs/devtools': ^0.1.18
+ esbuild: ^0.27.0 || ^0.28.0
+ jiti: '>=1.21.0'
+ less: ^4.0.0
+ sass: ^1.70.0
+ sass-embedded: ^1.70.0
+ stylus: '>=0.54.8'
+ sugarss: ^5.0.0
+ terser: ^5.16.0
+ tsx: ^4.8.1
+ yaml: ^2.4.2
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ '@vitejs/devtools':
+ optional: true
+ esbuild:
+ optional: true
+ jiti:
+ optional: true
+ less:
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+ tsx:
+ optional: true
+ yaml:
+ optional: true
+
+ vitest@4.1.5:
+ resolution: {integrity: sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==}
+ engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
+ hasBin: true
+ peerDependencies:
+ '@edge-runtime/vm': '*'
+ '@opentelemetry/api': ^1.9.0
+ '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0
+ '@vitest/browser-playwright': 4.1.5
+ '@vitest/browser-preview': 4.1.5
+ '@vitest/browser-webdriverio': 4.1.5
+ '@vitest/coverage-istanbul': 4.1.5
+ '@vitest/coverage-v8': 4.1.5
+ '@vitest/ui': 4.1.5
+ happy-dom: '*'
+ jsdom: '*'
+ vite: ^6.0.0 || ^7.0.0 || ^8.0.0
+ peerDependenciesMeta:
+ '@edge-runtime/vm':
+ optional: true
+ '@opentelemetry/api':
+ optional: true
+ '@types/node':
+ optional: true
+ '@vitest/browser-playwright':
+ optional: true
+ '@vitest/browser-preview':
+ optional: true
+ '@vitest/browser-webdriverio':
+ optional: true
+ '@vitest/coverage-istanbul':
+ optional: true
+ '@vitest/coverage-v8':
+ optional: true
+ '@vitest/ui':
+ optional: true
+ happy-dom:
+ optional: true
+ jsdom:
+ optional: true
+
which-boxed-primitive@1.1.1:
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
engines: {node: '>= 0.4'}
@@ -2880,6 +3276,11 @@ packages:
engines: {node: '>= 8'}
hasBin: true
+ why-is-node-running@2.3.0:
+ resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
+ engines: {node: '>=8'}
+ hasBin: true
+
word-wrap@1.2.5:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'}
@@ -3006,12 +3407,23 @@ snapshots:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.28.5
+ '@emnapi/core@1.10.0':
+ dependencies:
+ '@emnapi/wasi-threads': 1.2.1
+ tslib: 2.8.1
+ optional: true
+
'@emnapi/core@1.9.2':
dependencies:
'@emnapi/wasi-threads': 1.2.1
tslib: 2.8.1
optional: true
+ '@emnapi/runtime@1.10.0':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
'@emnapi/runtime@1.9.2':
dependencies:
tslib: 2.8.1
@@ -3219,6 +3631,13 @@ snapshots:
'@tybys/wasm-util': 0.10.1
optional: true
+ '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)':
+ dependencies:
+ '@emnapi/core': 1.10.0
+ '@emnapi/runtime': 1.10.0
+ '@tybys/wasm-util': 0.10.1
+ optional: true
+
'@next/env@16.2.3': {}
'@next/eslint-plugin-next@16.2.3':
@@ -3293,6 +3712,8 @@ snapshots:
dependencies:
'@octokit/openapi-types': 27.0.0
+ '@oxc-project/types@0.128.0': {}
+
'@radix-ui/number@1.1.1': {}
'@radix-ui/primitive@1.1.3': {}
@@ -4040,8 +4461,61 @@ snapshots:
'@radix-ui/rect@1.1.1': {}
+ '@rolldown/binding-android-arm64@1.0.0-rc.18':
+ optional: true
+
+ '@rolldown/binding-darwin-arm64@1.0.0-rc.18':
+ optional: true
+
+ '@rolldown/binding-darwin-x64@1.0.0-rc.18':
+ optional: true
+
+ '@rolldown/binding-freebsd-x64@1.0.0-rc.18':
+ optional: true
+
+ '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.18':
+ optional: true
+
+ '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.18':
+ optional: true
+
+ '@rolldown/binding-linux-arm64-musl@1.0.0-rc.18':
+ optional: true
+
+ '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.18':
+ optional: true
+
+ '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.18':
+ optional: true
+
+ '@rolldown/binding-linux-x64-gnu@1.0.0-rc.18':
+ optional: true
+
+ '@rolldown/binding-linux-x64-musl@1.0.0-rc.18':
+ optional: true
+
+ '@rolldown/binding-openharmony-arm64@1.0.0-rc.18':
+ optional: true
+
+ '@rolldown/binding-wasm32-wasi@1.0.0-rc.18':
+ dependencies:
+ '@emnapi/core': 1.10.0
+ '@emnapi/runtime': 1.10.0
+ '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)
+ optional: true
+
+ '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.18':
+ optional: true
+
+ '@rolldown/binding-win32-x64-msvc@1.0.0-rc.18':
+ optional: true
+
+ '@rolldown/pluginutils@1.0.0-rc.18': {}
+
'@rtsao/scc@1.1.0': {}
+ '@standard-schema/spec@1.1.0': {}
+
'@swc/helpers@0.5.15':
dependencies:
tslib: 2.8.1
@@ -4051,6 +4525,11 @@ snapshots:
tslib: 2.8.1
optional: true
+ '@types/chai@5.2.3':
+ dependencies:
+ '@types/deep-eql': 4.0.2
+ assertion-error: 2.0.1
+
'@types/d3-array@3.2.2': {}
'@types/d3-color@3.1.3': {}
@@ -4075,6 +4554,8 @@ snapshots:
'@types/d3-timer@3.0.2': {}
+ '@types/deep-eql@4.0.2': {}
+
'@types/estree@1.0.8': {}
'@types/json-schema@7.0.15': {}
@@ -4243,6 +4724,47 @@ snapshots:
'@unrs/resolver-binding-win32-x64-msvc@1.11.1':
optional: true
+ '@vitest/expect@4.1.5':
+ dependencies:
+ '@standard-schema/spec': 1.1.0
+ '@types/chai': 5.2.3
+ '@vitest/spy': 4.1.5
+ '@vitest/utils': 4.1.5
+ chai: 6.2.2
+ tinyrainbow: 3.1.0
+
+ '@vitest/mocker@4.1.5(vite@8.0.11(@types/node@25.6.0)(jiti@1.21.7))':
+ dependencies:
+ '@vitest/spy': 4.1.5
+ estree-walker: 3.0.3
+ magic-string: 0.30.21
+ optionalDependencies:
+ vite: 8.0.11(@types/node@25.6.0)(jiti@1.21.7)
+
+ '@vitest/pretty-format@4.1.5':
+ dependencies:
+ tinyrainbow: 3.1.0
+
+ '@vitest/runner@4.1.5':
+ dependencies:
+ '@vitest/utils': 4.1.5
+ pathe: 2.0.3
+
+ '@vitest/snapshot@4.1.5':
+ dependencies:
+ '@vitest/pretty-format': 4.1.5
+ '@vitest/utils': 4.1.5
+ magic-string: 0.30.21
+ pathe: 2.0.3
+
+ '@vitest/spy@4.1.5': {}
+
+ '@vitest/utils@4.1.5':
+ dependencies:
+ '@vitest/pretty-format': 4.1.5
+ convert-source-map: 2.0.0
+ tinyrainbow: 3.1.0
+
acorn-jsx@5.3.2(acorn@8.16.0):
dependencies:
acorn: 8.16.0
@@ -4344,6 +4866,8 @@ snapshots:
get-intrinsic: 1.3.0
is-array-buffer: 3.0.5
+ assertion-error@2.0.1: {}
+
ast-types-flow@0.0.8: {}
async-function@1.0.0: {}
@@ -4417,6 +4941,8 @@ snapshots:
caniuse-lite@1.0.30001787: {}
+ chai@6.2.2: {}
+
chalk@4.1.2:
dependencies:
ansi-styles: 4.3.0
@@ -4546,8 +5072,7 @@ snapshots:
has-property-descriptors: 1.0.2
object-keys: 1.1.1
- detect-libc@2.1.2:
- optional: true
+ detect-libc@2.1.2: {}
detect-node-es@1.1.0: {}
@@ -4654,6 +5179,8 @@ snapshots:
iterator.prototype: 1.1.5
math-intrinsics: 1.1.0
+ es-module-lexer@2.1.0: {}
+
es-object-atoms@1.1.1:
dependencies:
es-errors: 1.3.0
@@ -4882,10 +5409,16 @@ snapshots:
estraverse@5.3.0: {}
+ estree-walker@3.0.3:
+ dependencies:
+ '@types/estree': 1.0.8
+
esutils@2.0.3: {}
eventemitter3@4.0.7: {}
+ expect-type@1.3.0: {}
+
fast-content-type-parse@3.0.0: {}
fast-deep-equal@3.1.3: {}
@@ -5237,6 +5770,55 @@ snapshots:
prelude-ls: 1.2.1
type-check: 0.4.0
+ lightningcss-android-arm64@1.32.0:
+ optional: true
+
+ lightningcss-darwin-arm64@1.32.0:
+ optional: true
+
+ lightningcss-darwin-x64@1.32.0:
+ optional: true
+
+ lightningcss-freebsd-x64@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm-gnueabihf@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm64-gnu@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm64-musl@1.32.0:
+ optional: true
+
+ lightningcss-linux-x64-gnu@1.32.0:
+ optional: true
+
+ lightningcss-linux-x64-musl@1.32.0:
+ optional: true
+
+ lightningcss-win32-arm64-msvc@1.32.0:
+ optional: true
+
+ lightningcss-win32-x64-msvc@1.32.0:
+ optional: true
+
+ lightningcss@1.32.0:
+ dependencies:
+ detect-libc: 2.1.2
+ optionalDependencies:
+ lightningcss-android-arm64: 1.32.0
+ lightningcss-darwin-arm64: 1.32.0
+ lightningcss-darwin-x64: 1.32.0
+ lightningcss-freebsd-x64: 1.32.0
+ lightningcss-linux-arm-gnueabihf: 1.32.0
+ lightningcss-linux-arm64-gnu: 1.32.0
+ lightningcss-linux-arm64-musl: 1.32.0
+ lightningcss-linux-x64-gnu: 1.32.0
+ lightningcss-linux-x64-musl: 1.32.0
+ lightningcss-win32-arm64-msvc: 1.32.0
+ lightningcss-win32-x64-msvc: 1.32.0
+
lilconfig@3.1.3: {}
lines-and-columns@1.2.4: {}
@@ -5261,6 +5843,10 @@ snapshots:
dependencies:
react: 19.2.5
+ magic-string@0.30.21:
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+
math-intrinsics@1.1.0: {}
merge2@1.4.1: {}
@@ -5378,6 +5964,8 @@ snapshots:
define-properties: 1.2.1
es-object-atoms: 1.1.1
+ obug@2.1.1: {}
+
optionator@0.9.4:
dependencies:
deep-is: 0.1.4
@@ -5411,6 +5999,8 @@ snapshots:
path-parse@1.0.7: {}
+ pathe@2.0.3: {}
+
picocolors@1.1.1: {}
picomatch@2.3.2: {}
@@ -5466,6 +6056,12 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
+ postcss@8.5.14:
+ dependencies:
+ nanoid: 3.3.11
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
prelude-ls@1.2.1: {}
prop-types@15.8.1:
@@ -5667,6 +6263,27 @@ snapshots:
reusify@1.1.0: {}
+ rolldown@1.0.0-rc.18:
+ dependencies:
+ '@oxc-project/types': 0.128.0
+ '@rolldown/pluginutils': 1.0.0-rc.18
+ optionalDependencies:
+ '@rolldown/binding-android-arm64': 1.0.0-rc.18
+ '@rolldown/binding-darwin-arm64': 1.0.0-rc.18
+ '@rolldown/binding-darwin-x64': 1.0.0-rc.18
+ '@rolldown/binding-freebsd-x64': 1.0.0-rc.18
+ '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.18
+ '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.18
+ '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.18
+ '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.18
+ '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.18
+ '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.18
+ '@rolldown/binding-linux-x64-musl': 1.0.0-rc.18
+ '@rolldown/binding-openharmony-arm64': 1.0.0-rc.18
+ '@rolldown/binding-wasm32-wasi': 1.0.0-rc.18
+ '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.18
+ '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.18
+
run-parallel@1.2.0:
dependencies:
queue-microtask: 1.2.3
@@ -5784,10 +6401,16 @@ snapshots:
side-channel-map: 1.0.1
side-channel-weakmap: 1.0.2
+ siginfo@2.0.0: {}
+
source-map-js@1.2.1: {}
stable-hash@0.0.5: {}
+ stackback@0.0.2: {}
+
+ std-env@4.1.0: {}
+
stop-iteration-iterator@1.1.0:
dependencies:
es-errors: 1.3.0
@@ -5910,11 +6533,17 @@ snapshots:
tiny-invariant@1.3.3: {}
+ tinybench@2.9.0: {}
+
+ tinyexec@1.1.2: {}
+
tinyglobby@0.2.16:
dependencies:
fdir: 6.5.0(picomatch@4.0.4)
picomatch: 4.0.4
+ tinyrainbow@3.1.0: {}
+
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
@@ -6067,6 +6696,45 @@ snapshots:
d3-time: 3.1.0
d3-timer: 3.0.1
+ vite@8.0.11(@types/node@25.6.0)(jiti@1.21.7):
+ dependencies:
+ lightningcss: 1.32.0
+ picomatch: 4.0.4
+ postcss: 8.5.14
+ rolldown: 1.0.0-rc.18
+ tinyglobby: 0.2.16
+ optionalDependencies:
+ '@types/node': 25.6.0
+ fsevents: 2.3.3
+ jiti: 1.21.7
+
+ vitest@4.1.5(@types/node@25.6.0)(vite@8.0.11(@types/node@25.6.0)(jiti@1.21.7)):
+ dependencies:
+ '@vitest/expect': 4.1.5
+ '@vitest/mocker': 4.1.5(vite@8.0.11(@types/node@25.6.0)(jiti@1.21.7))
+ '@vitest/pretty-format': 4.1.5
+ '@vitest/runner': 4.1.5
+ '@vitest/snapshot': 4.1.5
+ '@vitest/spy': 4.1.5
+ '@vitest/utils': 4.1.5
+ es-module-lexer: 2.1.0
+ expect-type: 1.3.0
+ magic-string: 0.30.21
+ obug: 2.1.1
+ pathe: 2.0.3
+ picomatch: 4.0.4
+ std-env: 4.1.0
+ tinybench: 2.9.0
+ tinyexec: 1.1.2
+ tinyglobby: 0.2.16
+ tinyrainbow: 3.1.0
+ vite: 8.0.11(@types/node@25.6.0)(jiti@1.21.7)
+ why-is-node-running: 2.3.0
+ optionalDependencies:
+ '@types/node': 25.6.0
+ transitivePeerDependencies:
+ - msw
+
which-boxed-primitive@1.1.1:
dependencies:
is-bigint: 1.1.0
@@ -6112,6 +6780,11 @@ snapshots:
dependencies:
isexe: 2.0.0
+ why-is-node-running@2.3.0:
+ dependencies:
+ siginfo: 2.0.0
+ stackback: 0.0.2
+
word-wrap@1.2.5: {}
yallist@3.1.1: {}
diff --git a/public/screenshots/screenshot.png b/public/screenshots/screenshot.png
index 3e798659fcefd9801701ebbd2ddc8005ca61340d..4f0939a567e9af5261ad7fbdc819d4647e16d4cd 100644
GIT binary patch
literal 1673976
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