MicProtocol / packages /ui /src /components /SaveButton.tsx
Benaah's picture
Upload folder using huggingface_hub
43842af verified
Raw History Blame Contribute Delete
2.96 kB
import React from "react";
import { cn } from "../lib/cn";
export interface SaveButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
/** Whether the item is currently saved/pinned */
saved?: boolean;
/** Size of the save button */
size?: "sm" | "md" | "lg";
/** Label text shown next to the icon (default: hidden) */
label?: string;
}
const sizeMap: Record<string, { width: string; height: string; iconSize: string }> = {
sm: { width: "32px", height: "32px", iconSize: "16px" },
md: { width: "40px", height: "40px", iconSize: "20px" },
lg: { width: "48px", height: "48px", iconSize: "24px" },
};
/**
* SaveButton — Pinterest-style save/bookmark ("pin") toggle.
*
* Micro-interactions:
* - Hover: icon scales up slightly, brand color tint appears
* - Click: icon fills with brand color, scales up then springs back
* - Saved state: icon stays filled with brand color
*/
export const SaveButton = React.forwardRef<HTMLButtonElement, SaveButtonProps>(
(
{ saved = false, size = "md", label, className, style, disabled, ...props },
ref,
) => {
const s = sizeMap[size] || sizeMap.md;
return (
<button
ref={ref}
className={cn(
"relative inline-flex items-center justify-center overflow-hidden",
saved
? "bg-[var(--color-primary)] text-[var(--text-inverse)] border-[var(--color-primary)]"
: "bg-transparent text-[var(--text-secondary)] border-[var(--border-color)] hover:bg-[var(--bg-tertiary)] hover:text-[var(--color-primary)] hover:border-[var(--color-primary)]",
disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer active:scale-95",
"transition-all duration-200 ease-[cubic-bezier(0.16,1,0.3,1)] rounded-full",
className,
)}
style={{
width: s.width,
height: s.height,
...style,
}}
disabled={disabled}
aria-pressed={saved}
aria-label={label || (saved ? "Unsave" : "Save")}
{...props}
>
<span
className="pointer-events-none"
style={{ fontSize: s.iconSize, lineHeight: 1 }}
>
{saved ? "★" : "☆"}
</span>
{label && (
<span
className="ml-[var(--space-1)] text-[var(--text-xs)] font-medium whitespace-nowrap"
style={{ color: saved ? "var(--text-primary)" : "var(--text-secondary)" }}
>
{label}
</span>
)}
{/* Saved-state fill animation ring */}
{saved && (
<span
className="absolute inset-0 rounded-full border-[2px] border-[var(--color-primary-light)] opacity-0 saved:ring-animate"
style={{
animation: "pinPulse 0.6s cubic-bezier(0.16,1,0.3,1) forwards",
pointerEvents: "none",
}}
/>
)}
</button>
);
},
);
SaveButton.displayName = "SaveButton";