diff --git a/THEME_AND_BRANDING.md b/THEME_AND_BRANDING.md index 379c65d..e8c8707 100644 --- a/THEME_AND_BRANDING.md +++ b/THEME_AND_BRANDING.md @@ -295,10 +295,27 @@ When in doubt, add more space rather than less. ### Allowed Animations * Hover states: Subtle opacity shift (1.0 → 0.85) or slight color change (200ms ease) +* Pressed states: Brief scale-down (~2%) on activation, ~150ms — tactile + acknowledgement, never playful * Loading states: Simple fade-in (300ms ease) * Page transitions: None (or instant) * Scroll-triggered reveals: Simple fade-in (600ms ease) +### Interactive feedback + +* Every interactive control (buttons, links styled as buttons, clickable + rows/cards, filter chips, disclosure controls) must show a distinct + hover, focus-visible, and pressed state so a click or tap never feels + unacknowledged. +* Use the shared primitives rather than bespoke per-component motion: + `buttonVariants` in `components/ui/button.tsx` for `Button` instances and + `pressableClasses` in `lib/utils.ts` for other clickable surfaces. +* The pressed state is a small scale-down (scale 0.98, ~150ms) — no bounce, + spring, ripples, or large transforms, and no layout shift. +* Press transforms are gated by `motion-safe:` so `prefers-reduced-motion` + sessions keep the instant color/border acknowledgement without movement. +* Text-style links use the opacity hover only — they do not scale. + ### Forbidden Animations * Parallax scrolling * Animated SVG decorations diff --git a/components/admin-trade-workspace.tsx b/components/admin-trade-workspace.tsx index 2c41ab6..155a35c 100644 --- a/components/admin-trade-workspace.tsx +++ b/components/admin-trade-workspace.tsx @@ -24,6 +24,7 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { formatAdminDate, formatAdminDateTime } from "@/lib/admin-format"; +import { cn, pressableClasses } from "@/lib/utils"; import type { AdminPanelUser } from "@/components/admin-access"; import { normalizePhoneNumber, @@ -916,11 +917,13 @@ export function AdminTradeWorkspace({ user }: { user: AdminPanelUser }) { type="button" aria-current={selectedId === lead.id} onClick={() => void openLead(lead.id)} - className={`w-full rounded-md border px-2.5 py-2 text-left text-sm ${ + className={cn( + "w-full rounded-md border px-2.5 py-2 text-left text-sm", selectedId === lead.id ? "border-ocean/60 bg-ocean/[0.07]" - : "border-transparent hover:border-stone hover:bg-stone/25" - }`} + : "border-transparent hover:border-stone hover:bg-stone/25", + pressableClasses + )} >
diff --git a/components/admin-workspace.tsx b/components/admin-workspace.tsx index 8472fda..ba41b3e 100644 --- a/components/admin-workspace.tsx +++ b/components/admin-workspace.tsx @@ -1,6 +1,7 @@ "use client"; import type { Dispatch, SetStateAction } from "react"; +import { cn, pressableClasses } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { @@ -236,9 +237,13 @@ export function AdminWorkspace({ type="button" aria-current={selectedBeerSlug === beer.slug} onClick={() => onSelectBeer(beer.slug)} - className={`w-full rounded-md px-2 py-1.5 text-left text-sm ${ - selectedBeerSlug === beer.slug ? "bg-stone/40" : "hover:bg-stone/20" - }`} + className={cn( + "w-full rounded-md px-2 py-1.5 text-left text-sm", + selectedBeerSlug === beer.slug + ? "bg-stone/40" + : "hover:bg-stone/20", + pressableClasses + )} > {beer.name} @@ -352,9 +357,13 @@ export function AdminWorkspace({ type="button" aria-current={selectedVenueSlug === venue.slug} onClick={() => onSelectVenue(venue.slug)} - className={`w-full rounded-md px-2 py-1.5 text-left text-sm ${ - selectedVenueSlug === venue.slug ? "bg-stone/40" : "hover:bg-stone/20" - }`} + className={cn( + "w-full rounded-md px-2 py-1.5 text-left text-sm", + selectedVenueSlug === venue.slug + ? "bg-stone/40" + : "hover:bg-stone/20", + pressableClasses + )} > {venue.name} @@ -414,7 +423,7 @@ export function AdminWorkspace({
- + Carries Beers

@@ -441,7 +450,7 @@ export function AdminWorkspace({

- + On Tap

@@ -468,7 +477,7 @@ export function AdminWorkspace({

- + In Can

diff --git a/components/beer-card.tsx b/components/beer-card.tsx index b0d8f73..58889c9 100644 --- a/components/beer-card.tsx +++ b/components/beer-card.tsx @@ -1,7 +1,7 @@ import Image from "next/image"; import Link from "next/link"; import { Badge } from "@/components/ui/badge"; -import { beerImageUrl } from "@/lib/utils"; +import { beerImageUrl, cn, pressableClasses } from "@/lib/utils"; import type { Beer } from "@/lib/types"; interface BeerCardProps { @@ -15,7 +15,10 @@ export function BeerCard({ beer, imageUrl, priority }: BeerCardProps) { return (

Learn More diff --git a/components/mobile-menu.tsx b/components/mobile-menu.tsx index 0e181a4..cf4c189 100644 --- a/components/mobile-menu.tsx +++ b/components/mobile-menu.tsx @@ -2,6 +2,7 @@ import { useEffect, useId, useState } from "react"; import Link from "next/link"; +import { cn, pressableClasses } from "@/lib/utils"; const navLinks = [ { href: "/beers", label: "Beers" }, @@ -42,7 +43,10 @@ export function MobileMenu({ variant = "dark" }: MobileMenuProps) {
@@ -200,7 +203,10 @@ export function TourDatePicker({ : { year: v.year, month: v.month + 1 } ) } - className="inline-flex h-8 w-8 items-center justify-center rounded-md text-ink/70 transition-colors hover:bg-stone focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ocean/50" + className={cn( + "inline-flex h-8 w-8 items-center justify-center rounded-md text-ink/70 hover:bg-stone focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ocean/50", + pressableClasses + )} >