Polish pass: micro-interactions on admin and storefront pages

Admin design system:
- AdminCard: hover shadow elevation
- AdminTable: enhanced empty state, row hover
- AdminBadge: tracking-wide typography
- AdminButton: active scale press feedback
- AdminFormElements: input transitions
- AdminFilterBar: search input polish
- PageHeader: icon hover scale, chevron separator
- AdminToggle: glow effect, thumb shadow
- AdminEmptyState: icon hover, spacing
- AdminPagination: active solid bg, ellipsis

Storefronts (tuxedo + indian-river-direct):
- Button hover lifts (-translate-y-0.5)
- Shadow elevations on hover
- Button press feedback
- Consistent timing (duration-200)
- Product card image zoom
- FAQ empty state fix
This commit is contained in:
2026-06-02 14:29:27 +00:00
parent 8dee63fef2
commit d031a9783c
17 changed files with 386 additions and 112 deletions
@@ -59,7 +59,8 @@ export default function AdminButton({
inline-flex items-center justify-center font-semibold
transition-all duration-200 cursor-pointer
disabled:opacity-50 disabled:cursor-not-allowed
shadow-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2
focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2
active:scale-[0.98]
`;
const variantClass = variantClasses[variant];
@@ -12,7 +12,7 @@ type AdminCardProps = {
export default function AdminCard({ children, className = "", noPadding = false, style }: AdminCardProps) {
return (
<div
className={`rounded-2xl border bg-white shadow-[var(--admin-shadow-sm)] ${noPadding ? "" : "p-5"} ${className}`}
className={`rounded-2xl border bg-white shadow-[var(--admin-shadow-sm)] transition-all duration-200 hover:shadow-[var(--admin-shadow-md)] ${noPadding ? "" : "p-5"} ${className}`}
style={{
borderColor: 'var(--admin-border)',
...style
@@ -20,17 +20,17 @@ export default function AdminEmptyState({
return (
<div className={`flex flex-col items-center justify-center py-16 px-4 text-center ${className}`}>
{icon ? (
<div className="mb-4 text-[var(--admin-text-muted)]">{icon}</div>
<div className="mb-4 text-[var(--admin-text-muted)] opacity-70">{icon}</div>
) : (
<div className="mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-[var(--admin-bg-subtle)]">
<div className="mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-[var(--admin-bg-subtle)] transition-transform duration-200 hover:scale-105">
<svg className="h-8 w-8 text-[var(--admin-text-muted)]" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
</svg>
</div>
)}
<h3 className="text-lg font-semibold text-[var(--admin-text-primary)]">{title}</h3>
{description && <p className="mt-1 text-sm text-[var(--admin-text-muted)] max-w-sm">{description}</p>}
{action && <div className="mt-4">{action}</div>}
{description && <p className="mt-2 text-sm text-[var(--admin-text-muted)] max-w-sm leading-relaxed">{description}</p>}
{action && <div className="mt-6">{action}</div>}
</div>
);
}
@@ -42,7 +42,7 @@ export default function AdminFilterBar({
const options = statusOptions ?? defaultOptions;
return (
<div className="rounded-2xl border border-[var(--admin-border)] bg-white p-4 shadow-[var(--admin-shadow-sm)]">
<div className="rounded-2xl border border-[var(--admin-border)] bg-white p-4 shadow-[var(--admin-shadow-sm)] transition-shadow duration-200 hover:shadow-[var(--admin-shadow-md)]">
<div className="flex flex-wrap items-center gap-3">
{/* Search */}
<div className="relative flex-1 min-w-[12rem]">
@@ -54,7 +54,7 @@ export default function AdminFilterBar({
placeholder={searchPlaceholder}
value={search}
onChange={(e) => onSearchChange(e.target.value)}
className="w-full rounded-xl border border-[var(--admin-border)] bg-white py-2 pl-10 pr-4 text-sm text-[var(--admin-text-primary)] outline-none focus:border-[var(--admin-accent)] placeholder:text-[var(--admin-text-muted)]"
className="w-full rounded-xl border border-[var(--admin-border)] bg-[var(--admin-bg-subtle)] py-2 pl-10 pr-4 text-sm text-[var(--admin-text-primary)] outline-none focus:border-[var(--admin-accent)] focus:bg-white focus:ring-2 focus:ring-[var(--admin-accent)]/20 placeholder:text-[var(--admin-text-muted)] transition-all duration-150"
/>
</div>
@@ -58,7 +58,7 @@ export function AdminTextInput({
value={value}
onChange={onChange}
disabled={disabled}
className={`w-full rounded-xl border border-[var(--admin-border)] bg-[var(--admin-card-bg)] px-3 py-2.5 text-sm text-[var(--admin-text-primary)] outline-none focus:border-[var(--admin-accent)] focus:ring-2 focus:ring-[var(--admin-accent)]/20 placeholder:text-[var(--admin-text-muted)] disabled:opacity-50 ${className}`}
className={`w-full rounded-xl border border-[var(--admin-border)] bg-[var(--admin-card-bg)] px-3 py-2.5 text-sm text-[var(--admin-text-primary)] outline-none focus:border-[var(--admin-accent)] focus:ring-2 focus:ring-[var(--admin-accent)]/20 placeholder:text-[var(--admin-text-muted)] disabled:opacity-50 transition-all duration-150 ${className}`}
maxLength={maxLength}
autoComplete={autoComplete}
step={step}
@@ -20,7 +20,7 @@ export default function AdminPagination({
<button
onClick={() => onPageChange(currentPage - 1)}
disabled={currentPage === 0}
className="flex h-8 w-8 items-center justify-center rounded-lg border border-[var(--admin-border)] text-[var(--admin-text-muted)] hover:text-[var(--admin-text-secondary)] hover:bg-[var(--admin-bg-subtle)] disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
className="flex h-8 w-8 items-center justify-center rounded-lg border border-[var(--admin-border)] text-[var(--admin-text-muted)] hover:text-[var(--admin-text-secondary)] hover:bg-[var(--admin-bg-subtle)] disabled:opacity-30 disabled:cursor-not-allowed transition-all duration-150"
>
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
@@ -43,8 +43,8 @@ export default function AdminPagination({
if (showEllipsisBefore || showEllipsisAfter) {
return (
<span key={`ellipsis-${page}`} className="px-2 text-[var(--admin-text-muted)]">
...
<span key={`ellipsis-${page}`} className="px-3 py-2 text-[var(--admin-text-muted)] text-sm">
&hellip;
</span>
);
}
@@ -53,10 +53,10 @@ export default function AdminPagination({
<button
key={page}
onClick={() => onPageChange(page)}
className={`flex h-8 min-w-[2rem] items-center justify-center rounded-lg text-sm font-medium transition-colors ${
className={`flex h-8 min-w-[2rem] items-center justify-center rounded-lg text-sm font-medium transition-all duration-150 ${
page === currentPage
? "bg-[var(--admin-accent-light)] text-[var(--admin-accent-text)] border border-[var(--admin-accent)]"
: "text-[var(--admin-text-secondary)] hover:bg-[var(--admin-bg-subtle)]"
? "bg-[var(--admin-accent)] text-white shadow-md"
: "text-[var(--admin-text-secondary)] hover:bg-[var(--admin-bg-subtle)] border border-transparent hover:border-[var(--admin-border)]"
}`}
>
{page + 1}
@@ -68,7 +68,7 @@ export default function AdminPagination({
<button
onClick={() => onPageChange(currentPage + 1)}
disabled={currentPage >= totalPages - 1}
className="flex h-8 w-8 items-center justify-center rounded-lg border border-[var(--admin-border)] text-[var(--admin-text-muted)] hover:text-[var(--admin-text-secondary)] hover:bg-[var(--admin-bg-subtle)] disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
className="flex h-8 w-8 items-center justify-center rounded-lg border border-[var(--admin-border)] text-[var(--admin-text-muted)] hover:text-[var(--admin-text-secondary)] hover:bg-[var(--admin-bg-subtle)] disabled:opacity-30 disabled:cursor-not-allowed transition-all duration-150"
>
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
@@ -27,7 +27,7 @@ export default function AdminTable<T extends Record<string, unknown>>({
className = "",
}: AdminTableProps<T>) {
return (
<div className={`overflow-x-auto rounded-2xl border border-[var(--admin-border)] bg-white shadow-[var(--admin-shadow-sm)] ${className}`}>
<div className={`overflow-x-auto rounded-2xl border border-[var(--admin-border)] bg-white shadow-[var(--admin-shadow-sm)] transition-shadow duration-200 hover:shadow-[var(--admin-shadow-md)] ${className}`}>
<table className="w-full text-left text-sm min-w-[600px]">
<thead>
<tr className="border-b border-[var(--admin-border)] bg-[var(--admin-bg-subtle)]">
@@ -42,15 +42,21 @@ export default function AdminTable<T extends Record<string, unknown>>({
{data.length === 0 ? (
<tr>
<td colSpan={columns.length} className="px-5 py-16 text-center text-sm text-[var(--admin-text-muted)]">
{emptyMessage}
<div className="flex flex-col items-center gap-2">
<svg className="w-8 h-8 opacity-40" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="1">
<path strokeLinecap="round" strokeLinejoin="round" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
</svg>
{emptyMessage}
</div>
</td>
</tr>
) : (
data.map((item) => (
data.map((item, index) => (
<tr
key={keyExtractor(item)}
className={`hover:bg-[var(--admin-bg-subtle)]/40 transition-colors ${onRowClick ? "cursor-pointer" : ""}`}
className={`group hover:bg-[var(--admin-bg-subtle)]/40 transition-all duration-150 ${onRowClick ? "cursor-pointer" : ""}`}
onClick={() => onRowClick?.(item)}
style={{ animationDelay: `${index * 30}ms` }}
>
{columns.map((col) => (
<td key={col.key} className={`px-5 py-3.5 ${col.className ?? ""}`}>
@@ -42,14 +42,14 @@ export function AdminToggle({
aria-checked={checked}
disabled={disabled}
onClick={() => !disabled && onChange(!checked)}
className={`relative inline-flex shrink-0 items-center rounded-full transition-colors ${
className={`relative inline-flex shrink-0 items-center rounded-full transition-all duration-200 ease-out ${
checked
? "bg-[var(--admin-accent)]"
? "bg-[var(--admin-accent)] shadow-[0_0_8px_rgba(202,117,67,0.4)]"
: "bg-[var(--admin-text-muted)]"
} ${classes.track}`}
>
<span
className={`inline-block rounded-full bg-white shadow transition-transform ${classes.thumb} ${classes.translate}`}
className={`inline-block rounded-full bg-white shadow-md transition-transform duration-200 ease-out ${classes.thumb} ${classes.translate}`}
/>
</button>
{(label || description) && (
@@ -36,7 +36,7 @@ export default function PageHeader({
{crumb.href ? (
<Link
href={crumb.href}
className="hover:text-[var(--admin-text-secondary)] transition-colors"
className="hover:text-[var(--admin-text-secondary)] transition-colors duration-150"
>
{crumb.label}
</Link>
@@ -44,7 +44,9 @@ export default function PageHeader({
<span className="text-[var(--admin-text-secondary)]">{crumb.label}</span>
)}
{index < breadcrumb.length - 1 && (
<span className="text-[var(--admin-text-muted)]">/</span>
<svg className="w-3 h-3 opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg>
)}
</span>
))}
@@ -56,7 +58,7 @@ export default function PageHeader({
{/* Left: Icon + Title + Subtitle */}
<div className="flex items-center gap-4">
{icon && (
<div className="flex items-center justify-center w-12 h-12 rounded-2xl bg-[var(--admin-accent-light)] text-[var(--admin-accent)] shadow-[var(--admin-shadow-sm)]">
<div className="flex items-center justify-center w-12 h-12 rounded-2xl bg-[var(--admin-accent-light)] text-[var(--admin-accent)] shadow-[var(--admin-shadow-sm)] transition-transform duration-200 hover:scale-105">
{icon}
</div>
)}