From 76229465ca9371ef533591439706c4d1ba02a636 Mon Sep 17 00:00:00 2001
From: roormonger <34205054+roormonger@users.noreply.github.com>
Date: Tue, 24 Mar 2026 00:02:33 -0400
Subject: [PATCH] more navigation refinements
---
src/components/GamesPage.tsx | 373 +++++++++++++++++++----------------
src/index.css | 32 ++-
2 files changed, 230 insertions(+), 175 deletions(-)
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 ? (
) : (
-
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) => (
-
- play_arrow
- Start Game
-
- )}
-
+
{detailsQuery.data.title}
- {}}
- onEnterPress={() => console.log("Download", detailsQuery.data?.id)}
- className="shrink-0"
- >
- {(focused) => (
-
- download
- Download
-
- )}
-
-
+
+
+ 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) => (
+
+ play_arrow
+ Start Game
+
+ )}
+
+
+ setActiveZone('details')}
+ onEnterPress={() => console.log("Download", detailsQuery.data?.id)}
+ className="shrink-0"
+ focusKey="DETAILS_DOWNLOAD"
+ >
+ {(focused) => (
+
+ download
+ Download
+
+ )}
+
+
- {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;