Files
flaticon-uicons/explorer.js
T
ubunteroz ae66c2e34c Initial commit: Flaticon UIcons package with local font build system
Add 50,000+ icon font package sourced from Flaticon API with local
webfonts and interactive icon explorer.

Features:
- 50,492 icons across 15 style variations (weight × corner)
- Self-hosted webfonts (TTF, WOFF, WOFF2)
- Interactive icon explorer with search and filters
- FontForge-based build pipeline for generating fonts from SVGs
- Drop-in CSS with class-based icon usage

Build scripts:
- scripts/build-font.py - Standalone FontForge Python script
- build-fonts.js - Node.js orchestrator for font generation
- update-icon-list.js - Fetch icon metadata from Flaticon API
- build-icons-js.js - Generate browser-ready icon dataset

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Z.ai GLM 4.7 <noreply@z.ai>
2026-01-24 10:58:23 +08:00

500 lines
18 KiB
JavaScript

const PREFIX_CONFIG = {
rs: { label: 'Regular Straight', weight: 'regular', corner: 'straight' },
rr: { label: 'Regular Rounded', weight: 'regular', corner: 'rounded' },
bs: { label: 'Bold Straight', weight: 'bold', corner: 'straight' },
br: { label: 'Bold Rounded', weight: 'bold', corner: 'rounded' },
ss: { label: 'Solid Straight', weight: 'solid', corner: 'straight' },
sr: { label: 'Solid Rounded', weight: 'solid', corner: 'rounded' },
ts: { label: 'Thin Straight', weight: 'thin', corner: 'straight' },
tr: { label: 'Thin Rounded', weight: 'thin', corner: 'rounded' },
rc: { label: 'Regular Chubby', weight: 'regular', corner: 'chubby' },
sc: { label: 'Solid Chubby', weight: 'solid', corner: 'chubby' },
tc: { label: 'Thin Chubby', weight: 'thin', corner: 'chubby' },
ds: { label: 'Duotone Straight', weight: 'duotone', corner: 'straight' },
dr: { label: 'Duotone Rounded', weight: 'duotone', corner: 'rounded' },
dc: { label: 'Duotone Chubby', weight: 'duotone', corner: 'chubby' },
brands: { label: 'Brands', weight: 'brands', corner: 'brands' }
};
let allIcons = [];
let iconsByPrefix = {};
let availablePrefixes = new Set();
let iconNameToVariants = {};
let state = {
search: '',
weight: 'regular',
corner: 'straight',
type: 'interface',
page: 1,
perPage: 120,
iconSize: 32,
prefix: null
};
let currentModalIcon = null;
let currentModalPrefix = null;
const searchInput = document.getElementById('searchInput');
const iconGrid = document.getElementById('iconGrid');
const emptyState = document.getElementById('emptyState');
const loadingState = document.getElementById('loadingState');
const visibleCount = document.getElementById('visibleCount');
const totalCount = document.getElementById('totalCount');
const loadingIndicator = document.getElementById('loadingIndicator');
const toast = document.getElementById('toast');
const toastMessage = document.getElementById('toastMessage');
const modalOverlay = document.getElementById('modalOverlay');
const prevBtn = document.getElementById('prevBtn');
const nextBtn = document.getElementById('nextBtn');
const pageInfo = document.getElementById('pageInfo');
const pageInput = document.getElementById('pageInput');
const sizeSlider = document.getElementById('sizeSlider');
const sizeValue = document.getElementById('sizeValue');
const themeToggle = document.getElementById('themeToggle');
const modalCopyNameBtn = document.getElementById('modalCopyNameBtn');
function getCurrentPrefix() {
if (state.type === 'brands') return 'brands';
const weightMap = {
regular: { straight: 'rs', rounded: 'rr', chubby: 'rc' },
bold: { straight: 'bs', rounded: 'br', chubby: null },
solid: { straight: 'ss', rounded: 'sr', chubby: 'sc' },
thin: { straight: 'ts', rounded: 'tr', chubby: 'tc' },
duotone: { straight: 'ds', rounded: 'dr', chubby: 'dc' }
};
return weightMap[state.weight]?.[state.corner] || 'rs';
}
async function loadIcons() {
try {
if (Array.isArray(window.FLATICON_ICONS) && window.FLATICON_ICONS.length) {
allIcons = window.FLATICON_ICONS;
} else {
const response = await fetch('data/all_icons.json');
if (!response.ok) throw new Error('Failed to load icons');
allIcons = await response.json();
}
iconsByPrefix = {};
iconNameToVariants = {};
allIcons.forEach(icon => {
const prefix = icon.prefix;
const name = icon.name;
if (!iconsByPrefix[prefix]) iconsByPrefix[prefix] = [];
iconsByPrefix[prefix].push(icon);
availablePrefixes.add(prefix);
if (!iconNameToVariants[name]) iconNameToVariants[name] = new Set();
iconNameToVariants[name].add(prefix);
});
loadingState.style.display = 'none';
updateFilterAvailability();
renderIcons();
} catch (error) {
console.error('Error loading icons:', error);
loadingState.innerHTML = `
<div class="empty-icon"><span class="fi fi-rs-exclamation"></span></div>
<h3>Failed to load icons</h3>
<p>Run <code>node build-icons-js.js</code> to generate <code>data/all_icons.js</code>, then reload.</p>
`;
}
}
function updateFilterAvailability() {
const weightAvailability = {
regular: availablePrefixes.has('rs') || availablePrefixes.has('rr') || availablePrefixes.has('rc'),
bold: availablePrefixes.has('bs') || availablePrefixes.has('br'),
solid: availablePrefixes.has('ss') || availablePrefixes.has('sr') || availablePrefixes.has('sc'),
thin: availablePrefixes.has('ts') || availablePrefixes.has('tr') || availablePrefixes.has('tc'),
duotone: availablePrefixes.has('ds') || availablePrefixes.has('dr') || availablePrefixes.has('dc')
};
document.querySelectorAll('#weightFilter .filter-btn').forEach(btn => {
const weight = btn.dataset.weight;
btn.classList.toggle('disabled', !weightAvailability[weight]);
});
updateCornerAvailability();
}
function updateCornerAvailability() {
const cornerMap = {
regular: { straight: 'rs', rounded: 'rr', chubby: 'rc' },
bold: { straight: 'bs', rounded: 'br', chubby: null },
solid: { straight: 'ss', rounded: 'sr', chubby: 'sc' },
thin: { straight: 'ts', rounded: 'tr', chubby: 'tc' },
duotone: { straight: 'ds', rounded: 'dr', chubby: 'dc' }
};
const currentWeight = state.weight;
document.querySelectorAll('#cornerFilter .filter-btn').forEach(btn => {
const corner = btn.dataset.corner;
const prefix = cornerMap[currentWeight]?.[corner];
const isAvailable = prefix && availablePrefixes.has(prefix);
btn.classList.toggle('disabled', !isAvailable);
});
}
function parseSearchQuery(query) {
const terms = [];
const regex = /"([^"]+)"|(\S+)/g;
let match;
while ((match = regex.exec(query)) !== null) {
terms.push((match[1] || match[2]).toLowerCase());
}
return terms.filter(term => term.length > 0);
}
function matchesSearch(icon, terms, currentPrefix) {
if (terms.length === 0) return { matches: true, score: 0 };
const name = icon.name.toLowerCase();
const tags = icon.tags ? icon.tags.toLowerCase().split(',').map(t => t.trim()) : [];
let score = 0;
let allTermsMatch = true;
for (const term of terms) {
let termMatched = false;
if (name === term) {
score += 100;
termMatched = true;
}
else if (name.startsWith(term + '-')) {
score += 50;
termMatched = true;
}
else if (new RegExp('\\b' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b').test(name)) {
score += 25;
termMatched = true;
}
else if (name.includes(term)) {
score += 10;
termMatched = true;
}
else if (tags.some(tag => tag === term)) {
score += 20;
termMatched = true;
}
else if (tags.some(tag => tag.includes(term))) {
score += 5;
termMatched = true;
}
if (!termMatched) {
allTermsMatch = false;
}
}
if (icon.prefix === currentPrefix) {
score += 15;
}
return { matches: allTermsMatch && score > 0, score };
}
function getFilteredIcons() {
const currentPrefix = getCurrentPrefix();
let icons = iconsByPrefix[currentPrefix] || [];
if (!state.search) return icons;
const terms = parseSearchQuery(state.search);
const matchingIconNames = new Set();
for (const prefix of Object.keys(iconsByPrefix)) {
for (const icon of iconsByPrefix[prefix]) {
const result = matchesSearch(icon, terms, currentPrefix);
if (result.matches) {
matchingIconNames.add(icon.name);
}
}
}
return icons.filter(icon => matchingIconNames.has(icon.name));
}
function animateGrid() {
const cards = iconGrid.querySelectorAll('.icon-card');
cards.forEach((card, index) => {
card.style.animationDelay = `${index * 8}ms`;
card.classList.add('animate-in');
});
}
function animateModal() {
}
function renderIcons() {
const prefix = getCurrentPrefix();
const filtered = getFilteredIcons();
const start = (state.page - 1) * state.perPage;
const pageIcons = filtered.slice(start, start + state.perPage);
const totalPages = Math.ceil(filtered.length / state.perPage);
const totalIconCount = Object.values(iconsByPrefix).reduce((sum, arr) => sum + arr.length, 0);
totalCount.textContent = totalIconCount.toLocaleString();
visibleCount.textContent = filtered.length.toLocaleString();
document.documentElement.style.setProperty('--icon-size', state.iconSize + 'px');
document.documentElement.style.setProperty('--icon-card-size', Math.max(100, state.iconSize * 3.2) + 'px');
if (pageIcons.length === 0) {
iconGrid.style.display = 'none';
emptyState.style.display = 'block';
document.getElementById('pagination').style.display = 'none';
return;
}
iconGrid.style.display = 'grid';
emptyState.style.display = 'none';
document.getElementById('pagination').style.display = 'flex';
iconGrid.innerHTML = pageIcons.map(icon => {
const iconPrefix = icon.prefix || prefix;
const className = `fi-${iconPrefix}-${icon.name}`;
return `
<div class="icon-card" data-name="${icon.name}" data-prefix="${iconPrefix}">
<span class="fi ${className} icon"></span>
<div class="name">${icon.name}</div>
</div>
`;
}).join('');
animateGrid();
prevBtn.disabled = state.page === 1;
nextBtn.disabled = state.page >= totalPages;
pageInfo.textContent = `Page ${state.page} of ${Math.max(1, totalPages)}`;
pageInput.max = totalPages;
pageInput.placeholder = state.page;
}
function showToast(msg) {
toastMessage.textContent = msg;
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 2500);
}
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(() => showToast('Copied: ' + text));
}
function showModal(name, currentPrefix) {
currentModalIcon = name;
currentModalPrefix = currentPrefix;
const modalIcon = document.getElementById('modalIcon');
const modalTitle = document.getElementById('modalTitle');
const modalSubtitle = document.getElementById('modalSubtitle');
const modalVariations = document.getElementById('modalVariations');
modalTitle.textContent = name;
const availableVariants = iconNameToVariants[name] || new Set();
modalSubtitle.textContent = `Available in ${availableVariants.size} variation${availableVariants.size !== 1 ? 's' : ''}`;
const className = `fi-${currentPrefix}-${name}`;
modalIcon.className = `modal-icon fi ${className}`;
const allPrefixes = Object.keys(PREFIX_CONFIG);
modalVariations.innerHTML = allPrefixes.map(prefix => {
const config = PREFIX_CONFIG[prefix];
const isAvailable = availableVariants.has(prefix);
const varClassName = `fi-${prefix}-${name}`;
const iconMarkup = isAvailable
? `<span class="fi ${varClassName} var-icon"></span>`
: `<span class="var-icon">N/A</span>`;
return `
<div class="modal-var ${prefix === currentPrefix ? 'active' : ''} ${!isAvailable ? 'unavailable' : ''}"
data-prefix="${prefix}" data-name="${name}">
${iconMarkup}
<div class="var-label">${config.label.split(' ').map(w => w[0]).join('')}</div>
</div>
`;
}).join('');
modalVariations.querySelectorAll('.modal-var:not(.unavailable)').forEach(el => {
el.addEventListener('click', () => {
const prefix = el.dataset.prefix;
const newClassName = `fi-${prefix}-${name}`;
modalIcon.className = `modal-icon fi ${newClassName}`;
currentModalPrefix = prefix;
modalVariations.querySelectorAll('.modal-var').forEach(v => v.classList.remove('active'));
el.classList.add('active');
});
});
modalOverlay.classList.add('show');
animateModal();
}
function toggleTheme() {
const isDark = document.documentElement.dataset.theme === 'dark';
document.documentElement.dataset.theme = isDark ? 'light' : 'dark';
themeToggle.innerHTML = isDark
? '<span class="fi fi-rs-sun"></span>'
: '<span class="fi fi-rs-moon"></span>';
localStorage.setItem('theme', isDark ? 'light' : 'dark');
}
function initTheme() {
const currentTheme = document.documentElement.dataset.theme || 'dark';
themeToggle.innerHTML = currentTheme === 'dark'
? '<span class="fi fi-rs-moon"></span>'
: '<span class="fi fi-rs-sun"></span>';
}
searchInput.addEventListener('input', e => {
state.search = e.target.value;
state.page = 1;
renderIcons();
});
document.getElementById('weightFilter').addEventListener('click', e => {
const btn = e.target.closest('.filter-btn');
if (!btn || btn.classList.contains('disabled')) return;
document.querySelectorAll('#weightFilter .filter-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
state.weight = btn.dataset.weight;
state.page = 1;
updateCornerAvailability();
const currentCornerBtn = document.querySelector('#cornerFilter .filter-btn.active');
if (currentCornerBtn?.classList.contains('disabled')) {
const validCorner = document.querySelector('#cornerFilter .filter-btn:not(.disabled)');
if (validCorner) {
document.querySelectorAll('#cornerFilter .filter-btn').forEach(b => b.classList.remove('active'));
validCorner.classList.add('active');
state.corner = validCorner.dataset.corner;
}
}
renderIcons();
});
document.getElementById('cornerFilter').addEventListener('click', e => {
const btn = e.target.closest('.filter-btn');
if (!btn || btn.classList.contains('disabled')) return;
document.querySelectorAll('#cornerFilter .filter-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
state.corner = btn.dataset.corner;
state.page = 1;
renderIcons();
});
document.getElementById('typeFilter').addEventListener('click', e => {
const btn = e.target.closest('.filter-btn');
if (!btn) return;
document.querySelectorAll('#typeFilter .filter-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
state.type = btn.dataset.type;
state.page = 1;
const isBrands = state.type === 'brands';
document.getElementById('weightFilter').parentElement.classList.toggle('disabled-section', isBrands);
document.getElementById('cornerFilter').parentElement.classList.toggle('disabled-section', isBrands);
document.querySelectorAll('#weightFilter .filter-btn, #cornerFilter .filter-btn').forEach(b => {
b.classList.toggle('disabled', isBrands);
});
renderIcons();
});
sizeSlider.addEventListener('input', e => {
state.iconSize = parseInt(e.target.value);
sizeValue.textContent = state.iconSize + 'px';
renderIcons();
});
iconGrid.addEventListener('click', e => {
const card = e.target.closest('.icon-card');
if (card) {
showModal(card.dataset.name, card.dataset.prefix);
}
});
document.getElementById('modalCopyBtn').addEventListener('click', () => {
const className = `fi-${currentModalPrefix}-${currentModalIcon}`;
copyToClipboard(`<span class="fi ${className}"></span>`);
});
document.getElementById('modalCopyCssBtn').addEventListener('click', () => {
const className = `fi-${currentModalPrefix}-${currentModalIcon}`;
copyToClipboard(`fi ${className}`);
});
modalCopyNameBtn.addEventListener('click', () => {
copyToClipboard(currentModalIcon);
});
document.getElementById('modalClose').addEventListener('click', () => {
modalOverlay.classList.remove('show');
});
modalOverlay.addEventListener('click', e => {
if (e.target === modalOverlay) modalOverlay.classList.remove('show');
});
prevBtn.addEventListener('click', () => {
if (state.page > 1) {
state.page--;
renderIcons();
window.scrollTo({ top: 0, behavior: 'smooth' });
}
});
nextBtn.addEventListener('click', () => {
const totalPages = Math.ceil(getFilteredIcons().length / state.perPage);
if (state.page < totalPages) {
state.page++;
renderIcons();
window.scrollTo({ top: 0, behavior: 'smooth' });
}
});
document.getElementById('goBtn').addEventListener('click', () => {
const page = parseInt(pageInput.value);
const totalPages = Math.ceil(getFilteredIcons().length / state.perPage);
if (page >= 1 && page <= totalPages) {
state.page = page;
renderIcons();
window.scrollTo({ top: 0, behavior: 'smooth' });
}
});
pageInput.addEventListener('keypress', e => {
if (e.key === 'Enter') document.getElementById('goBtn').click();
});
themeToggle.addEventListener('click', toggleTheme);
document.addEventListener('keydown', e => {
if (e.key === 'Escape') modalOverlay.classList.remove('show');
if (e.key === '/' && document.activeElement !== searchInput) {
e.preventDefault();
searchInput.focus();
}
if (e.key === 'ArrowLeft' && !modalOverlay.classList.contains('show')) {
prevBtn.click();
}
if (e.key === 'ArrowRight' && !modalOverlay.classList.contains('show')) {
nextBtn.click();
}
});
initTheme();
loadIcons();