From bcc7c875599ef3ffd38b3cd8479a2974e566e1fc Mon Sep 17 00:00:00 2001 From: kelvinchen03 Date: Wed, 20 May 2026 02:19:42 -0400 Subject: [PATCH 1/2] feat: add Ecosystem Map page for repo emission and language visualization --- src/components/layout/Sidebar.tsx | 2 + src/pages/EcosystemPage.tsx | 450 ++++++++++++++++++++++++++++++ src/routes.tsx | 6 + 3 files changed, 458 insertions(+) create mode 100644 src/pages/EcosystemPage.tsx diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 9e0b0c45..c861924a 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -15,6 +15,7 @@ import VisibilityIcon from '@mui/icons-material/Visibility'; import BugReportIcon from '@mui/icons-material/BugReport'; import FolderCopyIcon from '@mui/icons-material/FolderCopy'; import SchoolIcon from '@mui/icons-material/School'; +import PublicIcon from '@mui/icons-material/Public'; import { useLinkBehavior } from '../common/linkBehavior'; import { useWatchlistTotalCount } from '../../hooks/useWatchlist'; @@ -85,6 +86,7 @@ const Sidebar: React.FC = ({ onNavigate, collapsed = false }) => { const navItems = [ { label: 'dashboard', path: '/dashboard', icon: }, { label: 'repositories', path: '/repositories', icon: }, + { label: 'ecosystem', path: '/ecosystem', icon: }, { label: 'watchlist', path: '/watchlist', diff --git a/src/pages/EcosystemPage.tsx b/src/pages/EcosystemPage.tsx new file mode 100644 index 00000000..4166c860 --- /dev/null +++ b/src/pages/EcosystemPage.tsx @@ -0,0 +1,450 @@ +import React, { useMemo } from 'react'; +import { Box, Card, Chip, Typography, alpha, type Theme } from '@mui/material'; +import { Page } from '../components/layout'; +import { SEO } from '../components'; +import { LinkBox } from '../components/common/linkBehavior'; +import { useReposAndWeights, useLanguagesAndWeights } from '../api'; +import { getRepositoryOwnerAvatarSrc } from '../utils/avatar'; + +const FONTS = { mono: '"JetBrains Mono", monospace' } as const; +const DEFAULT_ELIGIBILITY_GATE = 80; + +const SectionHeader: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => ( + ({ + fontFamily: FONTS.mono, + fontSize: '0.85rem', + fontWeight: 600, + color: theme.palette.text.primary, + textTransform: 'uppercase', + letterSpacing: '0.05em', + mb: { xs: 1.5, sm: 2 }, + pb: { xs: 1, sm: 1.25 }, + borderBottom: '1px solid', + borderColor: theme.palette.border.light, + })} + > + {children} + +); + +const cardSx = (theme: Theme) => ({ + p: { xs: 2, sm: 3 }, + borderRadius: 2, + border: '1px solid', + borderColor: theme.palette.border.light, + backgroundColor: theme.palette.surface.transparent, + display: 'flex', + flexDirection: 'column' as const, +}); + +const EcosystemPage: React.FC = () => { + const { data: repos, isLoading: isLoadingRepos } = useReposAndWeights(); + const { data: languages, isLoading: isLoadingLanguages } = + useLanguagesAndWeights(); + + const isLoading = isLoadingRepos || isLoadingLanguages; + + // ── Repository Emission Map ──────────────────────────────────────────────── + const repoEmissionData = useMemo(() => { + if (!repos) return []; + + const totalEmission = repos.reduce( + (sum, repo) => + sum + (parseFloat(String(repo.config?.emissionShare ?? 0)) || 0), + 0, + ); + + return repos + .map((repo) => ({ + fullName: repo.fullName, + owner: repo.owner, + emissionShare: parseFloat(String(repo.config?.emissionShare ?? 0)) || 0, + eligibilityGate: + repo.config?.eligibility?.min_credibility ?? DEFAULT_ELIGIBILITY_GATE, + percentageOfTotal: + totalEmission > 0 + ? ((parseFloat(String(repo.config?.emissionShare ?? 0)) || 0) / + totalEmission) * + 100 + : 0, + })) + .sort((a, b) => b.emissionShare - a.emissionShare); + }, [repos]); + + // ── Language Weight Chart ─────────────────────────────────────────────────── + const languageWeightData = useMemo(() => { + if (!languages) return []; + + // Group by language field (null → use extension as key) + const grouped = new Map< + string, + { extensions: Set; weight: number } + >(); + + languages.forEach((lang) => { + const key = lang.language || lang.extension; + const current = grouped.get(key) || { + extensions: new Set(), + weight: 0, + }; + current.extensions.add(lang.extension); + current.weight = Math.max( + current.weight, + parseFloat(String(lang.weight)) || 0, + ); + grouped.set(key, current); + }); + + // Convert to array and sort by weight descending + const result = Array.from(grouped.entries()).map(([name, data]) => ({ + name, + extensions: Array.from(data.extensions), + weight: data.weight, + })); + + const maxWeight = Math.max(...result.map((l) => l.weight), 1); + + return result + .map((item) => ({ + ...item, + weightPercentage: (item.weight / maxWeight) * 100, + })) + .sort((a, b) => b.weight - a.weight); + }, [languages]); + + const getRepoHref = (name: string) => + `/miners/repository?name=${encodeURIComponent(name)}`; + + return ( + + + + {/* ── Repository Emission Map ─────────────────────────────────────── */} + + Repository Emission Map + + {isLoading ? ( + ({ + color: alpha(theme.palette.text.primary, 0.3), + fontSize: '0.85rem', + fontStyle: 'italic', + py: 4, + textAlign: 'center', + })} + > + Loading repository data... + + ) : repoEmissionData.length === 0 ? ( + ({ + color: alpha(theme.palette.text.primary, 0.3), + fontSize: '0.85rem', + fontStyle: 'italic', + py: 4, + textAlign: 'center', + })} + > + No repository data available + + ) : ( + repoEmissionData.map((repo, index) => ( + + {/* Rank */} + + #{index + 1} + + + {/* Repository name with avatar */} + + + ({ + fontFamily: FONTS.mono, + fontSize: '0.85rem', + color: theme.palette.text.primary, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + fontWeight: 500, + })} + > + {repo.fullName} + + + + {/* Eligibility gate */} + ({ + fontFamily: FONTS.mono, + fontSize: '0.75rem', + color: alpha(theme.palette.text.primary, 0.6), + minWidth: { xs: 60, sm: 80 }, + textAlign: 'right', + })} + > + {repo.eligibilityGate}% + + + {/* Emission bar */} + + ({ + flex: 1, + height: 6, + borderRadius: 3, + backgroundColor: alpha(theme.palette.status.info, 0.15), + overflow: 'hidden', + })} + > + ({ + width: `${repo.percentageOfTotal}%`, + height: '100%', + backgroundColor: theme.palette.status.info, + borderRadius: 3, + })} + /> + + ({ + fontFamily: FONTS.mono, + fontSize: '0.75rem', + color: theme.palette.text.primary, + fontWeight: 600, + minWidth: { xs: 70, sm: 90 }, + textAlign: 'right', + })} + > + {repo.emissionShare.toFixed(4)} ( + {repo.percentageOfTotal.toFixed(1)}%) + + + + )) + )} + + + + {/* ── Language Weight Chart ─────────────────────────────────────────── */} + + Language Weight Chart + + {isLoading ? ( + ({ + color: alpha(theme.palette.text.primary, 0.3), + fontSize: '0.85rem', + fontStyle: 'italic', + py: 4, + textAlign: 'center', + })} + > + Loading language data... + + ) : languageWeightData.length === 0 ? ( + ({ + color: alpha(theme.palette.text.primary, 0.3), + fontSize: '0.85rem', + fontStyle: 'italic', + py: 4, + textAlign: 'center', + })} + > + No language data available + + ) : ( + languageWeightData.map((lang, index) => ( + + {/* Rank */} + + #{index + 1} + + + {/* Language name */} + ({ + fontFamily: FONTS.mono, + fontSize: '0.85rem', + color: theme.palette.text.primary, + fontWeight: 500, + minWidth: { xs: 80, sm: 120 }, + })} + > + {lang.name} + + + {/* Extension chips */} + + {lang.extensions.map((ext) => ( + + ))} + + + {/* Weight bar */} + + ({ + flex: 1, + height: 6, + borderRadius: 3, + backgroundColor: alpha( + theme.palette.status.success, + 0.15, + ), + overflow: 'hidden', + })} + > + ({ + width: `${lang.weightPercentage}%`, + height: '100%', + backgroundColor: theme.palette.status.success, + borderRadius: 3, + })} + /> + + ({ + fontFamily: FONTS.mono, + fontSize: '0.75rem', + color: theme.palette.text.primary, + fontWeight: 600, + minWidth: { xs: 60, sm: 80 }, + textAlign: 'right', + })} + > + {lang.weight.toFixed(4)} + + + + )) + )} + + + + + ); +}; + +export default EcosystemPage; diff --git a/src/routes.tsx b/src/routes.tsx index 3f59589e..4d079642 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -28,6 +28,7 @@ const WatchlistPage = React.lazy(() => import('./pages/WatchlistPage')); const RepositoryRegistrationPage = React.lazy( () => import('./pages/RepositoryRegistrationPage'), ); +const EcosystemPage = React.lazy(() => import('./pages/EcosystemPage')); // 404 page const NotFoundPage = React.lazy(() => import('./pages/NotFoundPage')); @@ -109,6 +110,11 @@ const routesArray: AppRoute[] = [ path: '/repository-registration', element: , }, + { + name: 'ecosystem', + path: '/ecosystem', + element: , + }, // 404 catch-all route (must be last) { From d17c27b2913aad87446300283891dd1f5cf7437f Mon Sep 17 00:00:00 2001 From: kelvinchen03 Date: Wed, 20 May 2026 05:44:33 -0400 Subject: [PATCH 2/2] feat: improve Ecosystem Map page for repo emission and language visualization --- src/pages/EcosystemPage.tsx | 920 +++++++++++++++++++++++++++--------- 1 file changed, 696 insertions(+), 224 deletions(-) diff --git a/src/pages/EcosystemPage.tsx b/src/pages/EcosystemPage.tsx index 4166c860..76742854 100644 --- a/src/pages/EcosystemPage.tsx +++ b/src/pages/EcosystemPage.tsx @@ -1,5 +1,22 @@ -import React, { useMemo } from 'react'; -import { Box, Card, Chip, Typography, alpha, type Theme } from '@mui/material'; +import React, { useMemo, useState } from 'react'; +import { + Box, + Card, + Chip, + Typography, + alpha, + type Theme, + TextField, + Stack, + Select, + MenuItem, + Button, +} from '@mui/material'; +import ArrowUpward from '@mui/icons-material/ArrowUpward'; +import ArrowDownward from '@mui/icons-material/ArrowDownward'; +import SearchIcon from '@mui/icons-material/Search'; +import ChevronLeft from '@mui/icons-material/ChevronLeft'; +import ChevronRight from '@mui/icons-material/ChevronRight'; import { Page } from '../components/layout'; import { SEO } from '../components'; import { LinkBox } from '../components/common/linkBehavior'; @@ -35,11 +52,13 @@ const cardSx = (theme: Theme) => ({ borderRadius: 2, border: '1px solid', borderColor: theme.palette.border.light, - backgroundColor: theme.palette.surface.transparent, + backgroundColor: alpha(theme.palette.background.paper, 0.3), display: 'flex', flexDirection: 'column' as const, }); +type SortDirection = 'asc' | 'desc' | null; + const EcosystemPage: React.FC = () => { const { data: repos, isLoading: isLoadingRepos } = useReposAndWeights(); const { data: languages, isLoading: isLoadingLanguages } = @@ -47,7 +66,35 @@ const EcosystemPage: React.FC = () => { const isLoading = isLoadingRepos || isLoadingLanguages; + // Search states + const [repoSearchQuery, setRepoSearchQuery] = useState(''); + const [languageSearchQuery, setLanguageSearchQuery] = useState(''); + + // Sort states + const [repoSortColumn, setRepoSortColumn] = + useState('emissionShare'); + const [repoSortDirection, setRepoSortDirection] = + useState('desc'); + const [languageSortColumn, setLanguageSortColumn] = + useState('weight'); + const [languageSortDirection, setLanguageSortDirection] = + useState('desc'); + + // Pagination states + const [repoPage, setRepoPage] = useState(0); + const [repoRowsPerPage, setRepoRowsPerPage] = useState(10); + const [languagePage, setLanguagePage] = useState(0); + const [languageRowsPerPage, setLanguageRowsPerPage] = useState(10); + // ── Repository Emission Map ──────────────────────────────────────────────── + type RepoEmissionItem = { + fullName: string; + owner: string; + emissionShare: number; + eligibilityGate: number; + percentageOfTotal: number; + }; + const repoEmissionData = useMemo(() => { if (!repos) return []; @@ -57,24 +104,53 @@ const EcosystemPage: React.FC = () => { 0, ); - return repos - .map((repo) => ({ - fullName: repo.fullName, - owner: repo.owner, - emissionShare: parseFloat(String(repo.config?.emissionShare ?? 0)) || 0, - eligibilityGate: - repo.config?.eligibility?.min_credibility ?? DEFAULT_ELIGIBILITY_GATE, - percentageOfTotal: - totalEmission > 0 - ? ((parseFloat(String(repo.config?.emissionShare ?? 0)) || 0) / - totalEmission) * - 100 - : 0, - })) - .sort((a, b) => b.emissionShare - a.emissionShare); - }, [repos]); + const baseData = repos.map((repo) => ({ + fullName: repo.fullName, + owner: repo.owner, + emissionShare: parseFloat(String(repo.config?.emissionShare ?? 0)) || 0, + eligibilityGate: + repo.config?.eligibility?.min_credibility ?? DEFAULT_ELIGIBILITY_GATE, + percentageOfTotal: + totalEmission > 0 + ? ((parseFloat(String(repo.config?.emissionShare ?? 0)) || 0) / + totalEmission) * + 100 + : 0, + })); + + // Filter by search query + const filtered = repoSearchQuery + ? baseData.filter( + (repo) => + repo.fullName + .toLowerCase() + .includes(repoSearchQuery.toLowerCase()) || + repo.owner.toLowerCase().includes(repoSearchQuery.toLowerCase()), + ) + : baseData; + + // Sort by selected column + const sorted = [...filtered].sort((a, b) => { + const aVal = a[repoSortColumn]; + const bVal = b[repoSortColumn]; + if (repoSortDirection === 'asc') { + return aVal > bVal ? 1 : aVal < bVal ? -1 : 0; + } else { + return aVal < bVal ? 1 : aVal > bVal ? -1 : 0; + } + }); + + return sorted; + }, [repos, repoSearchQuery, repoSortColumn, repoSortDirection]); // ── Language Weight Chart ─────────────────────────────────────────────────── + type LanguageWeightItem = { + name: string; + extensions: string[]; + weight: number; + weightPercentage: number; + }; + const languageWeightData = useMemo(() => { if (!languages) return []; @@ -98,7 +174,7 @@ const EcosystemPage: React.FC = () => { grouped.set(key, current); }); - // Convert to array and sort by weight descending + // Convert to array const result = Array.from(grouped.entries()).map(([name, data]) => ({ name, extensions: Array.from(data.extensions), @@ -107,17 +183,226 @@ const EcosystemPage: React.FC = () => { const maxWeight = Math.max(...result.map((l) => l.weight), 1); - return result - .map((item) => ({ - ...item, - weightPercentage: (item.weight / maxWeight) * 100, - })) - .sort((a, b) => b.weight - a.weight); - }, [languages]); + const baseData = result.map((item) => ({ + ...item, + weightPercentage: (item.weight / maxWeight) * 100, + })); + + // Filter by search query + const filtered = languageSearchQuery + ? baseData.filter( + (lang) => + lang.name + .toLowerCase() + .includes(languageSearchQuery.toLowerCase()) || + lang.extensions.some((ext) => + ext.toLowerCase().includes(languageSearchQuery.toLowerCase()), + ), + ) + : baseData; + + // Sort by selected column + const sorted = [...filtered].sort((a, b) => { + const aVal = a[languageSortColumn]; + const bVal = b[languageSortColumn]; + if (languageSortDirection === 'asc') { + return aVal > bVal ? 1 : aVal < bVal ? -1 : 0; + } else { + return aVal < bVal ? 1 : aVal > bVal ? -1 : 0; + } + }); + + return sorted; + }, [ + languages, + languageSearchQuery, + languageSortColumn, + languageSortDirection, + ]); const getRepoHref = (name: string) => `/miners/repository?name=${encodeURIComponent(name)}`; + const handleRepoSort = (column: keyof RepoEmissionItem) => { + if (repoSortColumn === column) { + setRepoSortDirection( + repoSortDirection === 'asc' + ? 'desc' + : repoSortDirection === 'desc' + ? null + : 'asc', + ); + } else { + setRepoSortColumn(column); + setRepoSortDirection('asc'); + } + }; + + const handleLanguageSort = (column: keyof LanguageWeightItem) => { + if (languageSortColumn === column) { + setLanguageSortDirection( + languageSortDirection === 'asc' + ? 'desc' + : languageSortDirection === 'desc' + ? null + : 'asc', + ); + } else { + setLanguageSortColumn(column); + setLanguageSortDirection('asc'); + } + }; + + const handleRepoPageChange = (newPage: number) => { + setRepoPage(newPage); + }; + + const handleRepoRowsPerPageChange = (value: number) => { + setRepoRowsPerPage(value); + setRepoPage(0); + }; + + const handleLanguagePageChange = (newPage: number) => { + setLanguagePage(newPage); + }; + + const handleLanguageRowsPerPageChange = (value: number) => { + setLanguageRowsPerPage(value); + setLanguagePage(0); + }; + + const SortIcon: React.FC<{ + column: string; + activeColumn: string; + direction: SortDirection; + }> = ({ column, activeColumn, direction }) => { + if (activeColumn !== column || !direction) return null; + return direction === 'asc' ? ( + + ) : ( + + ); + }; + + const SortableHeader: React.FC<{ + children: React.ReactNode; + column: string; + activeColumn: string; + direction: SortDirection; + onSort: () => void; + }> = ({ children, column, activeColumn, direction, onSort }) => { + const baseSx = { + fontFamily: FONTS.mono, + fontSize: '0.75rem', + fontWeight: 600, + color: 'text.secondary', + background: 'none', + border: 'none', + cursor: 'pointer', + display: 'flex', + alignItems: 'center', + p: 0, + '&:hover': { color: 'text.primary' }, + }; + + return ( + + {children} + + + ); + }; + + const Pagination: React.FC<{ + page: number; + rowsPerPage: number; + totalRows: number; + onPageChange: (page: number) => void; + onRowsPerPageChange: (value: number) => void; + }> = ({ + page, + rowsPerPage, + totalRows, + onPageChange, + onRowsPerPageChange, + }) => { + const totalPages = Math.ceil(totalRows / rowsPerPage); + const startRow = page * rowsPerPage + 1; + const endRow = Math.min((page + 1) * rowsPerPage, totalRows); + + return ( + + + {totalRows > 0 ? `${startRow}-${endRow} of ${totalRows}` : '0 of 0'} + + + + + {totalPages > 0 ? page + 1 : 0} / {totalPages} + + + + ); + }; + return ( { {/* ── Repository Emission Map ─────────────────────────────────────── */} Repository Emission Map + + {/* Search bar */} + + setRepoSearchQuery(e.target.value)} + InputProps={{ + startAdornment: ( + + ), + sx: { + fontFamily: FONTS.mono, + fontSize: '0.85rem', + '& .MuiOutlinedInput-notchedOutline': { + borderColor: 'border.light', + }, + '&:hover .MuiOutlinedInput-notchedOutline': { + borderColor: 'border.medium', + }, + }, + }} + sx={(theme) => ({ + backgroundColor: alpha(theme.palette.background.paper, 0.3), + borderRadius: 1, + '& .MuiInputBase-root': { fontFamily: FONTS.mono }, + })} + /> + + + {/* Table header */} + + + handleRepoSort('fullName')} + > + Repository + + + + handleRepoSort('eligibilityGate')} + > + Gate + + + + + + handleRepoSort('emissionShare')} + > + Emission + + + + + {isLoading ? ( @@ -168,141 +548,235 @@ const EcosystemPage: React.FC = () => { No repository data available ) : ( - repoEmissionData.map((repo, index) => ( - - {/* Rank */} - - #{index + 1} - - - {/* Repository name with avatar */} + repoEmissionData + .slice( + repoPage * repoRowsPerPage, + (repoPage + 1) * repoRowsPerPage, + ) + .map((repo, _index) => ( - + > + + ({ + fontFamily: FONTS.mono, + fontSize: '0.85rem', + color: theme.palette.text.primary, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + fontWeight: 500, + })} + > + {repo.fullName} + + + ({ fontFamily: FONTS.mono, - fontSize: '0.85rem', - color: theme.palette.text.primary, - overflow: 'hidden', - textOverflow: 'ellipsis', - whiteSpace: 'nowrap', - fontWeight: 500, + fontSize: '0.75rem', + color: alpha(theme.palette.text.primary, 0.6), + minWidth: { xs: 50, sm: 60 }, + textAlign: 'right', })} > - {repo.fullName} + {repo.eligibilityGate}% - - {/* Eligibility gate */} - ({ - fontFamily: FONTS.mono, - fontSize: '0.75rem', - color: alpha(theme.palette.text.primary, 0.6), - minWidth: { xs: 60, sm: 80 }, - textAlign: 'right', - })} - > - {repo.eligibilityGate}% - - - {/* Emission bar */} - ({ - flex: 1, - height: 6, - borderRadius: 3, - backgroundColor: alpha(theme.palette.status.info, 0.15), - overflow: 'hidden', - })} + sx={{ + display: 'flex', + alignItems: 'center', + gap: 1, + minWidth: { xs: 140, sm: 180 }, + }} > ({ - width: `${repo.percentageOfTotal}%`, - height: '100%', - backgroundColor: theme.palette.status.info, + flex: 1, + height: 6, borderRadius: 3, + backgroundColor: alpha( + theme.palette.status.info, + 0.15, + ), + overflow: 'hidden', })} - /> + > + ({ + width: `${repo.percentageOfTotal}%`, + height: '100%', + backgroundColor: theme.palette.status.info, + borderRadius: 3, + })} + /> + + ({ + fontFamily: FONTS.mono, + fontSize: '0.75rem', + color: theme.palette.text.primary, + fontWeight: 600, + minWidth: { xs: 80, sm: 90 }, + textAlign: 'right', + })} + > + {repo.emissionShare.toFixed(4)} ( + {repo.percentageOfTotal.toFixed(1)}%) + - ({ - fontFamily: FONTS.mono, - fontSize: '0.75rem', - color: theme.palette.text.primary, - fontWeight: 600, - minWidth: { xs: 70, sm: 90 }, - textAlign: 'right', - })} - > - {repo.emissionShare.toFixed(4)} ( - {repo.percentageOfTotal.toFixed(1)}%) - - - )) + )) )} + {/* ── Language Weight Chart ─────────────────────────────────────────── */} - + ({ ...cardSx(theme), mt: 3 })} elevation={0}> Language Weight Chart + + {/* Search bar */} + + setLanguageSearchQuery(e.target.value)} + InputProps={{ + startAdornment: ( + + ), + sx: { + fontFamily: FONTS.mono, + fontSize: '0.85rem', + '& .MuiOutlinedInput-notchedOutline': { + borderColor: 'border.light', + }, + '&:hover .MuiOutlinedInput-notchedOutline': { + borderColor: 'border.medium', + }, + }, + }} + sx={(theme) => ({ + backgroundColor: alpha(theme.palette.background.paper, 0.3), + borderRadius: 1, + '& .MuiInputBase-root': { fontFamily: FONTS.mono }, + })} + /> + + + {/* Table header */} + + + handleLanguageSort('name')} + > + Language + + + + Extensions + + + + + handleLanguageSort('weight')} + > + Weight + + + + + {isLoading ? ( @@ -330,117 +804,115 @@ const EcosystemPage: React.FC = () => { No language data available ) : ( - languageWeightData.map((lang, index) => ( - - {/* Rank */} - - #{index + 1} - - - {/* Language name */} - ({ - fontFamily: FONTS.mono, - fontSize: '0.85rem', - color: theme.palette.text.primary, - fontWeight: 500, - minWidth: { xs: 80, sm: 120 }, - })} - > - {lang.name} - - - {/* Extension chips */} - - {lang.extensions.map((ext) => ( - - ))} - - - {/* Weight bar */} + languageWeightData + .slice( + languagePage * languageRowsPerPage, + (languagePage + 1) * languageRowsPerPage, + ) + .map((lang, _index) => ( - ({ - flex: 1, - height: 6, - borderRadius: 3, - backgroundColor: alpha( - theme.palette.status.success, - 0.15, - ), - overflow: 'hidden', + fontFamily: FONTS.mono, + fontSize: '0.85rem', + color: theme.palette.text.primary, + fontWeight: 500, + minWidth: { xs: 80, sm: 120 }, })} + > + {lang.name} + + + + {lang.extensions.map((ext) => ( + + ))} + + + ({ - width: `${lang.weightPercentage}%`, - height: '100%', - backgroundColor: theme.palette.status.success, + flex: 1, + height: 6, borderRadius: 3, + backgroundColor: alpha( + theme.palette.status.success, + 0.15, + ), + overflow: 'hidden', })} - /> + > + ({ + width: `${lang.weightPercentage}%`, + height: '100%', + backgroundColor: theme.palette.status.success, + borderRadius: 3, + })} + /> + + ({ + fontFamily: FONTS.mono, + fontSize: '0.75rem', + color: theme.palette.text.primary, + fontWeight: 600, + minWidth: { xs: 60, sm: 80 }, + textAlign: 'right', + })} + > + {lang.weight.toFixed(4)} + - ({ - fontFamily: FONTS.mono, - fontSize: '0.75rem', - color: theme.palette.text.primary, - fontWeight: 600, - minWidth: { xs: 60, sm: 80 }, - textAlign: 'right', - })} - > - {lang.weight.toFixed(4)} - - - )) + )) )} +