more navigation refinements
This commit is contained in:
@@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { FocusContext, useFocusable, setFocus } from '@noriginmedia/norigin-spatial-navigation';
|
import { FocusContext, useFocusable, setFocus } from '@noriginmedia/norigin-spatial-navigation';
|
||||||
import { rommApiClient, Platform, Game } from '../api/client';
|
import { rommApiClient, Platform, Game } from '../api/client';
|
||||||
|
import { useInputMode } from '../context/InputModeContext';
|
||||||
|
|
||||||
interface FocusableItemProps {
|
interface FocusableItemProps {
|
||||||
onFocus: () => void;
|
onFocus: () => void;
|
||||||
@@ -13,19 +14,24 @@ interface FocusableItemProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const FocusableItem = ({ onFocus, onEnterPress, children, className, focusKey, scrollOptions }: FocusableItemProps) => {
|
const FocusableItem = ({ onFocus, onEnterPress, children, className, focusKey, scrollOptions }: FocusableItemProps) => {
|
||||||
const { ref, focused } = useFocusable({
|
const { mode } = useInputMode();
|
||||||
|
const { ref, focused: rawFocused } = useFocusable({
|
||||||
onFocus: () => {
|
onFocus: () => {
|
||||||
onFocus();
|
onFocus();
|
||||||
if (scrollOptions) {
|
if (mode === 'gamepad') {
|
||||||
ref.current?.scrollIntoView(scrollOptions);
|
if (scrollOptions) {
|
||||||
} else {
|
ref.current?.scrollIntoView(scrollOptions);
|
||||||
ref.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
|
} else {
|
||||||
|
ref.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onEnterPress,
|
onEnterPress,
|
||||||
focusKey
|
focusKey
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const focused = mode === 'gamepad' ? rawFocused : false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className={`${className} ${focused ? 'focused' : ''}`}>
|
<div ref={ref} className={`${className} ${focused ? 'focused' : ''}`}>
|
||||||
{children(focused)}
|
{children(focused)}
|
||||||
@@ -37,25 +43,26 @@ const PlatformItem = ({ platform, active, onSelect }: { platform: Platform, acti
|
|||||||
<FocusableItem
|
<FocusableItem
|
||||||
onFocus={onSelect}
|
onFocus={onSelect}
|
||||||
className="shrink-0"
|
className="shrink-0"
|
||||||
|
focusKey={`PLATFORM_${platform.id}`}
|
||||||
>
|
>
|
||||||
{(focused) => (
|
{(focused) => (
|
||||||
<div className="flex flex-col items-center gap-2 group cursor-pointer w-[110px]">
|
<div className="flex flex-col items-center gap-2 group cursor-pointer w-[110px]">
|
||||||
<div className={`w-[90px] h-[90px] rounded-[24px] transition-all duration-500 border flex items-center justify-center p-5
|
<div className={`w-[84px] h-[84px] rounded-[12px] transition-all duration-500 border-2 flex items-center justify-center p-3
|
||||||
${focused ? 'bg-[#2563eb] border-[#2563eb] scale-110 shadow-[0_15px_40px_rgba(37,99,235,0.4)]' :
|
${focused || active ? 'border-[#2563eb] bg-transparent scale-110 z-20 shadow-[0_5px_15px_rgba(37,99,235,0.2)]' :
|
||||||
active ? 'bg-white/10 border-white/20' : 'bg-white/5 border-white/5 group-hover:border-white/20'}`}
|
'bg-white/5 border-white/5 group-hover:border-white/10'}`}
|
||||||
>
|
>
|
||||||
{platform.iconUrl ? (
|
{platform.iconUrl ? (
|
||||||
<img
|
<img
|
||||||
src={platform.iconUrl}
|
src={platform.iconUrl}
|
||||||
alt={platform.name}
|
alt={platform.name}
|
||||||
className={`w-full h-full object-contain transition-all duration-500 ${focused ? 'scale-110 brightness-100' : 'grayscale brightness-200 opacity-30 group-hover:opacity-60'}`}
|
className={`w-full h-full object-contain transition-all duration-500 ${focused || active ? 'scale-110 brightness-100' : 'grayscale brightness-200 opacity-30 group-hover:opacity-60'}`}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span className={`material-symbols-outlined text-3xl transition-colors ${focused ? 'text-white' : 'text-white/20'}`}>sports_esports</span>
|
<span className={`material-symbols-outlined text-3xl transition-colors ${focused || active ? 'text-white' : 'text-white/20'}`}>sports_esports</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={`text-[9px] font-black uppercase geist-mono tracking-widest text-center transition-all duration-300 w-full px-2 truncate
|
<div className={`text-[9px] font-black uppercase geist-mono tracking-widest text-center transition-all duration-300 w-full px-2 truncate
|
||||||
${focused ? 'text-[#2563eb] translate-y-1' : active ? 'text-white' : 'text-white/20'}`}
|
${focused || active ? 'text-[#2563eb] translate-y-1' : 'text-white/20'}`}
|
||||||
>
|
>
|
||||||
{platform.name}
|
{platform.name}
|
||||||
</div>
|
</div>
|
||||||
@@ -72,22 +79,20 @@ const GameListItem = ({ game, active, onFocus }: { game: Game, active: boolean,
|
|||||||
scrollOptions={{ behavior: 'smooth', block: 'nearest' }}
|
scrollOptions={{ behavior: 'smooth', block: 'nearest' }}
|
||||||
>
|
>
|
||||||
{(focused) => (
|
{(focused) => (
|
||||||
<div className={`px-4 py-2.5 transition-all duration-300 flex items-center gap-4 cursor-pointer group border-r-2
|
<div className={`flex items-center gap-4 px-6 py-2.5 cursor-pointer transition-all duration-300 border-l-4 ${focused ? 'bg-[#2563eb]/20 border-[#2563eb] z-10' : active ? 'bg-[#2563eb]/10 border-[#2563eb]/70' : 'border-transparent hover:bg-white/5'}`}>
|
||||||
${focused ? 'bg-white/10 border-[#2563eb]' :
|
<div className="w-8 h-11 bg-white/5 rounded-[4px] overflow-hidden shrink-0 border border-white/5">
|
||||||
active ? 'bg-white/5 border-transparent' : 'bg-transparent border-transparent hover:bg-white/5'}`}
|
|
||||||
>
|
|
||||||
<div className="w-8 h-11 bg-white/5 rounded-[4px] overflow-hidden shrink-0 border border-white/5 group-hover:border-white/20 transition-colors">
|
|
||||||
<img src={game.coverUrl} alt="" className="w-full h-full object-cover" />
|
<img src={game.coverUrl} alt="" className="w-full h-full object-cover" />
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1 overflow-hidden">
|
||||||
<div className={`text-xs font-bold truncate transition-colors ${focused || active ? 'text-white' : 'text-white/60 group-hover:text-white/80'}`}>
|
<div className="overflow-hidden whitespace-nowrap">
|
||||||
{game.title}
|
<span className={`text-xs font-bold transition-colors ${focused ? 'marquee-active text-white' : active ? 'text-white' : 'text-white/60'}`}>
|
||||||
|
{game.title}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[10px] text-white/40 font-medium geist-mono uppercase tracking-tight">
|
<div className="text-[10px] text-white/40 font-medium geist-mono uppercase tracking-tight">
|
||||||
{game.size}
|
{game.size}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{focused && <div className="material-symbols-outlined text-[#2563eb] text-lg animate-pulse">chevron_right</div>}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</FocusableItem>
|
</FocusableItem>
|
||||||
@@ -100,39 +105,56 @@ const AlphabetScroller = ({
|
|||||||
letters: string[],
|
letters: string[],
|
||||||
onLetterPress: (letter: string) => void
|
onLetterPress: (letter: string) => void
|
||||||
}) => {
|
}) => {
|
||||||
const { focusKey } = useFocusable({
|
|
||||||
focusKey: 'ALPHABET_SCROLLER'
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FocusContext.Provider value={focusKey}>
|
<div className="w-10 flex flex-col items-center justify-center py-8 bg-black/40 border-l border-white/5 gap-2 select-none">
|
||||||
<div className="w-10 flex flex-col items-center justify-center py-8 bg-black/40 border-l border-white/5 gap-2 select-none">
|
{letters.map(letter => (
|
||||||
{letters.map(letter => (
|
<FocusableItem
|
||||||
<FocusableItem
|
key={letter}
|
||||||
key={letter}
|
onFocus={() => {}}
|
||||||
onFocus={() => {}}
|
onEnterPress={() => onLetterPress(letter)}
|
||||||
onEnterPress={() => onLetterPress(letter)}
|
className="w-full flex justify-center py-1"
|
||||||
className="w-full flex justify-center py-1"
|
focusKey={`LETTER_${letter}`}
|
||||||
scrollOptions={{ behavior: 'smooth', block: 'center' }}
|
scrollOptions={{ behavior: 'smooth', block: 'center' }}
|
||||||
>
|
>
|
||||||
{(focused) => (
|
{(focused) => (
|
||||||
<div className={`text-[13px] font-black geist-mono transition-all duration-200
|
<div className={`text-[13px] font-black geist-mono transition-all duration-200
|
||||||
${focused ? 'text-[#2563eb] scale-125' : 'text-white/30 hover:text-white/70'}`}
|
${focused ? 'text-[#2563eb] scale-125' : 'text-white/30 hover:text-white/70'}`}
|
||||||
>
|
>
|
||||||
{letter}
|
{letter}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</FocusableItem>
|
</FocusableItem>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</FocusContext.Provider>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GamesPage = () => {
|
export const GamesPage = () => {
|
||||||
const { ref: pageRef, focusKey: pageFocusKey } = useFocusable();
|
|
||||||
const [selectedPlatformId, setSelectedPlatformId] = useState<number | null>(null);
|
const [selectedPlatformId, setSelectedPlatformId] = useState<number | null>(null);
|
||||||
const [selectedGameId, setSelectedGameId] = useState<string | null>(null);
|
const [selectedGameId, setSelectedGameId] = useState<string | null>(null);
|
||||||
|
const [activeZone, setActiveZone] = useState<'platforms' | 'games' | 'alphabet' | 'details' | null>(null);
|
||||||
|
|
||||||
|
const { ref: platformsRef, focusKey: platformsFocusKey } = useFocusable({
|
||||||
|
focusKey: 'PLATFORMS_ZONE',
|
||||||
|
trackChildren: true,
|
||||||
|
preferredChildFocusKey: selectedPlatformId ? `PLATFORM_${selectedPlatformId}` : undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
const { ref: gamesRef, focusKey: gamesFocusKey } = useFocusable({
|
||||||
|
focusKey: 'GAMES_ZONE',
|
||||||
|
trackChildren: true,
|
||||||
|
preferredChildFocusKey: selectedGameId ? `GAME_${selectedGameId}` : undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
const { ref: alphabetRef, focusKey: alphabetFocusKey } = useFocusable({
|
||||||
|
focusKey: 'ALPHABET_ZONE',
|
||||||
|
trackChildren: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const { ref: detailsRef, focusKey: detailsFocusKey } = useFocusable({
|
||||||
|
focusKey: 'DETAILS_ZONE',
|
||||||
|
trackChildren: true
|
||||||
|
});
|
||||||
|
|
||||||
// Queries
|
// Queries
|
||||||
const platformsQuery = useQuery({
|
const platformsQuery = useQuery({
|
||||||
@@ -172,7 +194,8 @@ export const GamesPage = () => {
|
|||||||
|
|
||||||
const handlePlatformFocus = (id: number) => {
|
const handlePlatformFocus = (id: number) => {
|
||||||
setSelectedPlatformId(id);
|
setSelectedPlatformId(id);
|
||||||
setSelectedGameId(null); // Reset game selection when platform changes
|
setSelectedGameId(null);
|
||||||
|
setActiveZone('platforms');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Alphabet Logic
|
// Alphabet Logic
|
||||||
@@ -205,156 +228,168 @@ export const GamesPage = () => {
|
|||||||
if (platformsQuery.isLoading) return <div className="h-full flex items-center justify-center text-white/50 geist-mono uppercase">Initializing Archives...</div>;
|
if (platformsQuery.isLoading) return <div className="h-full flex items-center justify-center text-white/50 geist-mono uppercase">Initializing Archives...</div>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FocusContext.Provider value={pageFocusKey}>
|
<div className="h-full flex flex-col bg-[#10131f] overflow-hidden">
|
||||||
<div ref={pageRef} className="h-full flex flex-col bg-[#10131f] overflow-hidden">
|
|
||||||
{/* Header / Platforms */}
|
{/* Header / Platforms */}
|
||||||
<div className="px-12 py-5 shrink-0 border-b border-white/5 bg-black/10">
|
<div className="px-12 py-4 shrink-0 border-b border-white/5 bg-black/10">
|
||||||
<div className="flex gap-6 overflow-x-auto no-scrollbar pb-2">
|
<FocusContext.Provider value={platformsFocusKey}>
|
||||||
{filteredPlatforms.map(p => (
|
<div ref={platformsRef} className={`flex gap-6 overflow-x-auto py-4 ${activeZone === 'platforms' ? 'scrollbar-active' : ''}`}>
|
||||||
<PlatformItem
|
{filteredPlatforms.map(p => (
|
||||||
key={p.id}
|
<PlatformItem
|
||||||
platform={p}
|
key={p.id}
|
||||||
active={selectedPlatformId === p.id}
|
platform={p}
|
||||||
onSelect={() => handlePlatformFocus(p.id)}
|
active={selectedPlatformId === p.id}
|
||||||
/>
|
onSelect={() => handlePlatformFocus(p.id)}
|
||||||
))}
|
/>
|
||||||
</div>
|
))}
|
||||||
|
</div>
|
||||||
|
</FocusContext.Provider>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Split Layout */}
|
{/* Main Split Layout */}
|
||||||
<div className="flex-1 flex overflow-hidden">
|
<div className="flex-1 flex overflow-hidden">
|
||||||
{/* Left Column: Game List + Alphabet */}
|
{/* Left Column: Game List + Alphabet */}
|
||||||
<div className="w-[500px] border-r border-white/5 flex overflow-hidden bg-black/20">
|
<div className="w-[500px] border-r border-white/5 flex overflow-hidden bg-black/20">
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div className="flex-1 flex flex-col overflow-hidden">
|
||||||
{gamesQuery.isLoading ? (
|
<FocusContext.Provider value={gamesFocusKey}>
|
||||||
<div className="p-12 text-center text-white/20 geist-mono text-[10px] uppercase">Retrieving index...</div>
|
<div ref={gamesRef} className={`flex-1 overflow-y-auto ${activeZone === 'games' ? 'scrollbar-active' : ''}`}>
|
||||||
) : (
|
{gamesQuery.isLoading ? (
|
||||||
gamesQuery.data?.map(game => (
|
<div className="p-12 text-center text-white/20 geist-mono text-[10px] uppercase">Retrieving index...</div>
|
||||||
<GameListItem
|
) : (
|
||||||
key={game.id}
|
gamesQuery.data?.map(game => (
|
||||||
game={game}
|
<GameListItem
|
||||||
active={selectedGameId === game.id}
|
key={game.id}
|
||||||
onFocus={() => setSelectedGameId(game.id)}
|
game={game}
|
||||||
/>
|
active={selectedGameId === game.id}
|
||||||
))
|
onFocus={() => { setSelectedGameId(game.id); setActiveZone('games'); }}
|
||||||
)}
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</FocusContext.Provider>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{activeLetters.length > 0 && (
|
{activeLetters.length > 0 && (
|
||||||
<AlphabetScroller
|
<FocusContext.Provider value={alphabetFocusKey}>
|
||||||
letters={activeLetters}
|
<div ref={alphabetRef} className="h-full flex">
|
||||||
onLetterPress={handleLetterPress}
|
<AlphabetScroller
|
||||||
/>
|
letters={activeLetters}
|
||||||
|
onLetterPress={handleLetterPress}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FocusContext.Provider>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Column: Game Details */}
|
{/* Right Column: Game Details */}
|
||||||
<div className="flex-1 overflow-y-auto no-scrollbar relative">
|
<FocusContext.Provider value={detailsFocusKey}>
|
||||||
{detailsQuery.data ? (
|
<div ref={detailsRef} className={`flex-1 overflow-y-auto relative ${activeZone === 'details' ? 'scrollbar-active' : ''}`}>
|
||||||
<div className="p-16 animate-in fade-in slide-in-from-right-8 duration-700">
|
{detailsQuery.data ? (
|
||||||
<div className="flex gap-12 items-start">
|
<div className="p-16">
|
||||||
{/* Poster */}
|
<div className="flex gap-12 items-start">
|
||||||
<div className="w-[320px] rounded-[16px] overflow-hidden shadow-[0_20px_50px_rgba(0,0,0,0.5)] border border-white/10 shrink-0 sticky top-0 group">
|
{/* Poster */}
|
||||||
<img
|
<div className="w-[320px] rounded-[16px] overflow-hidden shadow-[0_20px_50px_rgba(0,0,0,0.5)] border border-white/10 shrink-0 sticky top-0 group">
|
||||||
src={detailsQuery.data.coverUrl}
|
<img
|
||||||
className="w-full aspect-[2/3] object-cover transition-transform duration-1000 group-hover:scale-110"
|
src={detailsQuery.data.coverUrl}
|
||||||
alt=""
|
className="w-full aspect-[2/3] object-cover transition-transform duration-1000 group-hover:scale-110"
|
||||||
/>
|
alt=""
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-black via-transparent to-transparent opacity-60"></div>
|
/>
|
||||||
</div>
|
<div className="absolute inset-0 bg-gradient-to-t from-black via-transparent to-transparent opacity-60"></div>
|
||||||
|
|
||||||
{/* Info Pane */}
|
|
||||||
<div className="flex-1 pt-4">
|
|
||||||
<div className="flex items-center gap-3 mb-6">
|
|
||||||
<span className="px-3 py-1 bg-[#2563eb] text-white text-[9px] font-black uppercase tracking-widest rounded-sm">PLAYABLE</span>
|
|
||||||
<span className="text-white/40 text-[10px] geist-mono uppercase tracking-widest">ID: ROM-{detailsQuery.data.id}</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 className="text-5xl font-black text-white mb-4 tracking-tighter leading-none">{detailsQuery.data.title}</h1>
|
{/* Info Pane */}
|
||||||
|
<div className="flex-1 pt-4">
|
||||||
<div className="flex flex-wrap gap-x-6 gap-y-2 mb-10 items-center">
|
<div className="flex items-center gap-3 mb-6">
|
||||||
<div className="flex items-center gap-2">
|
<span className="px-3 py-1 bg-[#2563eb] text-white text-[9px] font-black uppercase tracking-widest rounded-sm">PLAYABLE</span>
|
||||||
<span className="material-symbols-outlined text-[16px] text-[#2563eb]">developer_board</span>
|
<span className="text-white/40 text-[10px] geist-mono uppercase tracking-widest">ID: ROM-{detailsQuery.data.id}</span>
|
||||||
<span className="text-[11px] font-bold text-white/80 uppercase geist-mono">{detailsQuery.data.developer || 'Unknown Dev'}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="material-symbols-outlined text-[16px] text-[#2563eb]">calendar_today</span>
|
<h1 className="text-5xl font-black text-white mb-4 tracking-tighter leading-none">{detailsQuery.data.title}</h1>
|
||||||
<span className="text-[11px] font-bold text-white/80 uppercase geist-mono">{detailsQuery.data.releaseDate || 'N/A'}</span>
|
|
||||||
|
<div className="flex flex-wrap gap-x-6 gap-y-2 mb-10 items-center">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="material-symbols-outlined text-[16px] text-[#2563eb]">developer_board</span>
|
||||||
|
<span className="text-[11px] font-bold text-white/80 uppercase geist-mono">{detailsQuery.data.developer || 'Unknown Dev'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="material-symbols-outlined text-[16px] text-[#2563eb]">calendar_today</span>
|
||||||
|
<span className="text-[11px] font-bold text-white/80 uppercase geist-mono">{detailsQuery.data.releaseDate || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="material-symbols-outlined text-[16px] text-[#2563eb]">sports_esports</span>
|
||||||
|
<span className="text-[11px] font-bold text-white/80 uppercase geist-mono">{detailsQuery.data.system}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="material-symbols-outlined text-[16px] text-[#2563eb]">sports_esports</span>
|
<div className="flex gap-4 mb-12">
|
||||||
<span className="text-[11px] font-bold text-white/80 uppercase geist-mono">{detailsQuery.data.system}</span>
|
<FocusableItem
|
||||||
|
onFocus={() => setActiveZone('details')}
|
||||||
|
onEnterPress={() => console.log("Play Game", detailsQuery.data?.id)}
|
||||||
|
className="shrink-0"
|
||||||
|
focusKey="DETAILS_PLAY"
|
||||||
|
>
|
||||||
|
{(focused) => (
|
||||||
|
<button className={`flex items-center gap-3 px-8 py-4 rounded-full font-black uppercase tracking-tighter transition-all duration-300 shadow-xl ${focused ? 'bg-[#2563eb] text-white scale-105 shadow-[#2563eb]/40' : 'bg-white text-black hover:bg-white/90'}`}>
|
||||||
|
<span className="material-symbols-outlined filled">play_arrow</span>
|
||||||
|
Start Game
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</FocusableItem>
|
||||||
|
|
||||||
|
<FocusableItem
|
||||||
|
onFocus={() => setActiveZone('details')}
|
||||||
|
onEnterPress={() => console.log("Download", detailsQuery.data?.id)}
|
||||||
|
className="shrink-0"
|
||||||
|
focusKey="DETAILS_DOWNLOAD"
|
||||||
|
>
|
||||||
|
{(focused) => (
|
||||||
|
<button className={`flex items-center gap-3 px-8 py-4 rounded-full font-black uppercase tracking-tighter border-2 transition-all duration-300 ${focused ? 'border-[#2563eb] text-[#2563eb] bg-[#2563eb]/10 scale-105' : 'border-white/10 text-white hover:border-white/20 bg-white/5'}`}>
|
||||||
|
<span className="material-symbols-outlined">download</span>
|
||||||
|
Download
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</FocusableItem>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-4 mb-12">
|
<div className="space-y-8 max-w-2xl">
|
||||||
<FocusableItem
|
|
||||||
onFocus={() => {}}
|
|
||||||
onEnterPress={() => console.log("Play Game", detailsQuery.data?.id)}
|
|
||||||
className="shrink-0"
|
|
||||||
>
|
|
||||||
{(focused) => (
|
|
||||||
<button className={`flex items-center gap-3 px-8 py-4 rounded-full font-black uppercase tracking-tighter transition-all duration-300 shadow-xl ${focused ? 'bg-[#2563eb] text-white scale-105 shadow-[#2563eb]/40' : 'bg-white text-black hover:bg-white/90'}`}>
|
|
||||||
<span className="material-symbols-outlined filled">play_arrow</span>
|
|
||||||
Start Game
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</FocusableItem>
|
|
||||||
|
|
||||||
<FocusableItem
|
|
||||||
onFocus={() => {}}
|
|
||||||
onEnterPress={() => console.log("Download", detailsQuery.data?.id)}
|
|
||||||
className="shrink-0"
|
|
||||||
>
|
|
||||||
{(focused) => (
|
|
||||||
<button className={`flex items-center gap-3 px-8 py-4 rounded-full font-black uppercase tracking-tighter border-2 transition-all duration-300 ${focused ? 'border-[#2563eb] text-[#2563eb] bg-[#2563eb]/10 scale-105' : 'border-white/10 text-white hover:border-white/20 bg-white/5'}`}>
|
|
||||||
<span className="material-symbols-outlined">download</span>
|
|
||||||
Download
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</FocusableItem>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-8 max-w-2xl">
|
|
||||||
<section>
|
|
||||||
<h3 className="text-[10px] font-black text-[#2563eb] uppercase tracking-[0.4em] mb-4">Brief Summary</h3>
|
|
||||||
<p className="text-white/60 text-sm leading-relaxed font-medium">
|
|
||||||
{detailsQuery.data.summary || "This title does not have a comprehensive analysis in our local database yet. Our researchers are working to archive more metadata for this specific entry."}
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{detailsQuery.data.genres && detailsQuery.data.genres.length > 0 && (
|
|
||||||
<section>
|
<section>
|
||||||
<h3 className="text-[10px] font-black text-[#2563eb] uppercase tracking-[0.4em] mb-4">Classifications</h3>
|
<h3 className="text-[10px] font-black text-[#2563eb] uppercase tracking-[0.4em] mb-4">Brief Summary</h3>
|
||||||
<div className="flex flex-wrap gap-2">
|
<p className="text-white/60 text-sm leading-relaxed font-medium">
|
||||||
{detailsQuery.data.genres.map(g => (
|
{detailsQuery.data.summary || "This title does not have a comprehensive analysis in our local database yet. Our researchers are working to archive more metadata for this specific entry."}
|
||||||
<span key={g} className="px-3 py-1 bg-white/5 border border-white/5 rounded-full text-[9px] font-bold text-white/60 uppercase tracking-widest">{g}</span>
|
</p>
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
)}
|
|
||||||
|
{detailsQuery.data.genres && detailsQuery.data.genres.length > 0 && (
|
||||||
|
<section>
|
||||||
|
<h3 className="text-[10px] font-black text-[#2563eb] uppercase tracking-[0.4em] mb-4">Classifications</h3>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{detailsQuery.data.genres.map(g => (
|
||||||
|
<span key={g} className="px-3 py-1 bg-white/5 border border-white/5 rounded-full text-[9px] font-bold text-white/60 uppercase tracking-widest">{g}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : selectedGameId ? (
|
||||||
) : selectedGameId ? (
|
<div className="h-full flex items-center justify-center">
|
||||||
<div className="h-full flex items-center justify-center">
|
<div className="text-center">
|
||||||
<div className="text-center">
|
<div className="material-symbols-outlined text-4xl text-white/10 animate-spin mb-4">progress_activity</div>
|
||||||
<div className="material-symbols-outlined text-4xl text-white/10 animate-spin mb-4">progress_activity</div>
|
<div className="text-[10px] geist-mono text-white/20 uppercase tracking-widest">Hydrating Metadata...</div>
|
||||||
<div className="text-[10px] geist-mono text-white/20 uppercase tracking-widest">Hydrating Metadata...</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="h-full flex items-center justify-center p-20 text-center opacity-20">
|
|
||||||
<div>
|
|
||||||
<span className="material-symbols-outlined text-6xl mb-6">videogame_asset</span>
|
|
||||||
<p className="geist-mono uppercase tracking-[0.3em] font-black text-sm">Target selection required</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : (
|
||||||
)}
|
<div className="h-full flex items-center justify-center p-20 text-center opacity-20">
|
||||||
</div>
|
<div>
|
||||||
|
<span className="material-symbols-outlined text-6xl mb-6">videogame_asset</span>
|
||||||
|
<p className="geist-mono uppercase tracking-[0.3em] font-black text-sm">Target selection required</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</FocusContext.Provider>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</FocusContext.Provider>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,9 +5,10 @@
|
|||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
|
--color-accent: #2563eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Premium Global Scrollbars */
|
/* Premium Global Scrollbars — thumb color driven by CSS variable */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
height: 6px;
|
height: 6px;
|
||||||
@@ -16,34 +17,53 @@
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: rgba(255, 255, 255, 0.1);
|
background: var(--scrollbar-thumb-color, rgba(255, 255, 255, 0.1));
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
background-clip: content-box;
|
background-clip: content-box;
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: rgba(255, 255, 255, 0.2);
|
background: var(--scrollbar-thumb-hover-color, rgba(255, 255, 255, 0.2));
|
||||||
background-clip: content-box;
|
background-clip: content-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Firefox Support */
|
/* Firefox Support */
|
||||||
* {
|
* {
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: rgba(255, 255, 255, 0.1) transparent;
|
scrollbar-color: var(--scrollbar-thumb-color, rgba(255, 255, 255, 0.1)) transparent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background-color: #10131f; /* Adjusted to Stitch's deep charcoal */
|
background-color: #10131f;
|
||||||
overflow: hidden; /* Prevent body scroll per Stitch design */
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
.no-scrollbar::-webkit-scrollbar { display: none; }
|
.no-scrollbar::-webkit-scrollbar { display: none; }
|
||||||
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
|
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
|
||||||
|
|
||||||
|
/* Setting the CSS variable on the scrollable container causes the pseudo-element
|
||||||
|
to inherit it, which is the correct way to drive scrollbar color via state. */
|
||||||
|
.scrollbar-active {
|
||||||
|
--scrollbar-thumb-color: var(--color-accent);
|
||||||
|
--scrollbar-thumb-hover-color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Auto-scroll animation for long game titles when focused */
|
||||||
|
@keyframes scroll-title {
|
||||||
|
0%, 15% { transform: translateX(0%); }
|
||||||
|
85%, 100% { transform: translateX(-60%); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.marquee-active {
|
||||||
|
display: inline-block;
|
||||||
|
animation: scroll-title 4s ease-in-out infinite alternate;
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
body.gamepad-active {
|
body.gamepad-active {
|
||||||
cursor: none !important;
|
cursor: none !important;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user