# 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>
      )
    }