const iconPicker = { currentPack: 'all', searchQuery: '', selectedIcon: null, onSelect: null, loadedPacks: {}, currentLetter: '', currentPage: 1, itemsPerPage: 200, packs: { lucide: { name: 'Lucide', color: '#F56565', prefix: 'lucide' }, tabler: { name: 'Tabler', color: '#4299E1', prefix: 'tabler' }, 'material-symbols': { name: 'Material', color: '#48BB78', prefix: 'material-symbols' }, mdi: { name: 'MDI', color: '#A855F7', prefix: 'mdi' }, 'simple-icons': { name: 'Simple Icons', color: '#10B981', prefix: 'simple-icons' }, business: { name: 'Business', color: '#F59E0B', prefix: null } }, icons: { lucide: [], tabler: [], 'material-symbols': [], mdi: [], 'simple-icons': [], // Business icons use explicit icon keys across multiple collections. // This avoids silent failures when an app is not available in one collection. business: [ 'simple-icons:mysql', 'simple-icons:mariadb', 'simple-icons:postgresql', 'simple-icons:mongodb', 'simple-icons:redis', 'simple-icons:sqlite', 'simple-icons:docker', 'simple-icons:kubernetes', 'simple-icons:portainer', 'simple-icons:traefikproxy', 'simple-icons:nginx', 'simple-icons:apache', 'simple-icons:plex', 'simple-icons:jellyfin', 'simple-icons:emby', 'cbi:sonarr', 'cbi:radarr', 'cbi:prowlarr', 'simple-icons:nextcloud', 'simple-icons:owncloud', 'simple-icons:seafile', 'simple-icons:syncthing', 'simple-icons:grafana', 'simple-icons:prometheus', 'simple-icons:influxdb', 'simple-icons:elasticsearch', 'simple-icons:kibana', 'simple-icons:homeassistant', 'simple-icons:nodered', 'simple-icons:eclipsemosquitto', 'simple-icons:zigbee2mqtt', 'simple-icons:pihole', 'simple-icons:adguard', 'simple-icons:wireguard', 'simple-icons:openvpn', 'simple-icons:gitlab', 'simple-icons:gitea', 'simple-icons:github', 'simple-icons:bitbucket', 'simple-icons:jenkins', 'simple-icons:ansible', 'simple-icons:terraform', 'simple-icons:vault', 'simple-icons:wordpress', 'simple-icons:ghost', 'simple-icons:drupal', 'simple-icons:joomla', 'simple-icons:phpmyadmin', 'simple-icons:adminer', 'simple-icons:bitwarden', 'simple-icons:vaultwarden', 'simple-icons:keycloak', 'simple-icons:authelia', 'simple-icons:transmission', 'simple-icons:qbittorrent', 'simple-icons:deluge', 'simple-icons:frigate', 'simple-icons:immich', 'simple-icons:piwigo', 'simple-icons:calibreweb', 'simple-icons:truenas', 'simple-icons:openmediavault', 'simple-icons:cockpit', 'simple-icons:webmin', 'cbi:proxmox', 'simple-icons:vmware', 'simple-icons:unraid', 'simple-icons:opnsense', 'simple-icons:pfsense' ] }, show(onSelectCallback, currentIconKey = null) { this.onSelect = onSelectCallback; this.selectedIcon = currentIconKey; this.searchQuery = ''; this.currentPack = 'all'; const modal = document.getElementById('icon-picker-modal'); if (!modal) { this.createModal(); } this.render(); document.getElementById('icon-picker-modal').classList.remove('hidden'); document.getElementById('icon-picker-search').focus(); }, close() { document.getElementById('icon-picker-modal').classList.add('hidden'); }, async ensurePackLoaded(pack) { if (pack === 'business' || pack === 'all') return; if (this.loadedPacks[pack]) return; const packConfig = this.packs[pack]; if (!packConfig || !packConfig.prefix) return; this.loadedPacks[pack] = 'loading'; try { const url = `https://api.iconify.design/collection?prefix=${packConfig.prefix}`; const res = await fetch(url, { headers: { 'Accept': 'application/json' } }); if (!res.ok) throw new Error(`Iconify API error: ${res.status}`); const data = await res.json(); const set = new Set(); if (Array.isArray(data.uncategorized)) { data.uncategorized.forEach((name) => { if (typeof name === 'string' && name) set.add(name); }); } if (data && typeof data.categories === 'object' && data.categories) { Object.values(data.categories).forEach((arr) => { if (!Array.isArray(arr)) return; arr.forEach((name) => { if (typeof name === 'string' && name) set.add(name); }); }); } this.icons[pack] = Array.from(set).sort(); this.loadedPacks[pack] = true; } catch (e) { console.warn(`Failed to load ${pack} icons from Iconify API:`, e); this.loadedPacks[pack] = false; } }, createModal() { const modalHtml = `
`; document.body.insertAdjacentHTML('beforeend', modalHtml); this.renderAlphabetFilter(); document.getElementById('icon-picker-modal').addEventListener('click', (e) => { if (e.target.id === 'icon-picker-modal') { this.close(); } }); }, renderAlphabetFilter() { const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''); const container = document.getElementById('icon-picker-alphabet'); if (!container) return; const buttons = alphabet.map(letter => ` `).join(''); container.innerHTML = ` ${buttons} `; }, handleSearch(event) { this.searchQuery = event.target.value.toLowerCase(); this.render(); }, setFilter(pack) { this.currentPack = pack; this.currentPage = 1; document.querySelectorAll('#icon-picker-filters button').forEach(btn => { if (btn.dataset.filter === pack) { btn.className = 'px-3 py-1.5 rounded-lg text-xs font-medium transition-colors bg-purple-600 text-white'; } else { btn.className = 'px-3 py-1.5 rounded-lg text-xs font-medium transition-colors bg-gray-700 text-gray-300 hover:bg-gray-600'; } }); this.render(); }, setLetter(letter) { this.currentLetter = letter.toLowerCase(); this.currentPage = 1; document.querySelectorAll('#icon-picker-alphabet button').forEach(btn => { if (btn.dataset.letter === letter) { btn.className = 'px-2 py-1 rounded text-xs font-medium transition-colors bg-purple-600 text-white'; } else { btn.className = 'px-2 py-1 rounded text-xs font-medium transition-colors bg-gray-700 text-gray-300 hover:bg-gray-600'; } }); this.render(); }, setPage(page) { this.currentPage = page; this.render(); }, getFilteredIcons() { const result = []; const packs = this.currentPack === 'all' ? Object.keys(this.icons) : [this.currentPack]; packs.forEach(pack => { const icons = this.icons[pack] || []; icons.forEach(icon => { const isFullKey = typeof icon === 'string' && icon.includes(':'); const iconKey = isFullKey ? icon : `${pack}:${icon}`; const iconName = isFullKey ? icon.split(':').slice(1).join(':') : icon; const q = this.searchQuery; const letter = this.currentLetter; const matchesSearch = !q || iconName.includes(q) || iconKey.includes(q); const matchesLetter = !letter || iconName.toLowerCase().startsWith(letter); if (matchesSearch && matchesLetter) { result.push({ pack, icon: iconName, key: iconKey }); } }); }); return result; }, getPaginatedIcons(allIcons) { const start = (this.currentPage - 1) * this.itemsPerPage; const end = start + this.itemsPerPage; return { icons: allIcons.slice(start, end), total: allIcons.length, totalPages: Math.ceil(allIcons.length / this.itemsPerPage) }; }, renderPagination(totalPages, total) { const container = document.getElementById('icon-picker-pagination'); if (!container || totalPages <= 1) { if (container) container.innerHTML = ''; return; } const maxButtons = 7; let startPage = Math.max(1, this.currentPage - Math.floor(maxButtons / 2)); let endPage = Math.min(totalPages, startPage + maxButtons - 1); if (endPage - startPage < maxButtons - 1) { startPage = Math.max(1, endPage - maxButtons + 1); } let html = ` `; if (startPage > 1) { html += ` `; if (startPage > 2) { html += '...'; } } for (let i = startPage; i <= endPage; i++) { html += ` `; } if (endPage < totalPages) { if (endPage < totalPages - 1) { html += '...'; } html += ` `; } html += ` ${total} icônes `; container.innerHTML = html; }, async render() { const packsToLoad = this.currentPack === 'all' ? Object.keys(this.packs).filter(p => p !== 'business') : [this.currentPack]; await Promise.all(packsToLoad.map(pack => this.ensurePackLoaded(pack))); const grid = document.querySelector('#icon-picker-grid .grid'); const allIcons = this.getFilteredIcons(); const { icons, total, totalPages } = this.getPaginatedIcons(allIcons); if (icons.length === 0) { grid.innerHTML = `Aucune icône trouvée