import React, { FC, useCallback, useEffect, useMemo, useRef, useState, } from "react"; import { App, Modal } from "obsidian"; import { IconName, getIconIds } from "obsidian"; import { Input } from "@/components/atoms/Input"; import { StatusIcon } from "@/components/atoms/StatusIcon"; import { ObsidianIcon } from "@/components/atoms/ObsidianIcon"; import { createRoot, Root } from "react-dom/client"; const BATCH_SIZE = 80; interface LucideIconModalProps { initialValue?: string; onSelect: (value: string) => void; onCloseRequest: () => void; } const LucideIconModalContent: FC = ({ initialValue = "", onSelect, onCloseRequest, }) => { const [query, setQuery] = useState(""); const [visibleCount, setVisibleCount] = useState(BATCH_SIZE); const [showLabels, setShowLabels] = useState(true); const listRef = useRef(null); const iconIds = useMemo(() => { try { return getIconIds() .slice() .sort((a, b) => a.localeCompare(b)); } catch (error) { console.error("Failed to fetch Lucide icon list", error); return [] as IconName[]; } }, []); const computeFuzzyScore = useCallback( (needle: string, haystack: string): number | null => { let lastIndex = -1; let score = 0; for (let i = 0; i < needle.length; i += 1) { const char = needle[i]; const idx = haystack.indexOf(char, lastIndex + 1); if (idx === -1) { return null; } score += idx - lastIndex; lastIndex = idx; } return score + (haystack.length - lastIndex); }, [], ); const filteredIcons = useMemo(() => { if (!iconIds.length) { return []; } const normalized = query.trim().toLowerCase(); if (!normalized.length) { return iconIds; } const matches = iconIds .map((name) => { const target = name.toLowerCase(); const score = computeFuzzyScore(normalized, target); return score === null ? null : { name, score }; }) .filter( ( entry, ): entry is { name: string; score: number; } => entry !== null, ) .sort((a, b) => a.score - b.score || a.name.localeCompare(b.name)); return matches.map((entry) => entry.name); }, [iconIds, query, computeFuzzyScore]); const visibleIcons = useMemo( () => filteredIcons.slice(0, visibleCount), [filteredIcons, visibleCount], ); useEffect(() => { setVisibleCount(BATCH_SIZE); listRef.current?.scrollTo({ top: 0 }); }, [query, filteredIcons.length]); const handleScroll = useCallback( (event: React.UIEvent) => { const target = event.currentTarget; if ( target.scrollTop + target.clientHeight >= target.scrollHeight - 64 ) { setVisibleCount((prev) => Math.min(prev + BATCH_SIZE, filteredIcons.length), ); } }, [filteredIcons.length], ); const handleSelect = (iconName: string) => { onSelect(iconName); onCloseRequest(); }; return (

Select Lucide icon

Showing {visibleIcons.length} of {filteredIcons.length}{" "} ( Total {iconIds.length})

{visibleIcons.map((iconName) => ( ))} {visibleIcons.length === 0 && (
No icons match “{query.trim()}”.
)}
); }; type ModalOptions = { initialValue?: string; onSelect: (value: string) => void; }; export class LucideIconModal extends Modal { private root: Root | null = null; private readonly options: ModalOptions; constructor(app: App, options: ModalOptions) { super(app); this.options = options; } onOpen(): void { this.contentEl.empty(); this.contentEl.addClass("lucide-icon-modal-wrapper"); this.root = createRoot(this.contentEl); this.root.render( this.close()} />, ); } onClose(): void { this.root?.unmount(); this.root = null; this.contentEl.empty(); } }