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
48 changes: 46 additions & 2 deletions app/styles/touch.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
*
* All interactive elements should present a hit area of at least 44×44 px on
* touch devices. Use `.touch-target` on the element itself (when its visual
* size meets 44 px) or `.touch-target-expand` when the element is intentionally
* small and needs an invisible extended hit area via a pseudo-element.
* size meets 44 px), `.touch-ripple` for press feedback, or
* `.touch-target-expand` when the element is intentionally small and needs an
* invisible extended hit area via a pseudo-element.
*
* Usage:
* <button className="touch-target …">Save</button>
* <button className="touch-target touch-ripple …">Card</button>
* <button className="touch-target-expand relative …">×</button>
*/

Expand All @@ -22,6 +24,48 @@
min-width: 44px;
}

/**
* .touch-ripple
* Adds subtle tactile feedback for touch/click presses without extra markup.
* The pseudo-element only animates transform and opacity, so layout remains
* stable across breakpoints and dark mode.
*/
.touch-ripple {
-webkit-tap-highlight-color: transparent;
overflow: hidden;
position: relative;
}

.touch-ripple::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background: radial-gradient(circle at center, hsl(var(--primary) / 0.16), transparent 68%);
opacity: 0;
transform: scale(0.92);
transition:
opacity 180ms ease-out,
transform 220ms ease-out;
}

.touch-ripple:active::after {
opacity: 1;
transform: scale(1);
transition-duration: 120ms;
}

@media (prefers-reduced-motion: reduce) {
.touch-ripple::after {
transform: none;
transition: none;
}

.touch-ripple:active::after {
transform: none;
}
}

/**
* .touch-target-expand
* Expands the interactive hit area to 44×44 px via an absolute pseudo-element
Expand Down
31 changes: 21 additions & 10 deletions components/PredictionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,15 @@ const PredictionCard: React.FC<PredictionCardProps> = ({ prediction }) => {
return <PredictionCardSkeleton />;
}

const { title, description, stakeAmount, stakeToken, odds, potentialWinnings, winningsToken, eventDate, resolvedDate, status } = prediction;
const { title, description, category, outcome, stakeAmount, stakeToken, odds, potentialWinnings, winningsToken, eventDate, resolvedDate, status } = prediction;
const { icon: Icon, className, label } = statusMap[status];

return (
/* touch-target: enforces ≥44px hit area (WCAG 2.5.5 / Apple HIG). */
<button className="touch-target w-full text-left bg-card p-4 rounded-xl shadow-lg hover:bg-muted/50 transition duration-200 cursor-pointer border border-border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2">
<button
type="button"
className="touch-target touch-ripple relative isolate w-full overflow-hidden text-left bg-card p-4 rounded-xl shadow-lg hover:bg-muted/50 transition duration-200 cursor-pointer border border-border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
<div className="flex justify-between items-start mb-3">
<div className="flex flex-col items-start gap-1 pr-2">
<h3 className="text-lg font-semibold text-card-foreground line-clamp-2">{title}</h3>
Expand Down Expand Up @@ -156,14 +159,22 @@ const PredictionCard: React.FC<PredictionCardProps> = ({ prediction }) => {
<Collapsible open={isOddsExpanded} onOpenChange={setIsOddsExpanded}>
<CollapsibleTrigger asChild>
{/* touch-target: guarantees ≥44px tap area on the Odds trigger (WCAG 2.5.5). */}
<button
className="touch-target flex w-full items-center justify-between px-2 rounded hover:bg-muted/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
aria-expanded={isOddsExpanded}
aria-controls="odds-breakdown"
>
<p className="text-muted-foreground">Odds</p>
<p className="text-card-foreground font-medium">{odds.toFixed(1)}x</p>
</button>
<span
role="button"
tabIndex={0}
className="touch-target flex w-full items-center justify-between px-2 rounded hover:bg-muted/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
aria-expanded={isOddsExpanded}
aria-controls="odds-breakdown"
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
event.currentTarget.click();
}
}}
>
<p className="text-muted-foreground">Odds</p>
<p className="text-card-foreground font-medium">{odds.toFixed(1)}x</p>
</span>
</CollapsibleTrigger>
<CollapsibleContent id="odds-breakdown" className="mt-2 text-sm text-muted-foreground">
<p>Implied probability: {(1 / odds * 100).toFixed(1)}%</p>
Expand Down
Loading