Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .Jules/palette.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
## 2025-05-14 - [A11y: Semantic Buttons and Focus States]
**Learning:** Using semantic `<button type="button">` instead of `<span role="button">` provides native keyboard support (Enter/Space) and better screen reader integration. Adding `focus-visible:ring-2` ensures keyboard navigability is visible.
**Action:** Always prefer `<button>` for interactive elements. Ensure absolute-positioned interactive elements (like badges on cards) have a sufficient `z-index` (e.g., `z-10`) to remain clickable above images.

## 2025-05-14 - [Build: Strict TypeScript Checks]
**Learning:** The project's production build (`pnpm build`) includes strict TypeScript linting that fails on unused variables or state.
**Action:** Always run `pnpm build` and `tsc` before submitting to catch unused declarations that might have been left during development or refactoring.
27 changes: 15 additions & 12 deletions src/components/Library/GameCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ const GameCard: React.FC<GameCardProps> = ({ game, onClick, viewMode, onFilter,
return (
<div className="flex flex-wrap gap-1 mt-2 pointer-events-auto">
{displayedTags.map((tag) => (
<span
<button
key={tag.id}
type="button"
onClick={(e) => {
console.log('[GameCard] Click handler fired for tag:', tag.name);
e.stopPropagation();
Expand All @@ -44,11 +45,11 @@ const GameCard: React.FC<GameCardProps> = ({ game, onClick, viewMode, onFilter,
console.warn('[GameCard] onFilter is undefined!');
}
}}
className={`relative z-20 px-2 py-0.5 text-xs rounded-full text-white ${getCategoryColor(tag.category)} hover:opacity-80 hover:scale-110 hover:shadow-md transition-all cursor-pointer select-none border border-transparent hover:border-white/30 pointer-events-auto`}
role="button"
className={`relative z-20 px-2 py-0.5 text-xs rounded-full text-white ${getCategoryColor(tag.category)} hover:opacity-80 hover:scale-110 hover:shadow-md transition-all cursor-pointer select-none border border-transparent hover:border-white/30 focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-white pointer-events-auto`}
aria-label={`${t('filterByTag')}: ${tag.name}`}
>
{tag.name}
</span>
</button>
))}

{remainingCount > 0 && (
Expand All @@ -73,19 +74,20 @@ const GameCard: React.FC<GameCardProps> = ({ game, onClick, viewMode, onFilter,
return (
<div className="flex flex-wrap gap-1 mt-1">
{allMetadata.map((item) => (
<span
<button
key={`${item.type}-${item.id}`}
type="button"
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
console.log('[GameCard] Metadata tag clicked:', item.type, item.name);
onFilter?.(item.type, item.name);
}}
className="px-1.5 py-0.5 text-xs rounded bg-gray-700/50 theme-text-muted hover:bg-indigo-600/30 transition-colors cursor-pointer select-none"
role="button"
className="px-1.5 py-0.5 text-xs rounded bg-gray-700/50 theme-text-muted hover:bg-indigo-600/30 transition-colors cursor-pointer select-none focus-visible:ring-2 focus-visible:ring-indigo-500"
aria-label={`${t(item.type as any)}: ${item.name}`}
>
{item.name}
</span>
</button>
))}
</div>
);
Expand Down Expand Up @@ -132,18 +134,19 @@ const GameCard: React.FC<GameCardProps> = ({ game, onClick, viewMode, onFilter,
const label = COMPLETION_STATUS_LABELS[completion_status as keyof typeof COMPLETION_STATUS_LABELS] || completion_status;

return (
<span
<button
type="button"
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
console.log('[GameCard] Status badge clicked:', completion_status);
onFilter?.('status', completion_status);
}}
className={`absolute top-2 left-2 px-2 py-1 rounded-full text-xs text-white ${statusColors[completion_status] || 'bg-gray-600'} hover:opacity-80 transition-opacity cursor-pointer select-none`}
role="button"
className={`absolute top-2 left-2 px-2 py-1 rounded-full text-xs text-white z-10 ${statusColors[completion_status] || 'bg-gray-600'} hover:opacity-80 transition-opacity cursor-pointer select-none focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-white`}
aria-label={`${t('completionStatus')}: ${label}`}
>
{label}
</span>
</button>
);
};

Expand Down
16 changes: 12 additions & 4 deletions src/components/Library/GameDetailHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -276,8 +276,10 @@ export function GameDetailHeader({ game, onGameUpdated, onPlatformChange, onFilt
{game.genres.map((genre) => (
<button
key={genre.id}
type="button"
onClick={() => onFilter?.('genre', genre.name)}
className="px-2 py-0.5 text-xs rounded-full text-white bg-blue-600 hover:bg-blue-500 transition-opacity cursor-pointer"
className="px-2 py-0.5 text-xs rounded-full text-white bg-blue-600 hover:bg-blue-500 transition-opacity cursor-pointer focus-visible:ring-2 focus-visible:ring-blue-400 focus-visible:ring-offset-1 focus-visible:ring-offset-gray-900"
aria-label={`${t('genre')}: ${genre.name}`}
>
{genre.name}
</button>
Expand All @@ -292,8 +294,10 @@ export function GameDetailHeader({ game, onGameUpdated, onPlatformChange, onFilt
{game.game_modes.map((mode) => (
<button
key={mode.id}
type="button"
onClick={() => onFilter?.('mode', mode.name)}
className="px-2 py-0.5 text-xs rounded-full text-white bg-purple-600 hover:bg-purple-500 transition-opacity cursor-pointer"
className="px-2 py-0.5 text-xs rounded-full text-white bg-purple-600 hover:bg-purple-500 transition-opacity cursor-pointer focus-visible:ring-2 focus-visible:ring-purple-400 focus-visible:ring-offset-1 focus-visible:ring-offset-gray-900"
aria-label={`${t('gameMode')}: ${mode.name}`}
>
{mode.name}
</button>
Expand All @@ -308,8 +312,10 @@ export function GameDetailHeader({ game, onGameUpdated, onPlatformChange, onFilt
{game.player_perspectives.map((persp) => (
<button
key={persp.id}
type="button"
onClick={() => onFilter?.('perspective', persp.name)}
className="px-2 py-0.5 text-xs rounded-full text-white bg-green-600 hover:bg-green-500 transition-opacity cursor-pointer"
className="px-2 py-0.5 text-xs rounded-full text-white bg-green-600 hover:bg-green-500 transition-opacity cursor-pointer focus-visible:ring-2 focus-visible:ring-green-400 focus-visible:ring-offset-1 focus-visible:ring-offset-gray-900"
aria-label={`${t('perspective')}: ${persp.name}`}
>
{persp.name}
</button>
Expand All @@ -324,8 +330,10 @@ export function GameDetailHeader({ game, onGameUpdated, onPlatformChange, onFilt
{game.themes.map((theme) => (
<button
key={theme.id}
type="button"
onClick={() => onFilter?.('theme', theme.name)}
className="px-2 py-0.5 text-xs rounded-full text-white bg-orange-600 hover:bg-orange-500 transition-opacity cursor-pointer"
className="px-2 py-0.5 text-xs rounded-full text-white bg-orange-600 hover:bg-orange-500 transition-opacity cursor-pointer focus-visible:ring-2 focus-visible:ring-orange-400 focus-visible:ring-offset-1 focus-visible:ring-offset-gray-900"
aria-label={`${t('theme')}: ${theme.name}`}
>
{theme.name}
</button>
Expand Down
2 changes: 0 additions & 2 deletions src/components/Library/GameScreenshotsCarousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ export function GameScreenshotsCarousel({ gameId }: GameScreenshotsCarouselProps
const [igdbScreenshots, setIgdbScreenshots] = useState<string[]>([]);
const [currentIndex, setCurrentIndex] = useState(0);
const [loading, setLoading] = useState(true);
const [hasIgdbId, setHasIgdbId] = useState(false);

useEffect(() => {
const loadScreenshots = async () => {
Expand All @@ -29,7 +28,6 @@ export function GameScreenshotsCarousel({ gameId }: GameScreenshotsCarouselProps
// Load game data to check IGDB ID
try {
const game = await invoke<{ igdb_id: number | null }>("get_game_by_id", { id: gameId });
setHasIgdbId(!!game?.igdb_id);

// Load IGDB screenshots if available
if (game?.igdb_id) {
Expand Down