diff --git a/src/components/GamesPage.tsx b/src/components/GamesPage.tsx index 377284e..d1a70d9 100644 --- a/src/components/GamesPage.tsx +++ b/src/components/GamesPage.tsx @@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react'; import { useQuery } from '@tanstack/react-query'; import { FocusContext, useFocusable, setFocus } from '@noriginmedia/norigin-spatial-navigation'; import { rommApiClient, Platform, Game } from '../api/client'; +import { useInputMode } from '../context/InputModeContext'; interface FocusableItemProps { onFocus: () => void; @@ -13,19 +14,24 @@ interface FocusableItemProps { } const FocusableItem = ({ onFocus, onEnterPress, children, className, focusKey, scrollOptions }: FocusableItemProps) => { - const { ref, focused } = useFocusable({ + const { mode } = useInputMode(); + const { ref, focused: rawFocused } = useFocusable({ onFocus: () => { onFocus(); - if (scrollOptions) { - ref.current?.scrollIntoView(scrollOptions); - } else { - ref.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' }); + if (mode === 'gamepad') { + if (scrollOptions) { + ref.current?.scrollIntoView(scrollOptions); + } else { + ref.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' }); + } } }, onEnterPress, focusKey }); + const focused = mode === 'gamepad' ? rawFocused : false; + return (
{children(focused)} @@ -37,25 +43,26 @@ const PlatformItem = ({ platform, active, onSelect }: { platform: Platform, acti {(focused) => (
-
{platform.iconUrl ? ( {platform.name} ) : ( - sports_esports + sports_esports )}
{platform.name}
@@ -72,22 +79,20 @@ const GameListItem = ({ game, active, onFocus }: { game: Game, active: boolean, scrollOptions={{ behavior: 'smooth', block: 'nearest' }} > {(focused) => ( -
-
+
+
-
-
- {game.title} +
+
+ + {game.title} +
{game.size}
- {focused &&
chevron_right
}
)} @@ -100,39 +105,56 @@ const AlphabetScroller = ({ letters: string[], onLetterPress: (letter: string) => void }) => { - const { focusKey } = useFocusable({ - focusKey: 'ALPHABET_SCROLLER' - }); - return ( - -
- {letters.map(letter => ( - {}} - onEnterPress={() => onLetterPress(letter)} - className="w-full flex justify-center py-1" - scrollOptions={{ behavior: 'smooth', block: 'center' }} - > - {(focused) => ( -
- {letter} -
- )} -
- ))} -
-
+
+ {letters.map(letter => ( + {}} + onEnterPress={() => onLetterPress(letter)} + className="w-full flex justify-center py-1" + focusKey={`LETTER_${letter}`} + scrollOptions={{ behavior: 'smooth', block: 'center' }} + > + {(focused) => ( +
+ {letter} +
+ )} +
+ ))} +
); }; export const GamesPage = () => { - const { ref: pageRef, focusKey: pageFocusKey } = useFocusable(); const [selectedPlatformId, setSelectedPlatformId] = useState(null); const [selectedGameId, setSelectedGameId] = useState(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 const platformsQuery = useQuery({ @@ -172,7 +194,8 @@ export const GamesPage = () => { const handlePlatformFocus = (id: number) => { setSelectedPlatformId(id); - setSelectedGameId(null); // Reset game selection when platform changes + setSelectedGameId(null); + setActiveZone('platforms'); }; // Alphabet Logic @@ -205,156 +228,168 @@ export const GamesPage = () => { if (platformsQuery.isLoading) return
Initializing Archives...
; return ( - -
+
{/* Header / Platforms */} -
-
- {filteredPlatforms.map(p => ( - handlePlatformFocus(p.id)} - /> - ))} -
+
+ +
+ {filteredPlatforms.map(p => ( + handlePlatformFocus(p.id)} + /> + ))} +
+
{/* Main Split Layout */}
{/* Left Column: Game List + Alphabet */}
-
- {gamesQuery.isLoading ? ( -
Retrieving index...
- ) : ( - gamesQuery.data?.map(game => ( - setSelectedGameId(game.id)} - /> - )) - )} +
+ +
+ {gamesQuery.isLoading ? ( +
Retrieving index...
+ ) : ( + gamesQuery.data?.map(game => ( + { setSelectedGameId(game.id); setActiveZone('games'); }} + /> + )) + )} +
+
{activeLetters.length > 0 && ( - + +
+ +
+
)}
{/* Right Column: Game Details */} -
- {detailsQuery.data ? ( -
-
- {/* Poster */} -
- -
-
- - {/* Info Pane */} -
-
- PLAYABLE - ID: ROM-{detailsQuery.data.id} + +
+ {detailsQuery.data ? ( +
+
+ {/* Poster */} +
+ +
-

{detailsQuery.data.title}

- -
-
- developer_board - {detailsQuery.data.developer || 'Unknown Dev'} + {/* Info Pane */} +
+
+ PLAYABLE + ID: ROM-{detailsQuery.data.id}
-
- calendar_today - {detailsQuery.data.releaseDate || 'N/A'} -
-
- sports_esports - {detailsQuery.data.system} -
-
-
- {}} - onEnterPress={() => console.log("Play Game", detailsQuery.data?.id)} - className="shrink-0" - > - {(focused) => ( - - )} - +

{detailsQuery.data.title}

- {}} - onEnterPress={() => console.log("Download", detailsQuery.data?.id)} - className="shrink-0" - > - {(focused) => ( - - )} - -
+
+
+ developer_board + {detailsQuery.data.developer || 'Unknown Dev'} +
+
+ calendar_today + {detailsQuery.data.releaseDate || 'N/A'} +
+
+ sports_esports + {detailsQuery.data.system} +
+
-
-
-

Brief Summary

-

- {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."} -

-
+
+ setActiveZone('details')} + onEnterPress={() => console.log("Play Game", detailsQuery.data?.id)} + className="shrink-0" + focusKey="DETAILS_PLAY" + > + {(focused) => ( + + )} + + + setActiveZone('details')} + onEnterPress={() => console.log("Download", detailsQuery.data?.id)} + className="shrink-0" + focusKey="DETAILS_DOWNLOAD" + > + {(focused) => ( + + )} + +
- {detailsQuery.data.genres && detailsQuery.data.genres.length > 0 && ( +
-

Classifications

-
- {detailsQuery.data.genres.map(g => ( - {g} - ))} -
+

Brief Summary

+

+ {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."} +

- )} + + {detailsQuery.data.genres && detailsQuery.data.genres.length > 0 && ( +
+

Classifications

+
+ {detailsQuery.data.genres.map(g => ( + {g} + ))} +
+
+ )} +
-
- ) : selectedGameId ? ( -
-
-
progress_activity
-
Hydrating Metadata...
-
-
- ) : ( -
-
- videogame_asset -

Target selection required

+ ) : selectedGameId ? ( +
+
+
progress_activity
+
Hydrating Metadata...
+
-
- )} -
+ ) : ( +
+
+ videogame_asset +

Target selection required

+
+
+ )} +
+
- ); }; diff --git a/src/index.css b/src/index.css index b94f90d..79bfd82 100644 --- a/src/index.css +++ b/src/index.css @@ -5,9 +5,10 @@ @layer base { :root { color-scheme: dark; + --color-accent: #2563eb; } - /* Premium Global Scrollbars */ + /* Premium Global Scrollbars — thumb color driven by CSS variable */ ::-webkit-scrollbar { width: 6px; height: 6px; @@ -16,33 +17,52 @@ background: transparent; } ::-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: 1px solid transparent; background-clip: content-box; } ::-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; } /* Firefox Support */ * { 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 { margin: 0; min-height: 100vh; - background-color: #10131f; /* Adjusted to Stitch's deep charcoal */ - overflow: hidden; /* Prevent body scroll per Stitch design */ + background-color: #10131f; + overflow: hidden; } @layer utilities { .no-scrollbar::-webkit-scrollbar { display: 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 { cursor: none !important;