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>
This commit is contained in:
2026-01-24 10:58:00 +08:00
commit ae66c2e34c
96 changed files with 249711 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
# OS
.DS_Store
Thumbs.db
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
package-lock.json
yarn.lock
# Source SVGs (too large for git)
svgs/
# Data files (generated)
data/all_icons.json
data/page*.json
data/icons-full.json
data/*.tmp
# FontForge temp files
*.new
fonts/webfonts/*.new
+81
View File
@@ -0,0 +1,81 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
This is **@invisi/flaticon-uicons**, a local web-compatible icon font package with 50,492+ icons across 15 style variations (stroke weights × corner styles). Icons are sourced from Flaticon's public API and converted to webfonts using FontForge.
The output is a drop-in CSS font system - users link `fonts/flaticon.css` and use class names like `fi-rs-bookmark` to display icons.
## Development Commands
```bash
# Fetch latest icon metadata from Flaticon API (writes to data/all_icons.json)
npm run update:icons
# or: node update-icon-list.js
# Convert JSON to browser-ready JS (writes to data/all_icons.js)
npm run build:icons
# or: node build-icons-js.js
# Generate webfonts from SVGs using FontForge
npm run build:fonts
# or: node build-fonts.js
```
**Font build options:**
- `--prefix rs,rr` - Build only specific prefixes (comma-separated)
- `--clean` - Apply FontForge cleanup (remove overlap, simplify)
- `--outputDir ./path` - Custom output directory
- `--css ./custom.css` - Custom CSS output path
## Icon Naming Convention
`fi-{stroke}{corner}-{icon-name}`
**Stroke codes:** `r` (Regular), `b` (Bold), `s` (Solid/filled), `t` (Thin), `d` (Duotone)
**Corner codes:** `s` (Straight), `r` (Rounded), `c` (Chubby)
**Special:** `fi-brands-` for brand logos
Examples: `fi-rs-bookmark`, `fi-br-home`, `fi-brands-instagram`
## Architecture
### Data Pipeline
1. **`update-icon-list.js`** - Fetches icon metadata from Flaticon API with pagination (117 pages). Rate-limited (100ms delay). Outputs `data/all_icons.json` (~53MB).
2. **`build-icons-js.js`** - Converts JSON to `window.FLATICON_ICONS` global for browser use in explorer (~5.7MB).
3. **`build-fonts.js`** - Node.js orchestrator that calls FontForge via `scripts/build-font.py` to convert SVGs → TTF → WOFF/WOFF2. Generates CSS with `@font-face` rules and content-based icon classes.
4. **`scripts/build-font.py`** - Standalone FontForge Python script for building a single font variant.
### Source Directories
- `svgs/{prefix}/` - Raw SVG files organized by prefix (rs, rr, bs, etc.)
- `data/` - API responses and processed icon data (gitignored)
- `fonts/webfonts/` - Generated font files
- `fonts/css/` - Individual CSS files per prefix
- `scripts/` - Build scripts (Python for FontForge)
### Key Files
- `fonts/flaticon.css` - Unified CSS importing all font-face definitions
- `explorer.html` + `explorer.js` + `explorer.css` - Interactive icon browser with search/filter
- `index.js` - Module exports for npm distribution
### Build Requirements
- Node.js for data processing
- Python with FontForge CLI for font generation
- SVG files must exist in `svgs/{prefix}/` before building fonts
## Unicode Codepoints
Each prefix starts at `0xE001` and increments sequentially. Codepoints are assigned per-prefix, not globally unique.
## Icon Explorer
Open `explorer.html` in a browser to search, filter by style, and copy HTML snippets. Uses `data/all_icons.js` as its data source.
+137
View File
@@ -0,0 +1,137 @@
# [@invisi/flaticon-uicons](https://www.flaticon.com/uicons/interface-icons)
A local web-compatible icon font package with **50,000+ icons** across multiple style variations. Icons are sourced from Flaticon's public API and converted to webfonts using FontForge.
![Flaticon UIcons](https://media.flaticon.com/dist/min/img/interface-icons/uicons.png)
## Features
- **50,492+ icons** across 15 style variations (stroke weights × corner styles)
- **Self-hosted** - no CDN dependencies
- **Interactive explorer** - browse, search, and filter icons locally
- **Web fonts** - WOFF2, WOFF, TTF formats
- **Easy integration** - drop-in CSS with class-based icons
## Installation
```shell
npm i @invisi/flaticon-uicons
```
## Quick Start
Include the CSS in your HTML:
```html
<link rel="stylesheet" href="fonts/flaticon.css">
```
Use icons with `<span>` or `<i>` elements:
```html
<i class="fi fi-rs-user"></i>
<span class="fi fi-brs-home"></span>
<i class="fi fi-brands-instagram"></i>
```
## Icon Styles
| Weight | Corner | Prefix | Example |
|:-----------|:---------|:-------|:-------------------------------|
| Regular | Straight | fi-rs | `<i class="fi fi-rs-user"></i>` |
| Regular | Rounded | fi-rr | `<i class="fi fi-rr-user"></i>` |
| Bold | Straight | fi-bs | `<i class="fi fi-bs-user"></i>` |
| Bold | Rounded | fi-br | `<i class="fi fi-br-user"></i>` |
| Solid | Straight | fi-ss | `<i class="fi fi-ss-user"></i>` |
| Solid | Rounded | fi-sr | `<i class="fi fi-sr-user"></i>` |
| Thin | Straight | fi-ts | `<i class="fi fi-ts-user"></i>` |
| Thin | Rounded | fi-tr | `<i class="fi fi-tr-user"></i>` |
| Brands | - | fi-brands | `<i class="fi fi-brands-facebook"></i>` |
## Icon Count
| Variant | Icons |
|-------------------|--------|
| Regular Straight | 5,043 |
| Regular Rounded | 5,039 |
| Bold Straight | 5,055 |
| Bold Rounded | 5,041 |
| Solid Straight | 5,044 |
| Solid Rounded | 5,051 |
| Thin Straight | 5,053 |
| Thin Rounded | 5,032 |
| Regular Chubby | 3,093 |
| Solid Chubby | 3,093 |
| Thin Chubby | 3,093 |
| Duotone Straight | 180 |
| Duotone Rounded | 160 |
| Duotone Chubby | 270 |
| Brands | 245 |
**Total: 50,492 icon variations**
## Styling
Icons inherit `font-size` and `color` from their parent:
```html
<!-- Size -->
<i class="fi fi-rs-heart" style="font-size: 24px;"></i>
<i class="fi fi-rs-heart" style="font-size: 48px;"></i>
<!-- Color -->
<i class="fi fi-rs-star" style="color: #f1c40f;"></i>
<i class="fi fi-rs-heart" style="color: #e74c3c;"></i>
```
## Package Structure
```
fonts/
flaticon.css # Unified CSS (imports all styles)
css/
uicons-regular-straight.css
uicons-regular-rounded.css
uicons-bold-straight.css
...
webfonts/ # Font files (woff2, woff, ttf)
data/
all_icons.js # Icon data for explorer
explorer.html # Interactive icon browser
```
## Icon Explorer
Open `explorer.html` in a browser to:
- Browse all 50,000+ icons
- Search by name or tags
- Filter by weight, corner style, and type
- Copy HTML snippets and CSS classes
- Adjust icon preview size
## Development Scripts
```bash
# Fetch latest icon metadata from Flaticon API
npm run update:icons
# or: node update-icon-list.js
# Convert JSON to browser-ready JS
npm run build:icons
# or: node build-icons-js.js
# Generate webfonts from SVGs using FontForge
npm run build:fonts
# or: node build-fonts.js
```
## License
Icons sourced from [Flaticon UIcons](https://www.flaticon.com/uicons). Please refer to [Flaticon's license](https://www.flaticon.com/uicons) for usage terms.
## Attribution
```
Uicons by <a href="https://www.flaticon.com/uicons">Flaticon</a>
```
+171
View File
@@ -0,0 +1,171 @@
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');
const PREFIX_CONFIG = {
rs: { label: 'Regular Straight', file: 'regular-straight' },
rr: { label: 'Regular Rounded', file: 'regular-rounded' },
bs: { label: 'Bold Straight', file: 'bold-straight' },
br: { label: 'Bold Rounded', file: 'bold-rounded' },
ss: { label: 'Solid Straight', file: 'solid-straight' },
sr: { label: 'Solid Rounded', file: 'solid-rounded' },
ts: { label: 'Thin Straight', file: 'thin-straight' },
tr: { label: 'Thin Rounded', file: 'thin-rounded' },
rc: { label: 'Regular Chubby', file: 'regular-chubby' },
sc: { label: 'Solid Chubby', file: 'solid-chubby' },
tc: { label: 'Thin Chubby', file: 'thin-chubby' },
ds: { label: 'Duotone Straight', file: 'duotone-straight' },
dr: { label: 'Duotone Rounded', file: 'duotone-rounded' },
dc: { label: 'Duotone Chubby', file: 'duotone-chubby' },
brands: { label: 'Brands', file: 'brands' }
};
function runFontForge({ prefix, outputDir, clean }) {
const config = PREFIX_CONFIG[prefix] || { label: prefix, file: prefix };
const args = [
'scripts/build-font.py',
prefix,
outputDir,
clean ? 'true' : 'false',
config.label,
config.file
];
const result = spawnSync('python3', args, {
stdio: 'inherit'
});
if (result.status !== 0) {
process.exit(result.status || 1);
}
}
function buildCss({ order, mapping, outputPath }) {
const lines = [];
lines.push('/*!');
lines.push(' * Flaticon Icon Fonts - Local Build');
lines.push(' * Generated from Flaticon icon API');
lines.push(' */');
lines.push('');
lines.push('[class^="fi-"], [class*=" fi-"] {');
lines.push(' display: inline-block;');
lines.push(' font-style: normal;');
lines.push(' font-weight: normal !important;');
lines.push(' font-variant: normal;');
lines.push(' text-transform: none;');
lines.push(' line-height: 1;');
lines.push(' -webkit-font-smoothing: antialiased;');
lines.push(' -moz-osx-font-smoothing: grayscale;');
lines.push('}');
lines.push('');
order.forEach(prefix => {
const config = PREFIX_CONFIG[prefix] || { label: prefix, file: prefix };
const label = config.label;
const fileSuffix = config.file;
const names = mapping[prefix] || [];
lines.push(`/* ${label} (${prefix}) */`);
lines.push('@font-face {');
lines.push(` font-family: "flaticon-${fileSuffix}";`);
lines.push(
` src: url("./webfonts/flaticon-${fileSuffix}.woff2") format("woff2"),`
);
lines.push(
` url("./webfonts/flaticon-${fileSuffix}.woff") format("woff"),`
);
lines.push(
` url("./webfonts/flaticon-${fileSuffix}.ttf") format("truetype");`
);
lines.push(' font-display: swap;');
lines.push('}');
lines.push('');
lines.push(
`i[class^="fi-${prefix}-"]:before, i[class*=" fi-${prefix}-"]:before,`
);
lines.push(
`span[class^="fi-${prefix}-"]:before, span[class*=" fi-${prefix}-"]:before {`
);
lines.push(` font-family: flaticon-${fileSuffix} !important;`);
lines.push(' font-style: normal;');
lines.push(' font-weight: normal !important;');
lines.push('}');
lines.push('');
const baseCodepoint = 0xe001;
names.forEach((name, index) => {
const codepoint = baseCodepoint + index;
lines.push(`.fi-${prefix}-${name}:before {`);
lines.push(` content: "\\${codepoint.toString(16).padStart(4, '0')}";`);
lines.push('}');
});
lines.push('');
});
lines.push('.fi { display: inline-block; font-style: normal; }');
lines.push('');
fs.writeFileSync(outputPath, lines.join('\n'));
}
function loadIcons() {
const raw = fs.readFileSync(path.join(__dirname, 'data', 'all_icons.json'), 'utf8');
const icons = JSON.parse(raw);
const iconsByPrefix = {};
icons.forEach(icon => {
const prefix = icon.prefix;
const name = icon.name;
if (!prefix || !name) return;
if (!iconsByPrefix[prefix]) iconsByPrefix[prefix] = new Set();
iconsByPrefix[prefix].add(name);
});
const result = {};
Object.keys(iconsByPrefix).forEach(prefix => {
result[prefix] = Array.from(iconsByPrefix[prefix]).sort();
});
return result;
}
function parseArgs(argv) {
const args = {};
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
if (!arg.startsWith('--')) continue;
const key = arg.slice(2);
const value = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[i + 1] : true;
if (value !== true) i += 1;
args[key] = value;
}
return args;
}
function main() {
const args = parseArgs(process.argv.slice(2));
const prefixes = args.prefix ? String(args.prefix).split(',') : Object.keys(PREFIX_CONFIG);
const clean = Boolean(args.clean);
const outputDir = args.outputDir || 'fonts/webfonts';
const cssPath = args.css || 'fonts/flaticon.css';
const iconsByPrefix = loadIcons();
const buildOrder = Object.keys(PREFIX_CONFIG).filter(prefix => prefixes.includes(prefix));
buildOrder.forEach(prefix => {
if (!iconsByPrefix[prefix] || iconsByPrefix[prefix].length === 0) {
console.warn(`Skipping ${prefix}: no icons found.`);
return;
}
runFontForge({ prefix, outputDir, clean });
});
buildCss({
order: buildOrder,
mapping: iconsByPrefix,
outputPath: cssPath
});
console.log('Done.');
}
main();
+36
View File
@@ -0,0 +1,36 @@
const fs = require('fs');
const path = require('path');
function buildIconsJs({
inputPath = path.join(__dirname, 'data', 'all_icons.json'),
outputPath = path.join(__dirname, 'data', 'all_icons.js')
} = {}) {
if (!fs.existsSync(inputPath)) {
throw new Error(`Missing ${inputPath}`);
}
const raw = fs.readFileSync(inputPath, 'utf8');
const icons = JSON.parse(raw).map(({ name, prefix, tags, is_brand }) => ({
name,
prefix,
tags,
is_brand
}));
const payload = `window.FLATICON_ICONS = ${JSON.stringify(icons)};\n`;
fs.writeFileSync(outputPath, payload, 'utf8');
const sizeMb = (payload.length / (1024 * 1024)).toFixed(1);
console.log(`Wrote ${outputPath} (${sizeMb} MB)`);
}
if (require.main === module) {
try {
buildIconsJs();
} catch (error) {
console.error(error.message || error);
process.exit(1);
}
}
module.exports = buildIconsJs;
File diff suppressed because one or more lines are too long
+1434
View File
File diff suppressed because it is too large Load Diff
+165
View File
@@ -0,0 +1,165 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Flaticon Explorer</title>
<link rel="stylesheet" href="fonts/flaticon.css">
<link rel="stylesheet" href="explorer.css">
<script>try{document.documentElement.dataset.theme=localStorage.getItem('theme')??(matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light')}catch{}</script>
</head>
<body>
<header class="header">
<div class="header-inner">
<div class="header-top">
<div class="logo-section">
<div class="logo-icon">
<span class="fi fi-rs-sparkles"></span>
</div>
<div>
<h1>Flaticon Explorer</h1>
<div class="header-subtitle">Just a little place to find nice icons</div>
</div>
</div>
<div class="header-actions">
<button class="theme-toggle" id="themeToggle" title="Toggle theme">
<span class="fi fi-rs-sun"></span>
</button>
</div>
</div>
<div class="filters">
<div class="search-container">
<div class="search-box">
<span class="fi fi-rs-search search-icon"></span>
<input type="text" class="search-input" id="searchInput" placeholder="Search icons by name or tags...">
<span class="search-shortcut">/</span>
</div>
</div>
<div class="filter-section">
<span class="filter-label"><span class="fi fi-rs-settings-sliders"></span> Weight</span>
<div class="filter-group" id="weightFilter">
<button class="filter-btn active" data-weight="regular">Regular</button>
<button class="filter-btn" data-weight="bold">Bold</button>
<button class="filter-btn" data-weight="solid">Solid</button>
<button class="filter-btn" data-weight="thin">Thin</button>
<button class="filter-btn" data-weight="duotone">Duotone</button>
</div>
</div>
<div class="filter-section">
<span class="filter-label"><span class="fi fi-rs-square"></span> Corner</span>
<div class="filter-group" id="cornerFilter">
<button class="filter-btn active" data-corner="straight">Straight</button>
<button class="filter-btn" data-corner="rounded">Rounded</button>
<button class="filter-btn" data-corner="chubby">Chubby</button>
</div>
</div>
<div class="filter-section">
<span class="filter-label"><span class="fi fi-rs-grid"></span> Type</span>
<div class="filter-group" id="typeFilter">
<button class="filter-btn active" data-type="interface">Interface</button>
<button class="filter-btn" data-type="brands">Brands</button>
</div>
</div>
<div class="size-control">
<span class="filter-label"><span class="fi fi-rs-expand"></span></span>
<input type="range" class="size-slider" id="sizeSlider" min="20" max="48" value="32">
<span class="size-value" id="sizeValue">32px</span>
</div>
</div>
</div>
</header>
<main class="main">
<div class="loading" id="loadingState">
<div class="loading-spinner"></div>
<p>Loading icon library...</p>
</div>
<div class="icon-grid" id="iconGrid" style="display:none"></div>
<div class="empty" id="emptyState" style="display:none">
<div class="empty-icon"><span class="fi fi-rs-search"></span></div>
<h3>No icons found</h3>
<p>Try adjusting your search or filters</p>
</div>
<div class="pagination" id="pagination" style="display:none">
<button class="page-btn" id="prevBtn">
<span class="fi fi-rs-angle-left"></span> Prev
</button>
<span class="page-info" id="pageInfo">Page 1 of 1</span>
<button class="page-btn" id="nextBtn">
Next <span class="fi fi-rs-angle-right"></span>
</button>
<div class="page-jump">
<input type="number" class="page-input" id="pageInput" min="1" placeholder="Go">
<button class="page-btn" id="goBtn">Go</button>
</div>
</div>
</main>
<div class="toast" id="toast">
<span class="fi fi-rs-clipboard-check"></span>
<span id="toastMessage">Copied!</span>
</div>
<div class="modal-overlay" id="modalOverlay">
<div class="modal">
<div class="modal-header">
<div class="modal-icon-wrapper">
<span class="modal-icon fi" id="modalIcon"></span>
</div>
<div class="modal-info">
<div class="modal-title" id="modalTitle"></div>
<div class="modal-subtitle" id="modalSubtitle"></div>
</div>
<div class="modal-actions-inline">
<button class="copy-btn-inline" id="modalCopyBtn" title="Copy HTML">
<span class="fi fi-rs-file-code"></span>
<span class="copy-label">HTML</span>
</button>
<button class="copy-btn-inline" id="modalCopyCssBtn" title="Copy CSS class">
<span class="fi fi-rs-palette"></span>
<span class="copy-label">CSS</span>
</button>
<button class="copy-btn-inline" id="modalCopyNameBtn" title="Copy icon name">
<span class="fi fi-rs-copy"></span>
<span class="copy-label">Name</span>
</button>
</div>
</div>
<div class="modal-content">
<div class="modal-section">
<div class="modal-label">All Variations</div>
<div class="modal-variations" id="modalVariations"></div>
</div>
<div class="modal-footer">
<button class="modal-btn" id="modalClose">
<span class="fi fi-rs-cross"></span> Close <span class="btn-key">Esc</span>
</button>
</div>
</div>
</div>
</div>
<div class="shortcuts-help">
<kbd>/</kbd> Search <span></span>
<kbd>Esc</kbd> Close <span></span>
<kbd></kbd><kbd></kbd> Navigate
</div>
<div class="stats">
<span id="loadingIndicator" class="loading-indicator" style="display:none"></span>
<span><strong id="visibleCount">0</strong> of <strong id="totalCount">0</strong></span>
</div>
<script src="data/all_icons.js"></script>
<script src="explorer.js"></script>
</body>
</html>
+499
View File
@@ -0,0 +1,499 @@
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();
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+691
View File
@@ -0,0 +1,691 @@
/*!
* _____ _ __ _____ ______ _ _ _
* |_ _| | | / _| |_ _| | ____| | | | (_)
* | | _ __ | |_ ___ _ __| |_ __ _ ___ ___ | | ___ ___ _ __ ___ ______ | |__ | | __ _| |_ _ ___ ___ _ __
* | | | '_ \| __/ _ \ '__| _/ _` |/ __/ _ \ | | / __/ _ \| '_ \/ __| |______| | __| | |/ _` | __| |/ __/ _ \| '_ \
* _| |_| | | | || __/ | | || (_| | (_| __/ _| || (_| (_) | | | \__ \ | | | | (_| | |_| | (_| (_) | | | |
* |_____|_| |_|\__\___|_| |_| \__,_|\___\___| |_____\___\___/|_| |_|___/ |_| |_|\__,_|\__|_|\___\___/|_| |_|
*
* UIcons 2.6.0 - https://www.flaticon.com/uicons/interface-icons
*/
@font-face {
font-family: "uicons-brands";
src: url("https://cdn-uicons.flaticon.com/2.6.0/uicons-brands/webfonts/uicons-brands.woff2") format("woff2"),
url("https://cdn-uicons.flaticon.com/2.6.0/uicons-brands/webfonts/uicons-brands.woff") format("woff"),
url("https://cdn-uicons.flaticon.com/2.6.0/uicons-brands/webfonts/uicons-brands.eot#iefix") format("embedded-opentype");
font-display: swap;
}
i[class^="fi-brands-"]:before, i[class*=" fi-brands-"]:before, span[class^="fi-brands-"]:before, span[class*="fi-brands-"]:before {
font-family: uicons-brands !important;
font-style: normal;
font-weight: normal !important;
font-variant: normal;
text-transform: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.fi-brands-3m:before {
content: "\e00c";
}
.fi-brands-500px:before {
content: "\e012";
}
.fi-brands-abbot-laboratories:before {
content: "\e01d";
}
.fi-brands-accusoft:before {
content: "\e020";
}
.fi-brands-acrobat:before {
content: "\e022";
}
.fi-brands-adobe:before {
content: "\e031";
}
.fi-brands-aecom:before {
content: "\e035";
}
.fi-brands-aero:before {
content: "\e036";
}
.fi-brands-after-effects:before {
content: "\e037";
}
.fi-brands-airbnb:before {
content: "\e049";
}
.fi-brands-algolia:before {
content: "\e055";
}
.fi-brands-amd:before {
content: "\e060";
}
.fi-brands-american-express:before {
content: "\e061";
}
.fi-brands-android:before {
content: "\e06b";
}
.fi-brands-animate:before {
content: "\e087";
}
.fi-brands-app-store-ios:before {
content: "\e091";
}
.fi-brands-apple:before {
content: "\e092";
}
.fi-brands-apple-pay:before {
content: "\e096";
}
.fi-brands-artstation:before {
content: "\e106";
}
.fi-brands-astrazeneca:before {
content: "\e10e";
}
.fi-brands-asus:before {
content: "\e10f";
}
.fi-brands-atandt:before {
content: "\e111";
}
.fi-brands-atlassian:before {
content: "\e112";
}
.fi-brands-atom:before {
content: "\e113";
}
.fi-brands-audition:before {
content: "\e11e";
}
.fi-brands-behance:before {
content: "\e186";
}
.fi-brands-bitcoin:before {
content: "\e1ad";
}
.fi-brands-blackberry:before {
content: "\e1b0";
}
.fi-brands-blogger:before {
content: "\e1c2";
}
.fi-brands-bluetooth:before {
content: "\e1ca";
}
.fi-brands-bootstrap:before {
content: "\e1f8";
}
.fi-brands-bridgestone:before {
content: "\e25b";
}
.fi-brands-burger-king:before {
content: "\e289";
}
.fi-brands-c:before {
content: "\e293";
}
.fi-brands-capture:before {
content: "\e2de";
}
.fi-brands-cc-amazon-pay:before {
content: "\e323";
}
.fi-brands-cc-apple-pay:before {
content: "\e324";
}
.fi-brands-cc-diners-club:before {
content: "\e325";
}
.fi-brands-cc-visa:before {
content: "\e326";
}
.fi-brands-centos:before {
content: "\e32a";
}
.fi-brands-character:before {
content: "\e333";
}
.fi-brands-chromecast:before {
content: "\e385";
}
.fi-brands-cloudflare:before {
content: "\e42f";
}
.fi-brands-confluence:before {
content: "\e491";
}
.fi-brands-creative-commons:before {
content: "\e4b2";
}
.fi-brands-creative-commons-by:before {
content: "\e4b3";
}
.fi-brands-creative-commons-nc:before {
content: "\e4b4";
}
.fi-brands-creative-commons-nc-eu:before {
content: "\e4b5";
}
.fi-brands-creative-commons-nc-jp:before {
content: "\e4b6";
}
.fi-brands-creative-commons-nd:before {
content: "\e4b7";
}
.fi-brands-creative-commons-pd:before {
content: "\e4b8";
}
.fi-brands-creative-commons-pd-alt:before {
content: "\e4b9";
}
.fi-brands-creative-commons-remix:before {
content: "\e4ba";
}
.fi-brands-creative-commons-sa:before {
content: "\e4bb";
}
.fi-brands-creative-commons-sampling:before {
content: "\e4bc";
}
.fi-brands-creative-commons-sampling-plus:before {
content: "\e4bd";
}
.fi-brands-creative-commons-share:before {
content: "\e4be";
}
.fi-brands-creative-commons-zero:before {
content: "\e4bf";
}
.fi-brands-css3:before {
content: "\e4da";
}
.fi-brands-css3-alt:before {
content: "\e4db";
}
.fi-brands-dailymotion:before {
content: "\e4fb";
}
.fi-brands-deezer:before {
content: "\e50f";
}
.fi-brands-delphi:before {
content: "\e516";
}
.fi-brands-dev:before {
content: "\e523";
}
.fi-brands-devianart:before {
content: "\e524";
}
.fi-brands-digg:before {
content: "\e552";
}
.fi-brands-dimension:before {
content: "\e55b";
}
.fi-brands-discord:before {
content: "\e564";
}
.fi-brands-docker:before {
content: "\e57c";
}
.fi-brands-dribbble:before {
content: "\e5ae";
}
.fi-brands-dropbox:before {
content: "\e5b7";
}
.fi-brands-drupal:before {
content: "\e5c0";
}
.fi-brands-ebay:before {
content: "\e5db";
}
.fi-brands-elementor:before {
content: "\e5e7";
}
.fi-brands-ethereum:before {
content: "\e612";
}
.fi-brands-etsy:before {
content: "\e614";
}
.fi-brands-evernote:before {
content: "\e619";
}
.fi-brands-facebook:before {
content: "\e675";
}
.fi-brands-facebook-messenger:before {
content: "\e676";
}
.fi-brands-fedex:before {
content: "\e688";
}
.fi-brands-figma:before {
content: "\e697";
}
.fi-brands-firefox:before {
content: "\e6e0";
}
.fi-brands-firefox-browser:before {
content: "\e6e1";
}
.fi-brands-flaticon:before {
content: "\e6fb";
}
.fi-brands-flaticon-1:before {
content: "\e6fc";
}
.fi-brands-flickr:before {
content: "\e6fe";
}
.fi-brands-flipboard:before {
content: "\e700";
}
.fi-brands-fonts:before {
content: "\e735";
}
.fi-brands-foursquare:before {
content: "\e746";
}
.fi-brands-freepik:before {
content: "\e74d";
}
.fi-brands-freepik-1:before {
content: "\e74e";
}
.fi-brands-fresco:before {
content: "\e751";
}
.fi-brands-github:before {
content: "\e787";
}
.fi-brands-gitlab:before {
content: "\e788";
}
.fi-brands-goodreads:before {
content: "\e79d";
}
.fi-brands-google:before {
content: "\e79e";
}
.fi-brands-haskell:before {
content: "\e821";
}
.fi-brands-hbo:before {
content: "\e82c";
}
.fi-brands-hotjar:before {
content: "\e87d";
}
.fi-brands-html5:before {
content: "\e8a4";
}
.fi-brands-huawei:before {
content: "\e8a5";
}
.fi-brands-hubspot:before {
content: "\e8a6";
}
.fi-brands-ibm:before {
content: "\e8ae";
}
.fi-brands-iconfinder:before {
content: "\e8b3";
}
.fi-brands-illustrator:before {
content: "\e8b8";
}
.fi-brands-illustrator-draw:before {
content: "\e8b9";
}
.fi-brands-imdb:before {
content: "\e8bd";
}
.fi-brands-incopy:before {
content: "\e8c6";
}
.fi-brands-indesign:before {
content: "\e8c8";
}
.fi-brands-instagram:before {
content: "\e8de";
}
.fi-brands-intel:before {
content: "\e8e4";
}
.fi-brands-invision:before {
content: "\e8f0";
}
.fi-brands-itunes:before {
content: "\e901";
}
.fi-brands-janseen:before {
content: "\e905";
}
.fi-brands-java:before {
content: "\e909";
}
.fi-brands-jcb:before {
content: "\e90b";
}
.fi-brands-jira:before {
content: "\e90c";
}
.fi-brands-johnson-and-johnson:before {
content: "\e90d";
}
.fi-brands-joomla:before {
content: "\e910";
}
.fi-brands-js:before {
content: "\e917";
}
.fi-brands-kickstarter:before {
content: "\e92d";
}
.fi-brands-line:before {
content: "\e9a1";
}
.fi-brands-linkedin:before {
content: "\e9a9";
}
.fi-brands-lisp:before {
content: "\e9b0";
}
.fi-brands-mailchimp:before {
content: "\e9e8";
}
.fi-brands-marriott-international:before {
content: "\ea07";
}
.fi-brands-mcdonalds:before {
content: "\ea18";
}
.fi-brands-media-encoder:before {
content: "\ea1c";
}
.fi-brands-medium:before {
content: "\ea20";
}
.fi-brands-meta:before {
content: "\ea4b";
}
.fi-brands-microsoft:before {
content: "\ea5a";
}
.fi-brands-microsoft-edge:before {
content: "\ea5b";
}
.fi-brands-microsoft-explorer:before {
content: "\ea5c";
}
.fi-brands-mysql:before {
content: "\eaca";
}
.fi-brands-napster:before {
content: "\eacd";
}
.fi-brands-nestle:before {
content: "\ead1";
}
.fi-brands-netflix:before {
content: "\ead2";
}
.fi-brands-node-js:before {
content: "\eaf0";
}
.fi-brands-nvidia:before {
content: "\eb08";
}
.fi-brands-oracle:before {
content: "\eb2d";
}
.fi-brands-patreon:before {
content: "\eb68";
}
.fi-brands-paypal:before {
content: "\eb71";
}
.fi-brands-pfizer:before {
content: "\ebda";
}
.fi-brands-photoshop:before {
content: "\ebec";
}
.fi-brands-photoshop-camera:before {
content: "\ebed";
}
.fi-brands-photoshop-express:before {
content: "\ebee";
}
.fi-brands-photoshop-lightroom:before {
content: "\ebef";
}
.fi-brands-photoshop-lightroom-classic:before {
content: "\ebf0";
}
.fi-brands-php:before {
content: "\ebf1";
}
.fi-brands-pinterest:before {
content: "\ec0a";
}
.fi-brands-postgre:before {
content: "\ec5a";
}
.fi-brands-premiere:before {
content: "\ec62";
}
.fi-brands-premiere-rush:before {
content: "\ec63";
}
.fi-brands-product-hunt:before {
content: "\ec78";
}
.fi-brands-python:before {
content: "\ec93";
}
.fi-brands-raspberry-pi:before {
content: "\ecbc";
}
.fi-brands-reddit:before {
content: "\ecd4";
}
.fi-brands-samsung:before {
content: "\ed51";
}
.fi-brands-sap:before {
content: "\ed54";
}
.fi-brands-sass:before {
content: "\ed55";
}
.fi-brands-shopify:before {
content: "\edcd";
}
.fi-brands-siemens:before {
content: "\ede5";
}
.fi-brands-sketch:before {
content: "\ee0d";
}
.fi-brands-skype:before {
content: "\ee24";
}
.fi-brands-slack:before {
content: "\ee25";
}
.fi-brands-slidesgo:before {
content: "\ee2f";
}
.fi-brands-snapchat:before {
content: "\ee3f";
}
.fi-brands-sony:before {
content: "\ee56";
}
.fi-brands-soundcloud:before {
content: "\ee6b";
}
.fi-brands-spark:before {
content: "\ee78";
}
.fi-brands-spotify:before {
content: "\ee8a";
}
.fi-brands-starbucks:before {
content: "\eee4";
}
.fi-brands-stock:before {
content: "\eef3";
}
.fi-brands-storyset:before {
content: "\ef02";
}
.fi-brands-stripe:before {
content: "\ef08";
}
.fi-brands-substance-3d-designer:before {
content: "\ef14";
}
.fi-brands-substance-3d-painter:before {
content: "\ef15";
}
.fi-brands-substance-3d-sampler:before {
content: "\ef16";
}
.fi-brands-substance-3d-stager:before {
content: "\ef17";
}
.fi-brands-swift:before {
content: "\ef37";
}
.fi-brands-t-mobile:before {
content: "\ef48";
}
.fi-brands-telegram:before {
content: "\ef7b";
}
.fi-brands-tencent:before {
content: "\ef85";
}
.fi-brands-the-home-depot:before {
content: "\ef9e";
}
.fi-brands-tik-tok:before {
content: "\efbf";
}
.fi-brands-trello:before {
content: "\f043";
}
.fi-brands-tripadvisor:before {
content: "\f04a";
}
.fi-brands-tumblr:before {
content: "\f072";
}
.fi-brands-twitch:before {
content: "\f07b";
}
.fi-brands-twitter:before {
content: "\f07c";
}
.fi-brands-twitter-alt:before {
content: "\f07d";
}
.fi-brands-twitter-alt-circle:before {
content: "\f07e";
}
.fi-brands-twitter-alt-square:before {
content: "\f07f";
}
.fi-brands-typescript:before {
content: "\f082";
}
.fi-brands-uber:before {
content: "\f085";
}
.fi-brands-ubuntu:before {
content: "\f086";
}
.fi-brands-unilever:before {
content: "\f092";
}
.fi-brands-unity:before {
content: "\f093";
}
.fi-brands-unsplash:before {
content: "\f096";
}
.fi-brands-ups:before {
content: "\f09e";
}
.fi-brands-usaa:before {
content: "\f0a2";
}
.fi-brands-verizon:before {
content: "\f0fb";
}
.fi-brands-videvo:before {
content: "\f106";
}
.fi-brands-vimeo:before {
content: "\f108";
}
.fi-brands-visa:before {
content: "\f10d";
}
.fi-brands-visual-basic:before {
content: "\f112";
}
.fi-brands-vk:before {
content: "\f113";
}
.fi-brands-walmart:before {
content: "\f12f";
}
.fi-brands-wepik:before {
content: "\f14f";
}
.fi-brands-whatsapp:before {
content: "\f151";
}
.fi-brands-wikipedia:before {
content: "\f163";
}
.fi-brands-windows:before {
content: "\f16d";
}
.fi-brands-wix:before {
content: "\f176";
}
.fi-brands-wordpress:before {
content: "\f17a";
}
.fi-brands-xd:before {
content: "\f18c";
}
.fi-brands-xing:before {
content: "\f18d";
}
.fi-brands-yahoo:before {
content: "\f18f";
}
.fi-brands-yandex:before {
content: "\f190";
}
.fi-brands-yelp:before {
content: "\f191";
}
.fi-brands-youtube:before {
content: "\f199";
}
.fi-brands-zoom:before {
content: "\f19d";
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+151748
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+54
View File
@@ -0,0 +1,54 @@
const fs = require('fs');
// Read API data
const allIcons = JSON.parse(fs.readFileSync('data/all_icons.json', 'utf8'));
// Read CDN availability
const cdnIcons = {};
const prefixes = ['rs', 'rr', 'bs', 'br', 'ss', 'sr', 'ts', 'tr', 'brands'];
prefixes.forEach(prefix => {
const content = fs.readFileSync(`/tmp/cdn-${prefix}.txt`, 'utf8');
cdnIcons[prefix] = new Set(content.trim().split('\n').filter(Boolean));
});
// Group icons by name
const iconsByName = {};
allIcons.forEach(icon => {
if (!iconsByName[icon.name]) {
iconsByName[icon.name] = {
name: icon.name,
isBrand: icon.is_brand,
tags: icon.tags,
variants: {}
};
}
iconsByName[icon.name].variants[icon.prefix] = {
id: icon.id,
svg: icon.svg,
inCdn: cdnIcons[icon.prefix]?.has(icon.name) || false
};
});
// Create output
const output = {
icons: Object.values(iconsByName),
stats: {
totalIcons: Object.keys(iconsByName).length,
totalVariants: allIcons.length,
cdnAvailable: {},
apiTotal: {}
},
cdnIcons: {}
};
prefixes.forEach(prefix => {
output.stats.cdnAvailable[prefix] = cdnIcons[prefix].size;
output.stats.apiTotal[prefix] = allIcons.filter(i => i.prefix === prefix).length;
output.cdnIcons[prefix] = Array.from(cdnIcons[prefix]).sort();
});
fs.writeFileSync('data/icons-full.json', JSON.stringify(output, null, 2));
console.log('Generated data/icons-full.json');
console.log('Total unique icons:', output.stats.totalIcons);
console.log('Total variants:', output.stats.totalVariants);
+8
View File
@@ -0,0 +1,8 @@
const path = require('path');
module.exports = {
css: path.join(__dirname, 'fonts', 'flaticon.css'),
webfonts: path.join(__dirname, 'fonts', 'webfonts'),
explorer: path.join(__dirname, 'explorer.html'),
iconData: path.join(__dirname, 'data', 'all_icons.js')
};
+26
View File
@@ -0,0 +1,26 @@
{
"name": "@invisi/flaticon-uicons",
"version": "0.1.0",
"description": "Flaticon UIcons font icon package with local explorer and webfonts.",
"license": "SEE LICENSE IN README",
"main": "index.js",
"style": "fonts/flaticon.css",
"files": [
"fonts",
"data/all_icons.js",
"explorer.html",
"README.md",
"index.js"
],
"scripts": {
"build:icons": "node build-icons-js.js",
"update:icons": "node update-icon-list.js",
"build:fonts": "node build-fonts.js"
},
"keywords": [
"flaticon",
"icons",
"fonts",
"uicons"
]
}
+89
View File
@@ -0,0 +1,89 @@
#!/usr/bin/env python3
"""
FontForge script to build icon fonts from SVG files.
Usage:
python scripts/build-font.py <prefix> <output_dir> <clean> <label> <file_suffix>
Args:
prefix: Icon prefix (e.g., 'rs', 'rr', 'bs', etc.)
output_dir: Directory to output font files
clean: Whether to apply FontForge cleanup ('true' or 'false')
label: Human-readable label for the font
file_suffix: Suffix for font filenames
"""
import json
import os
import sys
import fontforge
def main():
if len(sys.argv) < 6:
print("Usage: build-font.py <prefix> <output_dir> <clean> <label> <file_suffix>", file=sys.stderr)
sys.exit(1)
prefix = sys.argv[1]
output_dir = sys.argv[2]
clean = sys.argv[3].lower() == 'true'
label = sys.argv[4]
file_suffix = sys.argv[5]
# Load icon data
with open('data/all_icons.json', 'r', encoding='utf-8') as handle:
icons = json.load(handle)
# Get sorted icon names for this prefix
names = sorted({icon.get('name') for icon in icons if icon.get('prefix') == prefix})
if not names:
print(f"No icons for prefix {prefix}", file=sys.stderr)
sys.exit(1)
# Create font
font = fontforge.font()
font.encoding = 'UnicodeFull'
font.em = 1000
font.ascent = 850
font.descent = 150
font.fontname = 'flaticon-' + file_suffix
font.familyname = 'Flaticon ' + label
font.fullname = 'Flaticon ' + label
# Import glyphs from SVGs
base_codepoint = 0xE001
for index, name in enumerate(names):
codepoint = base_codepoint + index
glyph = font.createChar(codepoint, name)
svg_path = os.path.join('svgs', prefix, name + '.svg')
if not os.path.exists(svg_path):
print(f"Warning: {svg_path} not found, skipping", file=sys.stderr)
continue
glyph.importOutlines(svg_path)
glyph.correctDirection()
if clean:
glyph.removeOverlap()
glyph.simplify()
glyph.width = 1000
# Create output directory
os.makedirs(output_dir, exist_ok=True)
# Generate font files
ttf_path = os.path.join(output_dir, 'flaticon-' + file_suffix + '.ttf')
woff_path = os.path.join(output_dir, 'flaticon-' + file_suffix + '.woff')
woff2_path = os.path.join(output_dir, 'flaticon-' + file_suffix + '.woff2')
font.generate(ttf_path)
font.generate(woff_path)
try:
font.generate(woff2_path)
except Exception as e:
print(f"Warning: Could not generate woff2: {e}", file=sys.stderr)
font.close()
print(f'Built {label} -> {file_suffix}')
if __name__ == '__main__':
main()
+120
View File
@@ -0,0 +1,120 @@
const fs = require('fs');
const path = require('path');
const https = require('https');
const buildIconsJs = require('./build-icons-js');
const API_URL = 'https://www.flaticon.com/ajax/icon-fonts-most-downloaded/';
const HEADERS = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.9',
'Sec-Ch-Ua': '"Not A(Brand";v="99", "Google Chrome";v="121"',
'Sec-Ch-Ua-Mobile': '?0',
'Sec-Ch-Ua-Platform': '"macOS"',
'Sec-Fetch-Dest': 'document',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-Site': 'none'
};
function parseArgs(argv) {
const args = {};
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
if (!arg.startsWith('--')) continue;
const key = arg.slice(2);
const value = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[i + 1] : true;
if (value !== true) i += 1;
args[key] = value;
}
return args;
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function fetchJson(page) {
const url = `${API_URL}${page}`;
return new Promise((resolve, reject) => {
const request = https.request(url, { headers: HEADERS }, response => {
let data = '';
response.on('data', chunk => {
data += chunk;
});
response.on('end', () => {
try {
resolve(JSON.parse(data));
} catch (error) {
reject(error);
}
});
});
request.on('error', reject);
request.end();
});
}
async function run() {
const args = parseArgs(process.argv.slice(2));
const start = Number(args.start || 1);
let end = Number(args.end || 0);
const delay = Number(args.delay || 100);
if (Number.isNaN(start) || start < 1) {
throw new Error('Invalid --start value');
}
console.log(`Fetching icons starting at page ${start}...`);
const first = await fetchJson(start);
if (!first || !Array.isArray(first.items)) {
throw new Error('Unexpected response from Flaticon API');
}
if (!end || Number.isNaN(end)) {
end = Number(first.pages || start);
}
const itemsById = new Map();
const addItems = items => {
(items || []).forEach(item => {
if (item && item.id) {
itemsById.set(item.id, item);
}
});
};
addItems(first.items);
console.log(`Page ${start}/${end}: ${first.items.length} items`);
for (let page = start + 1; page <= end; page += 1) {
const payload = await fetchJson(page);
addItems(payload.items);
if (page % 25 === 0 || page === end) {
console.log(`Page ${page}/${end}: ${payload.items.length} items`);
}
if (delay > 0) {
await sleep(delay);
}
}
const outputDir = path.join(__dirname, 'data');
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
const allIcons = Array.from(itemsById.values());
const outputPath = path.join(outputDir, 'all_icons.json');
fs.writeFileSync(outputPath, JSON.stringify(allIcons), 'utf8');
console.log(`Wrote ${outputPath} (${allIcons.length} icons)`);
buildIconsJs({
inputPath: outputPath,
outputPath: path.join(outputDir, 'all_icons.js')
});
}
run().catch(error => {
console.error(error.message || error);
process.exit(1);
});