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 - [Clearable Range Inputs]
**Learning:** Native HTML range inputs cannot represent a null state once a value has been interacted with. For features like personal ratings where "not rated" is a valid state, a separate "Clear" or "Reset" button is necessary for a complete UX.
**Action:** Always provide a clear button for numeric inputs that should allow a null/unset state.

## 2025-05-14 - [Decorative Branding Accessibility]
**Learning:** Stylized branding overlays (like SVG text masks) often contain text that is redundant or confusing for screen readers if not properly hidden.
**Action:** Mark purely decorative branding elements with `aria-hidden="true"` to reduce screen reader noise.
22 changes: 20 additions & 2 deletions src/components/Library/GameDetailHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export function GameDetailHeader({ game, onGameUpdated, onPlatformChange, onFilt
alt={game.display_name}
className="w-full rounded-lg"
/>
<div className="svg-text-mask">
<div className="svg-text-mask" aria-hidden="true">
<span className="text-4xl font-black text-white drop-shadow-lg" style={{textShadow: '0 2px 8px rgba(0,0,0,0.4)'}}>PPGM</span>
</div>
</div>
Expand All @@ -148,6 +148,7 @@ export function GameDetailHeader({ game, onGameUpdated, onPlatformChange, onFilt
onClick={(e) => { e.stopPropagation(); onFavoriteToggle(); }}
className="absolute -top-2 -right-2 w-10 h-10 flex items-center justify-center text-3xl transition-transform hover:scale-110"
title={game.is_favorite ? t('removeFromFavorites') : t('addToFavorites')}
aria-label={game.is_favorite ? t('removeFromFavorites') : t('addToFavorites')}
>
{game.is_favorite ? (
<span className="text-yellow-400 drop-shadow-lg">★</span>
Expand Down Expand Up @@ -364,11 +365,28 @@ export function GameDetailHeader({ game, onGameUpdated, onPlatformChange, onFilt
{/* Personal Rating */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="theme-text-muted text-sm">{t('personalRating')} (0-100): <span className="text-purple-500 font-semibold">{game.personal_rating !== null && game.personal_rating !== undefined ? `${game.personal_rating}/100` : '-'}</span></span>
<label
htmlFor={`rating-${game.id}`}
className="theme-text-muted text-sm"
>
{t('personalRating')} (0-100): <span className="text-purple-500 font-semibold">{game.personal_rating !== null && game.personal_rating !== undefined ? `${game.personal_rating}/100` : '-'}</span>
</label>
{(game.personal_rating !== null && game.personal_rating !== undefined) && (
<button
type="button"
onClick={() => onRatingChange?.(null)}
className="text-xs text-red-400 hover:text-red-300 transition-colors flex items-center gap-1 cursor-pointer"
title={t('clearAll')}
aria-label={t('clearAll')}
>
<span aria-hidden="true">✕</span> {t('clearAll')}
</button>
)}
</div>
<div className="flex items-center gap-3">
<span className="text-xs theme-text-muted">0</span>
<input
id={`rating-${game.id}`}
type="range"
min="0"
max="100"
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