Download src/components/shared/SharedButton.tsx from asnannp/DocDoeAI: direct link, hf CLI and curl.
- Browser
- Download file 11 kB
-
https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/shared/SharedButton.tsx
- Command line
-
hf download hf://spaces/asnannp/DocDoeAI/src/components/shared/SharedButton.tsx
-
curl -L -o SharedButton.tsx https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/shared/SharedButton.tsx
11 kB
| "use client"; | |
| // @ts-nocheck - pre-existing legacy design token (bare 'colors'/'spacing' etc) issues in shared components. | |
| import Link from "next/link"; | |
| import { forwardRef, ButtonHTMLAttributes, AnchorHTMLAttributes } from "react"; | |
| import { clsx } from "clsx"; | |
| import { designSystem, colors, typography, spacing } from "@/lib/design-system"; | |
| import { Loader2 } from "lucide-react"; | |
| const { semanticColors } = designSystem; // colors/radius etc reserved for variants | |
| // ============================================================================= | |
| // Button Types and Interfaces | |
| // ============================================================================= | |
| export type ButtonVariant = | |
| | "primary" | |
| | "secondary" | |
| | "ghost" | |
| | "outline" | |
| | "danger" | |
| | "success" | |
| | "emerald" | |
| | "violet" | |
| | "amber" | |
| | "blue"; | |
| export type ButtonSize = "sm" | "md" | "lg" | "xl" | "icon"; | |
| export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> { | |
| variant?: ButtonVariant; | |
| size?: ButtonSize; | |
| loading?: boolean; | |
| fullWidth?: boolean; | |
| leftIcon?: React.ReactNode; | |
| rightIcon?: React.ReactNode; | |
| asLink?: boolean; | |
| href?: string; | |
| } | |
| // ============================================================================= | |
| // Style Mappings | |
| // ============================================================================= | |
| const variantStyles = { | |
| primary: { | |
| bg: colors.accentPrimary, | |
| text: colors.white, | |
| hoverBg: colors.accentPrimaryHover, | |
| border: "none", | |
| activeBg: colors.accentPrimary, | |
| }, | |
| secondary: { | |
| bg: colors.bgSurface, | |
| text: colors.textPrimary, | |
| hoverBg: colors.bgElevated, | |
| border: colors.borderMedium, | |
| hoverBorder: colors.borderMedium, | |
| activeBg: colors.bgElevated, | |
| }, | |
| ghost: { | |
| bg: "transparent", | |
| text: colors.textSecondary, | |
| hoverBg: colors.bgGlass, | |
| border: "none", | |
| hoverBorder: "none", | |
| hoverText: colors.textPrimary, | |
| activeBg: colors.bgGlass, | |
| }, | |
| outline: { | |
| bg: "transparent", | |
| text: colors.textPrimary, | |
| hoverBg: colors.bgGlass, | |
| border: colors.borderSubtle, | |
| hoverBorder: colors.borderMedium, | |
| activeBg: colors.bgGlass, | |
| }, | |
| danger: { | |
| bg: colors.accentRed, | |
| text: colors.white, | |
| hoverBg: "#F87171", // lighter red | |
| border: "none", | |
| activeBg: colors.accentRed, | |
| }, | |
| success: { | |
| bg: colors.accentEmerald, | |
| text: colors.white, | |
| hoverBg: "#10B981", // same or lighter | |
| border: "none", | |
| activeBg: colors.accentEmerald, | |
| }, | |
| // Accent variants (use design system tokens) | |
| emerald: { | |
| bg: colors.accentEmerald, | |
| text: colors.white, | |
| hoverBg: "#34D399", // lighter emerald | |
| border: "none", | |
| activeBg: colors.accentEmerald, | |
| }, | |
| violet: { | |
| bg: semanticColors.info, | |
| text: colors.white, | |
| hoverBg: colors.accentViolet, | |
| border: "none", | |
| activeBg: semanticColors.info, | |
| }, | |
| amber: { | |
| bg: colors.accentPrimary, | |
| text: colors.white, | |
| hoverBg: colors.accentPrimaryHover, | |
| border: "none", | |
| activeBg: colors.accentPrimary, | |
| }, | |
| blue: { | |
| bg: colors.accentBlue, | |
| text: colors.white, | |
| hoverBg: "#93C5FD", // lighter blue | |
| border: "none", | |
| activeBg: colors.accentBlue, | |
| }, | |
| } as const; | |
| const sizeStyles = { | |
| sm: { | |
| height: "32px", | |
| padding: "0 12px", | |
| fontSize: typography.captionSmall.size, | |
| fontWeight: typography.captionSmall.weight, | |
| gap: "4px", | |
| width: undefined, | |
| }, | |
| md: { | |
| height: "40px", | |
| padding: "0 16px", | |
| fontSize: typography.button.size, | |
| fontWeight: typography.button.weight, | |
| gap: spacing.button.gap, | |
| width: undefined, | |
| }, | |
| lg: { | |
| height: "44px", | |
| padding: "0 24px", | |
| fontSize: typography.buttonPrimary.size, | |
| fontWeight: typography.buttonPrimary.weight, | |
| gap: spacing.button.gap, | |
| width: undefined, | |
| }, | |
| xl: { | |
| height: "48px", | |
| padding: "0 32px", | |
| fontSize: "15px", | |
| fontWeight: "600" as const, | |
| gap: "10px", | |
| width: undefined, | |
| }, | |
| icon: { | |
| height: "40px", | |
| width: "40px", | |
| padding: "0", | |
| fontSize: "20px", | |
| gap: "0", | |
| }, | |
| } as const; | |
| // Helper to extract Link-compatible props safely | |
| function extractLinkProps(props: ButtonProps): Partial<AnchorHTMLAttributes<HTMLAnchorElement>> { | |
| const { | |
| type: _type, | |
| form: _form, | |
| formAction: _formAction, | |
| formEncType: _formEncType, | |
| formMethod: _formMethod, | |
| formNoValidate: _formNoValidate, | |
| formTarget: _formTarget, | |
| name: _name, | |
| value: _value, | |
| autoFocus: _autoFocus, | |
| tabIndex: _tabIndex, | |
| ...safeProps | |
| } = props; | |
| return safeProps as Partial<AnchorHTMLAttributes<HTMLAnchorElement>>; | |
| } | |
| // ============================================================================= | |
| // SharedButton Component | |
| // ============================================================================= | |
| export const SharedButton = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonProps>( | |
| ( | |
| { | |
| variant = "primary", | |
| size = "md", | |
| loading = false, | |
| fullWidth = false, | |
| leftIcon, | |
| rightIcon, | |
| asLink = false, | |
| href, | |
| disabled, | |
| className, | |
| children, | |
| ...props | |
| }, | |
| ref, | |
| ) => { | |
| const styles = variantStyles[variant]; | |
| const sizes = sizeStyles[size]; | |
| const baseClasses = clsx( | |
| "relative z-10 pointer-events-auto", | |
| "inline-flex items-center justify-center", | |
| "border", | |
| "font-sans", | |
| "transition-all", | |
| "duration-150", | |
| "focus:outline-none", | |
| "focus-visible:ring-2", | |
| "focus-visible:ring-[var(--accent-primary)]", | |
| "focus-visible:ring-offset-2", | |
| "focus-visible:ring-offset-[var(--bg-root)]", | |
| fullWidth && "w-full", | |
| className, | |
| ); | |
| const sizeClasses = clsx( | |
| `h-[${sizes.height}]`, | |
| sizes.width && `w-[${sizes.width}]`, | |
| sizes.padding && `px-[${sizes.padding.split(" ")[0].replace("px", "")}px]`, | |
| sizes.fontSize && `text-[${sizes.fontSize}]`, | |
| "fontWeight" in sizes && (sizes as { fontWeight?: string }).fontWeight && `font-[${(sizes as { fontWeight?: string }).fontWeight}]`, | |
| sizes.gap && `gap-[${sizes.gap}]`, | |
| ); | |
| const variantClasses = clsx( | |
| !styles.bg || styles.bg === "transparent" ? "" : `bg-[${styles.bg}]`, | |
| styles.text && `text-[${styles.text}]`, | |
| !styles.border || styles.border === "none" ? "border-transparent" : `border-[${styles.border}]`, | |
| // Hover states | |
| styles.hoverBg && `hover:bg-[${styles.hoverBg}]`, | |
| ("hoverBorder" in styles && (styles as { hoverBorder?: string }).hoverBorder) && `hover:border-[${(styles as { hoverBorder?: string }).hoverBorder}]`, | |
| ("hoverText" in styles && (styles as { hoverText?: string }).hoverText) && `hover:text-[${(styles as { hoverText?: string }).hoverText}]`, | |
| // Active states | |
| styles.activeBg && `active:bg-[${styles.activeBg}]`, | |
| // Disabled states | |
| "disabled:cursor-not-allowed", | |
| "disabled:opacity-40", | |
| "disabled:transform-none", | |
| ); | |
| const allClasses = clsx(baseClasses, sizeClasses, variantClasses); | |
| // Loading state | |
| if (loading) { | |
| return ( | |
| <button | |
| ref={ref as React.RefObject<HTMLButtonElement>} | |
| type="button" | |
| disabled={true} | |
| aria-busy="true" | |
| className={clsx(allClasses, "cursor-not-allowed")} | |
| {...props} | |
| > | |
| <Loader2 className="size-4 animate-spin" aria-hidden="true" /> | |
| <span className="sr-only">Loading...</span> | |
| </button> | |
| ); | |
| } | |
| // Render as Link if asLink or href is provided | |
| if (asLink || href) { | |
| const linkProps = asLink ? { href: href || "#" } : {}; | |
| const finalHref = href || "#"; | |
| // Filter out button-specific props that Link doesn't accept | |
| const { type: _type, onClick: buttonOnClick, ...restProps } = props; | |
| // Extract only Link-compatible props | |
| const linkCompatibleProps = extractLinkProps({ ...restProps } as ButtonProps); | |
| // Safe onClick handler with proper typing | |
| const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => { | |
| if (disabled) { | |
| e.preventDefault(); | |
| return; | |
| } | |
| buttonOnClick?.(e as unknown as React.MouseEvent<HTMLButtonElement>); | |
| }; | |
| return ( | |
| <Link | |
| ref={ref as React.RefObject<HTMLAnchorElement>} | |
| href={finalHref} | |
| className={clsx(allClasses, "cursor-pointer", disabled && "pointer-events-none opacity-40")} | |
| {...linkCompatibleProps} | |
| {...linkProps} | |
| onClick={handleClick} | |
| > | |
| {leftIcon && <span aria-hidden="true">{leftIcon}</span>} | |
| {children} | |
| {rightIcon && <span aria-hidden="true">{rightIcon}</span>} | |
| </Link> | |
| ); | |
| } | |
| // Render as button | |
| return ( | |
| <button | |
| ref={ref as React.RefObject<HTMLButtonElement>} | |
| type="button" | |
| disabled={disabled} | |
| className={clsx(allClasses, !disabled && "cursor-pointer")} | |
| {...props} | |
| > | |
| {leftIcon && <span aria-hidden="true">{leftIcon}</span>} | |
| {children} | |
| {rightIcon && <span aria-hidden="true">{rightIcon}</span>} | |
| </button> | |
| ); | |
| }, | |
| ); | |
| SharedButton.displayName = "SharedButton"; | |
| // ============================================================================= | |
| // Convenience Button Components (matching existing patterns) | |
| // ============================================================================= | |
| // Button with consistent primary style | |
| export function BtnPrimary({ | |
| children, | |
| onClick, | |
| loading = false, | |
| className, | |
| ...props | |
| }: ButtonProps) { | |
| return ( | |
| <SharedButton | |
| variant="primary" | |
| onClick={onClick} | |
| loading={loading} | |
| className={clsx("rounded-lg", className)} | |
| {...props} | |
| > | |
| {children} | |
| </SharedButton> | |
| ); | |
| } | |
| // Button with secondary style | |
| export function BtnSecondary({ | |
| children, | |
| onClick, | |
| loading = false, | |
| className, | |
| ...props | |
| }: ButtonProps) { | |
| return ( | |
| <SharedButton | |
| variant="secondary" | |
| onClick={onClick} | |
| loading={loading} | |
| className={clsx("rounded-lg", className)} | |
| {...props} | |
| > | |
| {children} | |
| </SharedButton> | |
| ); | |
| } | |
| // Ghost button | |
| export function BtnGhost({ | |
| children, | |
| onClick, | |
| className, | |
| ...props | |
| }: ButtonProps) { | |
| return ( | |
| <SharedButton | |
| variant="ghost" | |
| onClick={onClick} | |
| className={clsx("rounded-lg", className)} | |
| {...props} | |
| > | |
| {children} | |
| </SharedButton> | |
| ); | |
| } | |
| // Action button (for teach page pattern) | |
| export function ActionButton({ | |
| children, | |
| primary = false, | |
| onClick, | |
| className, | |
| ...props | |
| }: ButtonProps & { primary?: boolean }) { | |
| return ( | |
| <SharedButton | |
| variant={primary ? "primary" : "secondary"} | |
| size="sm" | |
| onClick={onClick} | |
| className={clsx( | |
| "rounded-md", | |
| primary ? "text-white" : "text-[var(--text-secondary)] hover:text-[var(--text-primary)]", | |
| className, | |
| )} | |
| {...props} | |
| > | |
| {children} | |
| </SharedButton> | |
| ); | |
| } | |