{"version":3,"file":"Spinner.mjs","sources":["../../../../src/lib/ui/feedback/Spinner.tsx"],"sourcesContent":["/**\n * @file Spinner Component\n * @description Standard loading spinner with memoization for performance\n * Performance optimized: static styles extracted, dynamic styles memoized\n * Uses theme tokens for consistent styling and z-index management\n */\n\nimport React, { memo, useRef, useEffect, useCallback, useMemo, type CSSProperties } from 'react';\nimport { tokens, colorTokens } from '../../theme/tokens';\n\n/**\n * Spinner size options\n */\nexport type SpinnerSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';\n\n/**\n * Spinner variant options\n */\nexport type SpinnerVariant = 'default' | 'primary' | 'secondary' | 'white';\n\n/**\n * Spinner props\n */\nexport interface SpinnerProps {\n  /** Spinner size */\n  size?: SpinnerSize;\n\n  /** Spinner color variant */\n  variant?: SpinnerVariant;\n\n  /** Custom color */\n  color?: string;\n\n  /** Accessible label */\n  label?: string;\n\n  /** Additional class name */\n  className?: string;\n\n  /** Center in container */\n  centered?: boolean;\n}\n\n// ============================================================================\n// STATIC STYLES AND CONFIGURATION\n// ============================================================================\n\n/**\n * Size mappings\n */\nconst sizeMap: Record<SpinnerSize, { size: number; border: number }> = {\n  xs: { size: 12, border: 2 },\n  sm: { size: 16, border: 2 },\n  md: { size: 24, border: 3 },\n  lg: { size: 32, border: 4 },\n  xl: { size: 48, border: 4 },\n};\n\n/**\n * Color mappings - using theme tokens\n */\nconst colorMap: Record<SpinnerVariant, string> = {\n  default: colorTokens.neutral[500],\n  primary: colorTokens.primary.default,\n  secondary: colorTokens.secondary.default,\n  white: colorTokens.neutral.white,\n};\n\nconst srOnlyStyle: CSSProperties = {\n  position: 'absolute',\n  width: '1px',\n  height: '1px',\n  padding: 0,\n  margin: '-1px',\n  overflow: 'hidden',\n  clip: 'rect(0, 0, 0, 0)',\n  whiteSpace: 'nowrap',\n  border: 0,\n};\n\nconst centeredContainerStyle: CSSProperties = {\n  display: 'flex',\n  alignItems: 'center',\n  justifyContent: 'center',\n  width: '100%',\n  height: '100%',\n  minHeight: '100px',\n};\n\nconst spinnerWithTextContainerBaseStyle: CSSProperties = {\n  display: 'inline-flex',\n  alignItems: 'center',\n  gap: '0.5rem',\n};\n\nconst overlayTextStyle: CSSProperties = {\n  marginTop: tokens.spacing.md,\n  color: colorTokens.text.secondary,\n  fontSize: tokens.fontSize.sm,\n};\n\n// Keyframe animation styles - only injected once\nconst keyframeStyles = `\n  @keyframes spin {\n    to {\n      transform: rotate(360deg);\n    }\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    [role=\"status\"] {\n      animation-duration: 1.5s !important;\n      animation-timing-function: steps(8) !important;\n    }\n  }\n`;\n\n// Focus styles for LoadingOverlay - ensures WCAG 2.4.7 compliance\n// Note: Using CSS variable fallback for theme token\nconst loadingOverlayFocusStyles = `\n  [role=\"alertdialog\"]:focus {\n    outline: 2px solid ${colorTokens.border.focus};\n    outline-offset: -2px;\n  }\n\n  [role=\"alertdialog\"]:focus:not(:focus-visible) {\n    outline: none;\n  }\n\n  [role=\"alertdialog\"]:focus-visible {\n    outline: 2px solid ${colorTokens.border.focus};\n    outline-offset: -2px;\n  }\n`;\n\n// ============================================================================\n// COMPONENTS\n// ============================================================================\n\n/**\n * Spinner component - memoized for performance\n */\nexport const Spinner = memo(\n  ({\n    size = 'md',\n    variant = 'default',\n    color,\n    label = 'Loading...',\n    className,\n    centered = false,\n  }: SpinnerProps): React.ReactElement => {\n    const { size: dimension, border } = sizeMap[size];\n    const spinnerColor = color ?? colorMap[variant];\n\n    // Memoize spinner style\n    const spinnerStyle = useMemo(\n      (): CSSProperties => ({\n        width: dimension,\n        height: dimension,\n        border: `${border}px solid transparent`,\n        borderTopColor: spinnerColor,\n        borderRadius: '50%',\n        animation: 'spin 0.75s linear infinite',\n      }),\n      [dimension, border, spinnerColor]\n    );\n\n    const spinner = (\n      <div role=\"status\" aria-label={label} className={className} style={spinnerStyle}>\n        <span style={srOnlyStyle}>{label}</span>\n\n        {/* Keyframe animation with reduced motion support */}\n        <style>{keyframeStyles}</style>\n      </div>\n    );\n\n    if (centered) {\n      return <div style={centeredContainerStyle}>{spinner}</div>;\n    }\n\n    return spinner;\n  }\n);\n\nSpinner.displayName = 'Spinner';\n\n/**\n * Inline spinner with text - memoized for performance\n */\nexport const SpinnerWithText = memo(\n  ({\n    text = 'Loading...',\n    size = 'sm',\n    variant = 'default',\n    className,\n  }: {\n    text?: string;\n    size?: SpinnerSize;\n    variant?: SpinnerVariant;\n    className?: string;\n  }): React.ReactElement => {\n    // Memoize text style based on size\n    const textStyle = useMemo(\n      (): CSSProperties => ({\n        fontSize: size === 'xs' ? '0.75rem' : '0.875rem',\n      }),\n      [size]\n    );\n\n    return (\n      <div className={className} style={spinnerWithTextContainerBaseStyle}>\n        <Spinner size={size} variant={variant} label={text} />\n        <span style={textStyle}>{text}</span>\n      </div>\n    );\n  }\n);\n\nSpinnerWithText.displayName = 'SpinnerWithText';\n\n/**\n * Full page loading overlay - memoized for performance\n * Includes focus trap to prevent interaction with background content\n */\nexport const LoadingOverlay = memo(\n  ({\n    visible = true,\n    text = 'Loading...',\n    blur = true,\n  }: {\n    visible?: boolean;\n    text?: string;\n    blur?: boolean;\n  }): React.ReactElement | null => {\n    const overlayRef = useRef<HTMLDivElement>(null);\n    const previousActiveElement = useRef<HTMLElement | null>(null);\n\n    // Focus trap - move focus to overlay when visible and restore when hidden\n    useEffect(() => {\n      if (visible) {\n        // Store the currently focused element\n        previousActiveElement.current = document.activeElement as HTMLElement;\n        // Focus the overlay container\n        overlayRef.current?.focus();\n      } else if (previousActiveElement.current) {\n        // Restore focus when overlay is hidden\n        previousActiveElement.current.focus();\n      }\n    }, [visible]);\n\n    // Trap keyboard focus within overlay\n    const handleKeyDown = useCallback((e: React.KeyboardEvent) => {\n      // Trap Tab key within the overlay\n      if (e.key === 'Tab') {\n        e.preventDefault();\n        // Keep focus on the overlay itself since there's nothing else to focus\n        overlayRef.current?.focus();\n      }\n    }, []);\n\n    // Memoize overlay style - uses focus-visible for accessibility\n    // Note: We keep a visible focus indicator for accessibility compliance (WCAG 2.4.7)\n    // Uses theme token for z-index to maintain proper stacking context\n    const overlayStyle = useMemo(\n      (): CSSProperties => ({\n        position: 'fixed',\n        inset: 0,\n        display: 'flex',\n        flexDirection: 'column',\n        alignItems: 'center',\n        justifyContent: 'center',\n        backgroundColor: blur ? 'rgba(255, 255, 255, 0.8)' : 'rgba(255, 255, 255, 0.95)',\n        backdropFilter: blur ? 'blur(4px)' : undefined,\n        zIndex: parseInt(tokens.zIndex.modal),\n        // Focus indicator for keyboard users - inset ring that doesn't affect layout\n        // The overlay background provides visual context, but we add a subtle ring for WCAG compliance\n      }),\n      [blur]\n    );\n\n    if (!visible) return null;\n\n    return (\n      <div\n        ref={overlayRef}\n        role=\"alertdialog\"\n        aria-modal=\"true\"\n        aria-label={text || 'Loading'}\n        aria-live=\"assertive\"\n        aria-busy=\"true\"\n        tabIndex={-1}\n        onKeyDown={handleKeyDown}\n        style={overlayStyle}\n      >\n        {/* Focus styles for accessibility compliance */}\n        <style>{loadingOverlayFocusStyles}</style>\n        <Spinner size=\"lg\" variant=\"primary\" label={text} />\n        {text && <p style={overlayTextStyle}>{text}</p>}\n      </div>\n    );\n  }\n);\n\nLoadingOverlay.displayName = 'LoadingOverlay';\n"],"names":["sizeMap","colorMap","colorTokens","srOnlyStyle","centeredContainerStyle","spinnerWithTextContainerBaseStyle","overlayTextStyle","tokens","keyframeStyles","loadingOverlayFocusStyles","Spinner","memo","size","variant","color","label","className","centered","dimension","border","spinnerColor","spinnerStyle","useMemo","spinner","jsx","SpinnerWithText","text","textStyle","jsxs","LoadingOverlay","visible","blur","overlayRef","useRef","previousActiveElement","useEffect","handleKeyDown","useCallback","e","overlayStyle"],"mappings":"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"}