Ctrl K
ComunidadGuías…

components/search-mode-selector.tsx

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
'use client'

import { useEffect, useState, useSyncExternalStore } from 'react'

import {
  IconCheck as Check,
  IconChevronDown as ChevronDown
} from '@tabler/icons-react'

import { SEARCH_MODE_CONFIGS } from '@/lib/config/search-modes'
import { SearchMode } from '@/lib/types/search'
import { cn } from '@/lib/utils'
import {
  getCookie,
  setCookie,
  subscribeToCookieChange
} from '@/lib/utils/cookies'

import { Button } from './ui/button'
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger
} from './ui/dropdown-menu'
import { HoverCard, HoverCardContent, HoverCardTrigger } from './ui/hover-card'

const VALID_SEARCH_MODES = new Set(['quick', 'adaptive'])

function getSearchModeSnapshot(): SearchMode {
  const savedMode = getCookie('searchMode')
  return savedMode === 'adaptive' ? 'adaptive' : 'quick'
}

interface SearchModeSelectorProps {
  isAdaptiveAuthRequired?: boolean
  onAdaptiveAuthRequired?: () => void
}

export function SearchModeSelector({
  isAdaptiveAuthRequired = false,
  onAdaptiveAuthRequired
}: SearchModeSelectorProps) {
  const value = useSyncExternalStore(
    subscribeToCookieChange,
    getSearchModeSnapshot,
    () => 'quick'
  )
  const [openHoverCard, setOpenHoverCard] = useState<string | null>(null)
  const [justSelected, setJustSelected] = useState(false)
  const [dropdownOpen, setDropdownOpen] = useState(false)

  useEffect(() => {
    const savedMode = getCookie('searchMode')
    if (savedMode && !VALID_SEARCH_MODES.has(savedMode)) {
      // Clean up invalid cookie value (e.g., old 'planning' mode)
      setCookie('searchMode', 'quick')
      return
    }

    if (isAdaptiveAuthRequired && savedMode === 'adaptive') {
      setCookie('searchMode', 'quick')
    }
  }, [isAdaptiveAuthRequired])

  const closeModeSelectControls = () => {
    setOpenHoverCard(null) // Close hover card on selection
    setDropdownOpen(false) // Close dropdown on selection
    setJustSelected(true)

    // Prevent hover card from reopening immediately
    setTimeout(() => {
      setJustSelected(false)
    }, 500)
  }

  const handleModeSelect = (mode: SearchMode) => {
    if (mode === 'adaptive' && isAdaptiveAuthRequired) {
      setCookie('searchMode', 'quick')
      closeModeSelectControls()
      onAdaptiveAuthRequired?.()
      return
    }

    setCookie('searchMode', mode)
    closeModeSelectControls()
  }

  const selectedMode = SEARCH_MODE_CONFIGS.find(
    config => config.value === value
  )
  const SelectedIcon = selectedMode?.icon
  const selectedIndex = Math.max(
    SEARCH_MODE_CONFIGS.findIndex(config => config.value === value),
    0
  )
  const modeCount = SEARCH_MODE_CONFIGS.length

  return (
    <>
      {/* Mobile Dropdown */}
      <div className="sm:hidden">
        <DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
          <DropdownMenuTrigger asChild>
            <Button
              variant="outline"
              size="sm"
              className="gap-1 rounded-full text-xs shadow-none transition-[background-color,color,box-shadow,transform]"
            >
              {SelectedIcon && (
                <SelectedIcon
                  className={cn(
                    'size-3.5 transition-colors',
                    selectedMode?.color
                  )}
                />
              )}
              <span className="text-xs font-medium">{selectedMode?.label}</span>
              <ChevronDown
                className={cn(
                  'ml-0.5 size-3 opacity-50 transition-transform duration-[160ms] ease-[var(--motion-ease-out)]',
                  dropdownOpen && 'rotate-180'
                )}
              />
            </Button>
          </DropdownMenuTrigger>
          <DropdownMenuContent align="start" className="w-64" sideOffset={5}>
            {SEARCH_MODE_CONFIGS.map(config => {
              const ModeIcon = config.icon
              const isSelected = value === config.value
              return (
                <DropdownMenuItem
                  key={config.value}
                  onClick={() => handleModeSelect(config.value)}
                  className="relative flex flex-col items-start gap-1 py-2 pl-8 pr-2 cursor-pointer focus:outline-none"
                >
                  {isSelected && (
                    <Check className="absolute left-2 top-2.5 size-4" />
                  )}
                  <div className="flex items-center gap-2">
                    <ModeIcon
                      className={cn('size-4 transition-colors', config.color)}
                    />
                    <span className="text-sm font-medium">{config.label}</span>
                  </div>
                  <div className="flex flex-col gap-0.5 ml-6">
                    <span className="text-xs text-muted-foreground">
                      {config.description}
                    </span>
                  </div>
                </DropdownMenuItem>
              )
            })}
          </DropdownMenuContent>
        </DropdownMenu>
      </div>

      {/* Desktop Toggle */}
      <div className="hidden sm:block">
        <div className="relative inline-flex items-center rounded-full bg-background border p-1">
          {/* Animated background indicator */}
          <div
            className="absolute inset-1 rounded-full bg-muted transition-[transform,width] duration-[180ms] ease-[var(--motion-ease-in-out)]"
            style={{
              width: `calc(${100 / modeCount}% - 4px)`,
              transform: `translateX(${selectedIndex * 100}%)`
            }}
          />

          {/* Mode buttons */}
          <div className="relative flex items-center">
            {SEARCH_MODE_CONFIGS.map((config, index) => {
              const Icon = config.icon
              const isSelected = value === config.value

              return (
                <HoverCard
                  key={config.value}
                  open={!justSelected && openHoverCard === config.value}
                  onOpenChange={open => {
                    if (!justSelected) {
                      setOpenHoverCard(open ? config.value : null)
                    }
                  }}
                  openDelay={100}
                  closeDelay={50}
                >
                  <HoverCardTrigger asChild>
                    <button
                      type="button"
                      onClick={() => handleModeSelect(config.value)}
                      className={cn(
                        'relative z-10 flex-1 items-center justify-center rounded-full px-3 py-2 transition-colors duration-[140ms] ease-[var(--motion-ease-out)]',
                        isSelected
                          ? 'text-foreground'
                          : 'text-muted-foreground hover:text-foreground/80'
                      )}
                      aria-label={`${config.label} mode`}
                      aria-pressed={isSelected}
                    >
                      <Icon
                        className={cn(
                          'h-3.5 w-3.5 transition-colors',
                          isSelected ? config.color : ''
                        )}
                      />
                    </button>
                  </HoverCardTrigger>

                  <HoverCardContent
                    className="w-72"
                    align="center"
                    sideOffset={8}
                  >
                    <div className="space-y-2">
                      <div className="flex items-center gap-2">
                        <Icon className={cn('size-5', config.color)} />
                        <h4 className="text-sm font-semibold">
                          {config.label}
                        </h4>
                      </div>
                      <p className="text-xs text-muted-foreground leading-tight">
                        {config.description}
                      </p>
                    </div>
                  </HoverCardContent>
                </HoverCard>
              )
            })}
          </div>
        </div>
      </div>
    </>
  )
}