All files / src/components TypeCustomizePopover.tsx

95.83% Statements 23/24
100% Branches 10/10
92.3% Functions 12/13
95.23% Lines 20/21

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124              10x 2x 572x                                   12x 12x 12x   12x   12x 1x 1x     12x 1x 1x     12x       3x             96x             1x                                   2x                           2867x                 1x                                          
import { useState, useMemo } from 'react'
import { MagnifyingGlass } from '@phosphor-icons/react'
import { ICON_OPTIONS, type IconEntry } from '../utils/iconRegistry'
import { ACCENT_COLORS } from '../utils/typeColors'
import { cn } from '@/lib/utils'
 
function filterIcons(icons: IconEntry[], query: string): IconEntry[] {
  if (!query) return icons
  const lower = query.toLowerCase()
  return icons.filter((o) => o.name.includes(lower))
}
 
interface TypeCustomizePopoverProps {
  currentIcon: string | null
  currentColor: string | null
  onChangeIcon: (icon: string) => void
  onChangeColor: (color: string) => void
  onClose: () => void
}
 
export function TypeCustomizePopover({
  currentIcon,
  currentColor,
  onChangeIcon,
  onChangeColor,
  onClose,
}: TypeCustomizePopoverProps) {
  const [selectedColor, setSelectedColor] = useState(currentColor)
  const [selectedIcon, setSelectedIcon] = useState(currentIcon)
  const [search, setSearch] = useState('')
 
  const filteredIcons = useMemo(() => filterIcons(ICON_OPTIONS, search), [search])
 
  const handleColorClick = (key: string) => {
    setSelectedColor(key)
    onChangeColor(key)
  }
 
  const handleIconClick = (name: string) => {
    setSelectedIcon(name)
    onChangeIcon(name)
  }
 
  return (
    <div
      className="bg-popover text-popover-foreground z-50 rounded-lg border shadow-md"
      style={{ width: 280, padding: 12 }}
      onClick={(e) => e.stopPropagation()}
      onContextMenu={(e) => e.stopPropagation()}
    >
      {/* Color section */}
      <div className="font-mono-overline mb-2 text-muted-foreground">COLOR</div>
      <div className="flex gap-2 mb-3 flex-wrap">
        {ACCENT_COLORS.map((c) => (
          <button
            key={c.key}
            className={cn(
              "flex items-center justify-center rounded-full border-2 cursor-pointer transition-all",
              selectedColor === c.key ? "border-foreground scale-110" : "border-transparent hover:scale-105",
            )}
            style={{ width: 24, height: 24, backgroundColor: c.css, border: selectedColor === c.key ? '2px solid var(--foreground)' : '2px solid transparent' }}
            onClick={() => handleColorClick(c.key)}
            title={c.label}
          />
        ))}
      </div>
 
      {/* Icon section */}
      <div className="font-mono-overline mb-2 text-muted-foreground">ICON</div>
 
      {/* Search input */}
      <div className="relative mb-2">
        <MagnifyingGlass
          size={14}
          className="absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground pointer-events-none"
        />
        <input
          type="text"
          value={search}
          onChange={(e) => setSearch(e.target.value)}
          placeholder="Search icons…"
          className="w-full rounded border border-border bg-background pl-7 pr-2 py-1 text-[12px] text-foreground placeholder:text-muted-foreground outline-none focus:border-primary"
        />
      </div>
 
      {/* Icon grid */}
      <div className="flex flex-wrap gap-1 overflow-y-auto" style={{ maxHeight: 240 }}>
        {filteredIcons.length === 0 ? (
          <div className="w-full py-6 text-center text-[12px] text-muted-foreground">
            No icons found
          </div>
        ) : (
          filteredIcons.map(({ name, Icon }) => (
            <button
              key={name}
              className={cn(
                "flex items-center justify-center rounded cursor-pointer transition-colors",
                selectedIcon === name
                  ? "bg-primary/10 text-primary"
                  : "text-muted-foreground hover:bg-accent hover:text-foreground",
              )}
              style={{ width: 30, height: 30 }}
              onClick={() => handleIconClick(name)}
              title={name}
            >
              <Icon size={16} />
            </button>
          ))
        )}
      </div>
 
      {/* Done button */}
      <div className="mt-3 flex justify-end">
        <button
          className="rounded px-3 py-1 text-[12px] font-medium text-muted-foreground hover:text-foreground hover:bg-accent cursor-pointer transition-colors border-none bg-transparent"
          onClick={onClose}
        >
          Done
        </button>
      </div>
    </div>
  )
}