diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index 00022d6a..811caaf4 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'; -import { Avatar, Box, Stack, Typography } from '@mui/material'; +import { Avatar, Box, Typography } from '@mui/material'; import { alpha } from '@mui/material/styles'; import { Page } from '../components/layout'; import { SEO } from '../components'; @@ -132,50 +132,30 @@ const sortByEmissionShare = (repos: Repository[]) => parseNumber(a.config?.emissionShare ?? 0), ); -const SiteOverlay: React.FC<{ host: string; live: boolean }> = ({ - host, - live, -}) => ( - <> - {live && ( - ({ - position: 'absolute', - top: 10, - right: 10, - width: 6, - height: 6, - borderRadius: '50%', - backgroundColor: theme.palette.status.merged, - zIndex: 1, - })} - /> - )} - ({ - position: 'absolute', - bottom: 6, - right: 8, - maxWidth: 'calc(100% - 16px)', - px: 0.75, - py: 0.25, - borderRadius: 0.75, - backgroundColor: alpha(theme.palette.common.black, 0.55), - color: alpha(theme.palette.common.white, 0.78), - fontFamily: 'var(--font-accent)', - fontSize: '0.58rem', - letterSpacing: '0.06em', - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - zIndex: 1, - pointerEvents: 'none', - })} - > - {host} - - +const SiteOverlay: React.FC<{ host: string }> = ({ host }) => ( + ({ + position: 'absolute', + bottom: 6, + right: 8, + maxWidth: 'calc(100% - 16px)', + px: 0.75, + py: 0.25, + borderRadius: 0.75, + backgroundColor: alpha(theme.palette.common.black, 0.55), + color: alpha(theme.palette.common.white, 0.78), + fontFamily: 'var(--font-accent)', + fontSize: '0.58rem', + letterSpacing: '0.06em', + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + zIndex: 1, + pointerEvents: 'none', + })} + > + {host} + ); const RepoCard: React.FC<{ repo: Repository; index: number }> = ({ @@ -356,7 +336,7 @@ const RepoCard: React.FC<{ repo: Repository; index: number }> = ({ })} > {website && (embedLive || showSiteShot) && ( - + )} {/* Backdrop: website screenshot → GitHub OG card → repo name. The @@ -471,6 +451,7 @@ const RepoCard: React.FC<{ repo: Repository; index: number }> = ({ }} > ({ @@ -478,6 +459,9 @@ const RepoCard: React.FC<{ repo: Repository; index: number }> = ({ height: 26, border: `1px solid ${theme.palette.border.medium}`, flexShrink: 0, + filter: 'grayscale(1)', + opacity: 0.88, + transition: 'filter 0.25s ease, opacity 0.25s ease', })} /> @@ -566,11 +550,70 @@ const RepoCardSkeleton: React.FC<{ index: number }> = ({ index }) => ( const DashboardTimeline = React.lazy(() => import('./dashboard/DashboardPage')); type Timeline = 'repositories' | 'dashboard'; -const TIMELINES: readonly Timeline[] = ['repositories', 'dashboard']; + +// A left/right dial button; grayed out when the dial can't turn that way +// (the word that direction is already showing). +const DialArrow: React.FC<{ + dir: 'left' | 'right'; + active: Timeline; + onTurn: (dir: 'left' | 'right') => void; +}> = ({ dir, active, onTurn }) => { + const target: Timeline = dir === 'right' ? 'dashboard' : 'repositories'; + const enabled = target !== active; + return ( + onTurn(dir)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + onTurn(dir); + } + }} + sx={(theme) => ({ + fontSize: '0.8rem', + lineHeight: 1, + px: 0.5, + cursor: enabled ? 'pointer' : 'default', + color: alpha(theme.palette.text.primary, enabled ? 0.45 : 0.14), + transition: 'color 0.2s ease', + ...(enabled && { + '&:hover': { + color: alpha(theme.palette.text.primary, 0.85), + }, + }), + '&:focus-visible': { + outline: `2px solid ${theme.palette.status.merged}`, + outlineOffset: 2, + borderRadius: 0.75, + }, + })} + > + {dir === 'left' ? '‹' : '›'} + + ); +}; const HomePage: React.FC = () => { const reposQuery = useReposAndWeights(); const [timeline, setTimeline] = useState('repositories'); + // The word leaving the dial; rendered just long enough to slide out in + // the direction of travel, then cleared by onAnimationEnd. + const [leaving, setLeaving] = useState<{ + word: Timeline; + dir: 'left' | 'right'; + } | null>(null); + + const turnDial = (dir: 'left' | 'right') => { + const target: Timeline = dir === 'right' ? 'dashboard' : 'repositories'; + if (target === timeline) return; + setLeaving({ word: timeline, dir }); + setTimeline(target); + }; const repos = useMemo( () => sortByEmissionShare(reposQuery.data ?? []), @@ -611,69 +654,108 @@ const HomePage: React.FC = () => { Gittensor - ({ - mt: { xs: 2.5, md: 3.5 }, + {/* Dial toggle: only the active timeline shows, flanked by arrows. + Turning the dial slides the old word out and the new word in, + both moving in the direction of travel. */} + - {TIMELINES.map((tab) => ( + + {/* The hidden word reserves the width of the longest label so + the window doesn't resize as the dial turns. */} + + + repositories + + {leaving && ( + setLeaving(null)} + sx={(theme) => ({ + position: 'absolute', + inset: 0, + textAlign: 'center', + color: theme.palette.status.merged, + animation: `${ + leaving.dir === 'right' + ? 'timelineDialOutLeft' + : 'timelineDialOutRight' + } 0.3s ease forwards`, + })} + > + {leaving.word} + + )} setTimeline(tab)} + component="span" + key={timeline} sx={(theme) => ({ - px: 2.25, - py: 0.7, - cursor: 'pointer', - fontFamily: 'var(--font-accent)', - fontSize: '0.68rem', - fontWeight: 700, - letterSpacing: '0.1em', - textTransform: 'uppercase', - whiteSpace: 'nowrap', - transition: 'all 0.25s ease', - backgroundColor: - timeline === tab - ? alpha(theme.palette.status.merged, 0.15) - : 'transparent', - color: - timeline === tab - ? theme.palette.status.merged - : alpha(theme.palette.text.primary, 0.4), - '&:hover': { - backgroundColor: - timeline === tab - ? alpha(theme.palette.status.merged, 0.15) - : alpha(theme.palette.text.primary, 0.06), - }, + position: 'absolute', + inset: 0, + textAlign: 'center', + color: theme.palette.status.merged, + animation: leaving + ? `${ + leaving.dir === 'right' + ? 'timelineDialInRight' + : 'timelineDialInLeft' + } 0.3s ease` + : undefined, })} > - {tab} + {timeline} - ))} - + + + {timeline === 'repositories' ? ( <> ({ - mt: { xs: 3, md: 4.5 }, + mt: { xs: 1.25, md: 1.5 }, mx: 'auto', maxWidth: 640, - color: alpha(theme.palette.text.primary, 0.55), + color: alpha(theme.palette.text.primary, 0.45), fontFamily: 'var(--font-accent)', - fontSize: '0.72rem', - letterSpacing: '0.14em', + fontSize: '0.66rem', + letterSpacing: '0.16em', textTransform: 'uppercase', textAlign: 'center', - lineHeight: 1.9, + lineHeight: 1.7, ...fadeUp(120), })} > @@ -682,7 +764,7 @@ const HomePage: React.FC = () => { { )} ) : ( - + ({ - mt: { xs: 1, md: 1.5 }, + mt: { xs: 1.25, md: 1.5 }, mx: 'auto', maxWidth: 640, - color: alpha(theme.palette.text.primary, 0.55), + color: alpha(theme.palette.text.primary, 0.45), fontFamily: 'var(--font-accent)', - fontSize: '0.72rem', - letterSpacing: '0.14em', + fontSize: '0.66rem', + letterSpacing: '0.16em', textTransform: 'uppercase', textAlign: 'center', - lineHeight: 1.9, + lineHeight: 1.7, ...fadeUp(120), })} >