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),
})}
>