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:
+24
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
## 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
@@ -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();
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+165
@@ -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
@@ -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
@@ -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
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.
@@ -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);
|
||||
@@ -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')
|
||||
};
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
Executable
+89
@@ -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()
|
||||
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user