Repository: antfu-collective/icones
Branch: main
Commit: 0bc591826236
Files: 98
Total size: 172.1 KB
Directory structure:
gitextract_bmvhmq2c/
├── .github/
│ ├── renovate.json
│ └── workflows/
│ └── ci.yml
├── .gitignore
├── .npmrc
├── .vscode/
│ └── settings.json
├── LICENSE
├── README.md
├── electron/
│ ├── build/
│ │ └── icon.icns
│ ├── electron-builder.json5
│ ├── eslint.config.js
│ ├── package.json
│ └── src/
│ ├── main/
│ │ └── index.ts
│ └── renderer/
│ └── index.js
├── eslint.config.js
├── index.html
├── netlify.toml
├── package.json
├── pnpm-workspace.yaml
├── public/
│ └── search.xml
├── scripts/
│ └── prepare.ts
├── src/
│ ├── App.vue
│ ├── auto-imports.d.ts
│ ├── components/
│ │ ├── ActionsMenu.vue
│ │ ├── Bag.vue
│ │ ├── CollectionEntries.vue
│ │ ├── CollectionEntry.vue
│ │ ├── ColorPicker.vue
│ │ ├── CustomSelect.vue
│ │ ├── DarkSwitcher.vue
│ │ ├── Drawer.vue
│ │ ├── FAB.vue
│ │ ├── Footer.vue
│ │ ├── HelpPage.vue
│ │ ├── Icon.vue
│ │ ├── IconButton.vue
│ │ ├── IconDetail.vue
│ │ ├── IconSet.vue
│ │ ├── Icons.vue
│ │ ├── InstallIconSet.vue
│ │ ├── Modal.vue
│ │ ├── ModalDialog.vue
│ │ ├── Navbar.vue
│ │ ├── Notification.vue
│ │ ├── Progress.vue
│ │ ├── SearchBar.vue
│ │ ├── SettingsCollectionsList.vue
│ │ ├── SnippetPreview.vue
│ │ ├── WithNavbar.vue
│ │ └── electron/
│ │ ├── NavElectron.vue
│ │ ├── NavPlaceholder.vue
│ │ └── SearchElectron.vue
│ ├── components.d.ts
│ ├── data/
│ │ ├── index.ts
│ │ ├── search-alias.ts
│ │ └── variant-category.ts
│ ├── env.ts
│ ├── hooks/
│ │ ├── color.ts
│ │ ├── index.ts
│ │ └── search.ts
│ ├── html.d.ts
│ ├── main.css
│ ├── main.ts
│ ├── pages/
│ │ ├── [...all].vue
│ │ ├── collection/
│ │ │ └── [id].vue
│ │ ├── index.vue
│ │ └── settings.vue
│ ├── shims.d.ts
│ ├── store/
│ │ ├── collection.ts
│ │ ├── dark.ts
│ │ ├── dialog.ts
│ │ ├── index.ts
│ │ ├── indexedDB.ts
│ │ ├── localstorage.ts
│ │ ├── packing.ts
│ │ └── progress.ts
│ ├── sw.ts
│ └── utils/
│ ├── case.ts
│ ├── dataUrlToBlob.ts
│ ├── electron.ts
│ ├── icons.ts
│ ├── pack-worker-client.ts
│ ├── pack.ts
│ ├── query.ts
│ ├── sample.ts
│ ├── shiki.ts
│ ├── svg/
│ │ ├── base64.ts
│ │ ├── bufferToString.ts
│ │ ├── helpers.ts
│ │ ├── htmlToJsx.ts
│ │ ├── index.ts
│ │ ├── loader.ts
│ │ └── prettier.ts
│ ├── svgToPng.ts
│ └── worker/
│ ├── index.ts
│ └── types.ts
├── tsconfig.json
├── unocss.config.ts
└── vite.config.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/renovate.json
================================================
{
"extends": [
"config:recommended"
],
"rangeStrategy": "bump",
"packageRules": [
{
"description": "Group all non-major updates weekly except @iconify/json",
"extends": ["schedule:weekly"],
"matchPackagePatterns": ["*"],
"excludePackageNames": ["@iconify/json"],
"matchUpdateTypes": ["minor", "patch"],
"groupName": "all non-major dependencies",
"groupSlug": "all-minor-patch"
},
{
"description": "Create non-major @iconify/json updates daily",
"matchUpdateTypes": ["minor", "patch"],
"matchPackagePatterns": ["^@iconify/json"],
"extends": ["schedule:daily"],
"automerge": true
},
{
"description": "Suppress major updates using Dependency Dashboard",
"matchUpdateTypes": ["major"],
"dependencyDashboardApproval": true
}
]
}
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Set node
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: pnpm
- name: Install
run: pnpm install
- name: Lint
run: pnpm run lint
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Set node
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: pnpm
- name: Install
run: pnpm install
- name: Typecheck
run: pnpm run typecheck
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
node-version: [lts/*]
os: [ubuntu-latest]
fail-fast: false
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
registry-url: https://registry.npmjs.org/
cache: pnpm
- run: pnpm install
- name: Build
run: pnpm run build
================================================
FILE: .gitignore
================================================
node_modules
yarn-error.log
dist
.idea
src/assets/collections.json
.DS_Store
public/collections
public/lib
release
collections-info.json
collections-meta.json
dist-electron
dev-dist
================================================
FILE: .npmrc
================================================
shamefully-hoist=true
ignore-workspace-root-check=true
shell-emulator=true
================================================
FILE: .vscode/settings.json
================================================
{
"cSpell.words": [
"icones"
],
// Enable the ESlint flat config support
"eslint.experimental.useFlatConfig": true,
// Disable the default formatter, use eslint instead
"prettier.enable": false,
"editor.formatOnSave": false,
// Auto fix
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.organizeImports": "never"
},
// Silent the stylistic rules in you IDE, but still auto fix them
"eslint.rules.customizations": [
{ "rule": "style/*", "severity": "off" },
{ "rule": "*-indent", "severity": "off" },
{ "rule": "*-spacing", "severity": "off" },
{ "rule": "*-spaces", "severity": "off" },
{ "rule": "*-order", "severity": "off" },
{ "rule": "*-dangle", "severity": "off" },
{ "rule": "*-newline", "severity": "off" },
{ "rule": "*quotes", "severity": "off" },
{ "rule": "*semi", "severity": "off" }
],
// Enable eslint for all supported languages
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"html",
"markdown",
"json",
"jsonc",
"yaml"
]
}
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2020 Anthony Fu
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
Icônes
Icon Explorer with Instant searching, powered by Iconify
Go to App
Electron is coming...





Dark Mode is now Live!

### Features
- **Instant Fuzzy Searching** _- all are done locally, no web queries!_
- The **Bag** _- select your icons and pack them into a ready-to-use icon font!_
- _[svg-packer](https://github.com/antfu/svg-packer) was born from this XD_
- Copy the usage scripts
- SVGs direct download
- Mobile friendly
- Collection bookmarks
- Categories filters
- Dark mode
- Built with [Vite](https://github.com/vitejs/vite) and Vue 3
- If you like how it's built - try [🏕 Vitesse](https://github.com/antfu/vitesse), an opinionated starter template made from Icônes
### Community
- [VS Code Extension](https://github.com/afzalsayed96/vscode-icones) by [@afzalsayed96](https://github.com/afzalsayed96)
### TODOs
- Electron client (Coming!)
- Full-offline mode - pack all the icons
## License
MIT - Anthony Fu 2020
================================================
FILE: electron/electron-builder.json5
================================================
/**
* @see https://www.electron.build/configuration/configuration
*/
{
"productName": "Icônes",
"appId": "me.antfu.icones",
"directories": {
"output": "release"
},
"icon": "build/icon.png",
"mac": {
"target": "dmg"
},
"extraResources": [
{
"from": "build/icon.png",
"to": "icon.png"
}
],
"publish": {
"provider": "github",
"owner": "antfu",
"repo": "icones",
"private": false
},
"files": ["dist-electron", "dist"],
"win": {
"target": [
{
"target": "nsis",
"arch": ["x64"]
}
]
},
"nsis": {
"oneClick": false,
"perMachine": false,
"allowToChangeInstallationDirectory": true,
"deleteAppDataOnUninstall": false
}
}
================================================
FILE: electron/eslint.config.js
================================================
// @ts-check
import antfu from '@antfu/eslint-config'
export default antfu(
{
ignores: [
// eslint ignore globs here
],
},
{
rules: {
// overrides
},
},
)
================================================
FILE: electron/package.json
================================================
{
"name": "icones-electron",
"version": "0.0.0",
"appname": "Icônes",
"description": "Explorer for Iconify with Instant searching.",
"author": "Anthony Fu",
"license": "MIT",
"homepage": "https://github.com/antfu/icones#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/antfu/icones.git"
},
"bugs": {
"url": "https://github.com/antfu/icones/issues"
},
"main": "dist-electron/main/index.js",
"copyright": "Copyright © 2020 Anthony Fu",
"scripts": {
"dev": "vite ../ --port 3333 --mode electron",
"copy": "cp -r ../dist ./",
"build": "vite build ../ --mode electron && pnpm copy && electron-builder"
},
"devDependencies": {
"electron": "27.0.4",
"electron-builder": "24.6.4",
"electron-devtools-installer": "3.2.0",
"vite-plugin-electron": "0.15.4",
"vite-plugin-electron-renderer": "0.14.5",
"vite-plugin-esmodule": "1.5.0"
}
}
================================================
FILE: electron/src/main/index.ts
================================================
import path from 'node:path'
import { app, BrowserWindow, shell } from 'electron'
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer'
let mainWindow: BrowserWindow | null = null
app.disableHardwareAcceleration()
const PROJECT_ROOT = path.resolve(__dirname, '../..')
async function createMainWindow() {
const win = new BrowserWindow({
title: app.name,
show: false,
width: 660,
height: 500,
minWidth: 200,
minHeight: 200,
titleBarStyle: 'hiddenInset',
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
},
})
if (app.isPackaged) {
win.loadFile(path.join(PROJECT_ROOT, 'dist/index.html'))
win.removeMenu()
}
else {
win.loadURL('http://localhost:3333/')
win.webContents.openDevTools()
await installExtension(VUEJS_DEVTOOLS)
}
win.on('ready-to-show', () => {
win.show()
})
win.on('closed', () => {
mainWindow = null
})
const handleRedirect = (e: Event, url: string) => {
if (url !== win.webContents.getURL()) {
e.preventDefault()
shell.openExternal(url)
}
}
// @ts-expect-error - no types
win.webContents.on('will-navigate', handleRedirect)
win.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url)
return { action: 'deny' }
})
return win
}
if (!app.requestSingleInstanceLock())
app.quit()
app.on('window-all-closed', () => {
app.quit()
})
app.on('activate', async () => {
if (!mainWindow)
mainWindow = await createMainWindow()
})
; (async () => {
await app.whenReady()
mainWindow = await createMainWindow()
mainWindow.focus()
})()
.catch(console.error)
================================================
FILE: electron/src/renderer/index.js
================================================
================================================
FILE: eslint.config.js
================================================
// @ts-check
import antfu from '@antfu/eslint-config'
export default antfu(
{
ignores: [
'**/src/assets/collections.json',
'**/public/collections',
'**/public/lib',
'**/release',
'**/collections-info.json',
'**/collections-meta.json',
'**/dist-electron',
],
formatters: true,
},
)
================================================
FILE: index.html
================================================
Icônes
================================================
FILE: netlify.toml
================================================
[build]
publish = "dist"
command = "pnpm run build"
[build.environment]
NODE_VERSION = "20"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
================================================
FILE: package.json
================================================
{
"name": "icones",
"type": "module",
"version": "0.0.0",
"private": true,
"packageManager": "pnpm@10.24.0",
"author": "Anthony Fu",
"license": "MIT",
"homepage": "https://github.com/antfu/icones#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/antfu/icones.git"
},
"bugs": {
"url": "https://github.com/antfu/icones/issues"
},
"scripts": {
"postinstall": "esno scripts/prepare.ts",
"lint": "eslint .",
"dev": "vite --port 3333 --open",
"dev-pwa": "SW_DEV=true vite --port 3333",
"typecheck": "vue-tsc --noEmit",
"dev:electron": "npm -C ./electron run dev",
"build": "NODE_ENV=production vite build",
"build:electron": "NODE_ENV=production npm -C ./electron run build"
},
"dependencies": {
"@antfu/utils": "^9.3.0",
"@vueuse/core": "^14.1.0",
"dexie": "^4.2.1",
"file-saver": "^2.0.5",
"floating-vue": "^5.2.2",
"fzf": "^0.5.2",
"hotkeys-js": "^3.13.15",
"iconify-icon": "^3.0.2",
"prettier": "^3.7.3",
"ultrahtml": "^1.6.0",
"vue": "^3.5.25",
"vue-chemistry": "^0.2.2",
"vue-router": "^4.6.3"
},
"devDependencies": {
"@antfu/eslint-config": "^6.2.0",
"@iconify/json": "^2.2.413",
"@types/file-saver": "^2.0.7",
"@types/fs-extra": "^11.0.4",
"@vitejs/plugin-vue": "^6.0.2",
"client-zip": "^2.5.0",
"dayjs": "^1.11.19",
"eslint": "^9.39.1",
"eslint-plugin-format": "^1.0.2",
"esno": "^4.8.0",
"fast-glob": "^3.3.3",
"fs-extra": "^11.3.2",
"lru-cache": "^11.2.4",
"pnpm": "^10.24.0",
"shiki": "^3.17.1",
"svg-packer": "^1.0.0",
"typescript": "^5.9.3",
"unocss": "^66.5.9",
"unplugin-auto-import": "^20.3.0",
"unplugin-vue-components": "^30.0.0",
"vite": "^7.2.6",
"vite-plugin-pages": "^0.33.1",
"vite-plugin-pwa": "^1.2.0",
"vue-tsc": "^3.1.5"
}
}
================================================
FILE: pnpm-workspace.yaml
================================================
packages:
- electron
neverBuiltDependencies:
- ttf2woff2
================================================
FILE: public/search.xml
================================================
Icônes
⚡️ Iconify Icon Explorer
UTF-8
https://icones.js.org/android-chrome-192x192.png
https://icones.js.org/android-chrome-192x192.png
https://icones.js.org/favicon.svg
================================================
FILE: scripts/prepare.ts
================================================
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import fs from 'fs-extra'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const out = path.resolve(__dirname, '../public')
function ObjectPick(source: Record, keys: string[]) {
const obj: Record = {}
for (const key of keys)
obj[key] = source[key]
return obj
}
function humanFileSize(size: number) {
const i = Math.floor(Math.log(size) / Math.log(1024))
const v = (size / 1024 ** i)
return `${v.toFixed(2)} ${['B', 'kB', 'MB', 'GB', 'TB'][i]}`
}
async function prepareJSON() {
const dir = path.resolve(__dirname, '../node_modules/@iconify/json')
const collectionsDir = path.resolve(__dirname, '../public/collections')
const raw = await fs.readJSON(path.join(dir, 'collections.json'))
await fs.ensureDir(collectionsDir)
const collections = Object
.entries(raw)
.map(([id, v]) => ({
...(v as any),
id,
category: (v as any).hidden ? 'Deprecated / Unavailable' : (v as any).category,
}))
const collectionsMeta = []
for (const info of collections) {
const setData = await fs.readJSON(path.join(dir, 'json', `${info.id}.json`))
const icons = Object.keys(setData.icons)
const categories = setData.categories
const meta = { ...info, icons, categories }
const rawFilePath = path.join(collectionsDir, `${info.id}.json`)
const metaFilePath = path.join(collectionsDir, `${info.id}-meta.json`)
await fs.writeJSON(rawFilePath, setData)
await fs.writeJSON(metaFilePath, meta)
collectionsMeta.push(meta)
info.sampleIcons = icons.slice(0, 9)
if (info.id === 'logos') {
info.sampleIcons = [
'vue',
'vitejs',
'vitest',
'rollupjs',
'github-icon',
'eslint',
'esbuild',
'typescript-icon',
'netlify-icon',
]
}
// non-square icons
if (['flag', 'flagpack', 'cif', 'fa', 'fontisto', 'et', 'ps'].includes(info.id))
info.sampleIcons = info.sampleIcons.slice(0, 6)
info.prepacked = {
prefix: setData.prefix,
width: setData.width,
height: setData.height,
icons: ObjectPick(setData.icons, info.sampleIcons),
}
info.size = humanFileSize(fs.statSync(rawFilePath).size)
}
await fs.writeJSON(path.join(out, 'collections-meta.json'), collectionsMeta)
const infoOut = path.resolve(__dirname, '../src/data')
await fs.writeJSON(path.join(infoOut, 'collections-info.json'), collections)
}
prepareJSON()
================================================
FILE: src/App.vue
================================================
================================================
FILE: src/auto-imports.d.ts
================================================
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const EffectScope: typeof import('vue').EffectScope
const asyncComputed: typeof import('@vueuse/core').asyncComputed
const autoResetRef: typeof import('@vueuse/core').autoResetRef
const computed: typeof import('vue').computed
const computedAsync: typeof import('@vueuse/core').computedAsync
const computedEager: typeof import('@vueuse/core').computedEager
const computedInject: typeof import('@vueuse/core').computedInject
const computedWithControl: typeof import('@vueuse/core').computedWithControl
const controlledComputed: typeof import('@vueuse/core').controlledComputed
const controlledRef: typeof import('@vueuse/core').controlledRef
const createApp: typeof import('vue').createApp
const createEventHook: typeof import('@vueuse/core').createEventHook
const createGlobalState: typeof import('@vueuse/core').createGlobalState
const createInjectionState: typeof import('@vueuse/core').createInjectionState
const createReactiveFn: typeof import('@vueuse/core').createReactiveFn
const createRef: typeof import('@vueuse/core').createRef
const createReusableTemplate: typeof import('@vueuse/core').createReusableTemplate
const createSharedComposable: typeof import('@vueuse/core').createSharedComposable
const createTemplatePromise: typeof import('@vueuse/core').createTemplatePromise
const createUnrefFn: typeof import('@vueuse/core').createUnrefFn
const customRef: typeof import('vue').customRef
const debouncedRef: typeof import('@vueuse/core').debouncedRef
const debouncedWatch: typeof import('@vueuse/core').debouncedWatch
const defineAsyncComponent: typeof import('vue').defineAsyncComponent
const defineComponent: typeof import('vue').defineComponent
const eagerComputed: typeof import('@vueuse/core').eagerComputed
const effectScope: typeof import('vue').effectScope
const extendRef: typeof import('@vueuse/core').extendRef
const getCurrentInstance: typeof import('vue').getCurrentInstance
const getCurrentScope: typeof import('vue').getCurrentScope
const getCurrentWatcher: typeof import('vue').getCurrentWatcher
const h: typeof import('vue').h
const ignorableWatch: typeof import('@vueuse/core').ignorableWatch
const inject: typeof import('vue').inject
const injectLocal: typeof import('@vueuse/core').injectLocal
const isDefined: typeof import('@vueuse/core').isDefined
const isProxy: typeof import('vue').isProxy
const isReactive: typeof import('vue').isReactive
const isReadonly: typeof import('vue').isReadonly
const isRef: typeof import('vue').isRef
const isShallow: typeof import('vue').isShallow
const makeDestructurable: typeof import('@vueuse/core').makeDestructurable
const markRaw: typeof import('vue').markRaw
const nextTick: typeof import('vue').nextTick
const onActivated: typeof import('vue').onActivated
const onBeforeMount: typeof import('vue').onBeforeMount
const onBeforeRouteLeave: typeof import('vue-router').onBeforeRouteLeave
const onBeforeRouteUpdate: typeof import('vue-router').onBeforeRouteUpdate
const onBeforeUnmount: typeof import('vue').onBeforeUnmount
const onBeforeUpdate: typeof import('vue').onBeforeUpdate
const onClickOutside: typeof import('@vueuse/core').onClickOutside
const onDeactivated: typeof import('vue').onDeactivated
const onElementRemoval: typeof import('@vueuse/core').onElementRemoval
const onErrorCaptured: typeof import('vue').onErrorCaptured
const onKeyStroke: typeof import('@vueuse/core').onKeyStroke
const onLongPress: typeof import('@vueuse/core').onLongPress
const onMounted: typeof import('vue').onMounted
const onRenderTracked: typeof import('vue').onRenderTracked
const onRenderTriggered: typeof import('vue').onRenderTriggered
const onScopeDispose: typeof import('vue').onScopeDispose
const onServerPrefetch: typeof import('vue').onServerPrefetch
const onStartTyping: typeof import('@vueuse/core').onStartTyping
const onUnmounted: typeof import('vue').onUnmounted
const onUpdated: typeof import('vue').onUpdated
const onWatcherCleanup: typeof import('vue').onWatcherCleanup
const pausableWatch: typeof import('@vueuse/core').pausableWatch
const provide: typeof import('vue').provide
const provideLocal: typeof import('@vueuse/core').provideLocal
const reactify: typeof import('@vueuse/core').reactify
const reactifyObject: typeof import('@vueuse/core').reactifyObject
const reactive: typeof import('vue').reactive
const reactiveComputed: typeof import('@vueuse/core').reactiveComputed
const reactiveOmit: typeof import('@vueuse/core').reactiveOmit
const reactivePick: typeof import('@vueuse/core').reactivePick
const readonly: typeof import('vue').readonly
const ref: typeof import('vue').ref
const refAutoReset: typeof import('@vueuse/core').refAutoReset
const refDebounced: typeof import('@vueuse/core').refDebounced
const refDefault: typeof import('@vueuse/core').refDefault
const refManualReset: typeof import('@vueuse/core').refManualReset
const refThrottled: typeof import('@vueuse/core').refThrottled
const refWithControl: typeof import('@vueuse/core').refWithControl
const resolveComponent: typeof import('vue').resolveComponent
const resolveRef: typeof import('@vueuse/core').resolveRef
const resolveUnref: typeof import('@vueuse/core').resolveUnref
const shallowReactive: typeof import('vue').shallowReactive
const shallowReadonly: typeof import('vue').shallowReadonly
const shallowRef: typeof import('vue').shallowRef
const syncRef: typeof import('@vueuse/core').syncRef
const syncRefs: typeof import('@vueuse/core').syncRefs
const templateRef: typeof import('@vueuse/core').templateRef
const throttledRef: typeof import('@vueuse/core').throttledRef
const throttledWatch: typeof import('@vueuse/core').throttledWatch
const toRaw: typeof import('vue').toRaw
const toReactive: typeof import('@vueuse/core').toReactive
const toRef: typeof import('vue').toRef
const toRefs: typeof import('vue').toRefs
const toValue: typeof import('vue').toValue
const triggerRef: typeof import('vue').triggerRef
const tryOnBeforeMount: typeof import('@vueuse/core').tryOnBeforeMount
const tryOnBeforeUnmount: typeof import('@vueuse/core').tryOnBeforeUnmount
const tryOnMounted: typeof import('@vueuse/core').tryOnMounted
const tryOnScopeDispose: typeof import('@vueuse/core').tryOnScopeDispose
const tryOnUnmounted: typeof import('@vueuse/core').tryOnUnmounted
const unref: typeof import('vue').unref
const unrefElement: typeof import('@vueuse/core').unrefElement
const until: typeof import('@vueuse/core').until
const useActiveElement: typeof import('@vueuse/core').useActiveElement
const useAnimate: typeof import('@vueuse/core').useAnimate
const useArrayDifference: typeof import('@vueuse/core').useArrayDifference
const useArrayEvery: typeof import('@vueuse/core').useArrayEvery
const useArrayFilter: typeof import('@vueuse/core').useArrayFilter
const useArrayFind: typeof import('@vueuse/core').useArrayFind
const useArrayFindIndex: typeof import('@vueuse/core').useArrayFindIndex
const useArrayFindLast: typeof import('@vueuse/core').useArrayFindLast
const useArrayIncludes: typeof import('@vueuse/core').useArrayIncludes
const useArrayJoin: typeof import('@vueuse/core').useArrayJoin
const useArrayMap: typeof import('@vueuse/core').useArrayMap
const useArrayReduce: typeof import('@vueuse/core').useArrayReduce
const useArraySome: typeof import('@vueuse/core').useArraySome
const useArrayUnique: typeof import('@vueuse/core').useArrayUnique
const useAsyncQueue: typeof import('@vueuse/core').useAsyncQueue
const useAsyncState: typeof import('@vueuse/core').useAsyncState
const useAttrs: typeof import('vue').useAttrs
const useBase64: typeof import('@vueuse/core').useBase64
const useBattery: typeof import('@vueuse/core').useBattery
const useBluetooth: typeof import('@vueuse/core').useBluetooth
const useBreakpoints: typeof import('@vueuse/core').useBreakpoints
const useBroadcastChannel: typeof import('@vueuse/core').useBroadcastChannel
const useBrowserLocation: typeof import('@vueuse/core').useBrowserLocation
const useCached: typeof import('@vueuse/core').useCached
const useClipboard: typeof import('@vueuse/core').useClipboard
const useClipboardItems: typeof import('@vueuse/core').useClipboardItems
const useCloned: typeof import('@vueuse/core').useCloned
const useColorMode: typeof import('@vueuse/core').useColorMode
const useConfirmDialog: typeof import('@vueuse/core').useConfirmDialog
const useCountdown: typeof import('@vueuse/core').useCountdown
const useCounter: typeof import('@vueuse/core').useCounter
const useCssModule: typeof import('vue').useCssModule
const useCssVar: typeof import('@vueuse/core').useCssVar
const useCssVars: typeof import('vue').useCssVars
const useCurrentElement: typeof import('@vueuse/core').useCurrentElement
const useCycleList: typeof import('@vueuse/core').useCycleList
const useDark: typeof import('@vueuse/core').useDark
const useDateFormat: typeof import('@vueuse/core').useDateFormat
const useDebounce: typeof import('@vueuse/core').useDebounce
const useDebounceFn: typeof import('@vueuse/core').useDebounceFn
const useDebouncedRefHistory: typeof import('@vueuse/core').useDebouncedRefHistory
const useDeviceMotion: typeof import('@vueuse/core').useDeviceMotion
const useDeviceOrientation: typeof import('@vueuse/core').useDeviceOrientation
const useDevicePixelRatio: typeof import('@vueuse/core').useDevicePixelRatio
const useDevicesList: typeof import('@vueuse/core').useDevicesList
const useDisplayMedia: typeof import('@vueuse/core').useDisplayMedia
const useDocumentVisibility: typeof import('@vueuse/core').useDocumentVisibility
const useDraggable: typeof import('@vueuse/core').useDraggable
const useDropZone: typeof import('@vueuse/core').useDropZone
const useElementBounding: typeof import('@vueuse/core').useElementBounding
const useElementByPoint: typeof import('@vueuse/core').useElementByPoint
const useElementHover: typeof import('@vueuse/core').useElementHover
const useElementSize: typeof import('@vueuse/core').useElementSize
const useElementVisibility: typeof import('@vueuse/core').useElementVisibility
const useEventBus: typeof import('@vueuse/core').useEventBus
const useEventListener: typeof import('@vueuse/core').useEventListener
const useEventSource: typeof import('@vueuse/core').useEventSource
const useEyeDropper: typeof import('@vueuse/core').useEyeDropper
const useFavicon: typeof import('@vueuse/core').useFavicon
const useFetch: typeof import('@vueuse/core').useFetch
const useFileDialog: typeof import('@vueuse/core').useFileDialog
const useFileSystemAccess: typeof import('@vueuse/core').useFileSystemAccess
const useFocus: typeof import('@vueuse/core').useFocus
const useFocusWithin: typeof import('@vueuse/core').useFocusWithin
const useFps: typeof import('@vueuse/core').useFps
const useFullscreen: typeof import('@vueuse/core').useFullscreen
const useGamepad: typeof import('@vueuse/core').useGamepad
const useGeolocation: typeof import('@vueuse/core').useGeolocation
const useId: typeof import('vue').useId
const useIdle: typeof import('@vueuse/core').useIdle
const useImage: typeof import('@vueuse/core').useImage
const useInfiniteScroll: typeof import('@vueuse/core').useInfiniteScroll
const useIntersectionObserver: typeof import('@vueuse/core').useIntersectionObserver
const useInterval: typeof import('@vueuse/core').useInterval
const useIntervalFn: typeof import('@vueuse/core').useIntervalFn
const useKeyModifier: typeof import('@vueuse/core').useKeyModifier
const useLastChanged: typeof import('@vueuse/core').useLastChanged
const useLink: typeof import('vue-router').useLink
const useLocalStorage: typeof import('@vueuse/core').useLocalStorage
const useMagicKeys: typeof import('@vueuse/core').useMagicKeys
const useManualRefHistory: typeof import('@vueuse/core').useManualRefHistory
const useMediaControls: typeof import('@vueuse/core').useMediaControls
const useMediaQuery: typeof import('@vueuse/core').useMediaQuery
const useMemoize: typeof import('@vueuse/core').useMemoize
const useMemory: typeof import('@vueuse/core').useMemory
const useModel: typeof import('vue').useModel
const useMounted: typeof import('@vueuse/core').useMounted
const useMouse: typeof import('@vueuse/core').useMouse
const useMouseInElement: typeof import('@vueuse/core').useMouseInElement
const useMousePressed: typeof import('@vueuse/core').useMousePressed
const useMutationObserver: typeof import('@vueuse/core').useMutationObserver
const useNavigatorLanguage: typeof import('@vueuse/core').useNavigatorLanguage
const useNetwork: typeof import('@vueuse/core').useNetwork
const useNow: typeof import('@vueuse/core').useNow
const useObjectUrl: typeof import('@vueuse/core').useObjectUrl
const useOffsetPagination: typeof import('@vueuse/core').useOffsetPagination
const useOnline: typeof import('@vueuse/core').useOnline
const usePageLeave: typeof import('@vueuse/core').usePageLeave
const useParallax: typeof import('@vueuse/core').useParallax
const useParentElement: typeof import('@vueuse/core').useParentElement
const usePerformanceObserver: typeof import('@vueuse/core').usePerformanceObserver
const usePermission: typeof import('@vueuse/core').usePermission
const usePointer: typeof import('@vueuse/core').usePointer
const usePointerLock: typeof import('@vueuse/core').usePointerLock
const usePointerSwipe: typeof import('@vueuse/core').usePointerSwipe
const usePreferredColorScheme: typeof import('@vueuse/core').usePreferredColorScheme
const usePreferredContrast: typeof import('@vueuse/core').usePreferredContrast
const usePreferredDark: typeof import('@vueuse/core').usePreferredDark
const usePreferredLanguages: typeof import('@vueuse/core').usePreferredLanguages
const usePreferredReducedMotion: typeof import('@vueuse/core').usePreferredReducedMotion
const usePreferredReducedTransparency: typeof import('@vueuse/core').usePreferredReducedTransparency
const usePrevious: typeof import('@vueuse/core').usePrevious
const useRafFn: typeof import('@vueuse/core').useRafFn
const useRefHistory: typeof import('@vueuse/core').useRefHistory
const useResizeObserver: typeof import('@vueuse/core').useResizeObserver
const useRoute: typeof import('vue-router').useRoute
const useRouter: typeof import('vue-router').useRouter
const useSSRWidth: typeof import('@vueuse/core').useSSRWidth
const useScreenOrientation: typeof import('@vueuse/core').useScreenOrientation
const useScreenSafeArea: typeof import('@vueuse/core').useScreenSafeArea
const useScriptTag: typeof import('@vueuse/core').useScriptTag
const useScroll: typeof import('@vueuse/core').useScroll
const useScrollLock: typeof import('@vueuse/core').useScrollLock
const useSessionStorage: typeof import('@vueuse/core').useSessionStorage
const useShare: typeof import('@vueuse/core').useShare
const useSlots: typeof import('vue').useSlots
const useSorted: typeof import('@vueuse/core').useSorted
const useSpeechRecognition: typeof import('@vueuse/core').useSpeechRecognition
const useSpeechSynthesis: typeof import('@vueuse/core').useSpeechSynthesis
const useStepper: typeof import('@vueuse/core').useStepper
const useStorage: typeof import('@vueuse/core').useStorage
const useStorageAsync: typeof import('@vueuse/core').useStorageAsync
const useStyleTag: typeof import('@vueuse/core').useStyleTag
const useSupported: typeof import('@vueuse/core').useSupported
const useSwipe: typeof import('@vueuse/core').useSwipe
const useTemplateRef: typeof import('vue').useTemplateRef
const useTemplateRefsList: typeof import('@vueuse/core').useTemplateRefsList
const useTextDirection: typeof import('@vueuse/core').useTextDirection
const useTextSelection: typeof import('@vueuse/core').useTextSelection
const useTextareaAutosize: typeof import('@vueuse/core').useTextareaAutosize
const useThrottle: typeof import('@vueuse/core').useThrottle
const useThrottleFn: typeof import('@vueuse/core').useThrottleFn
const useThrottledRefHistory: typeof import('@vueuse/core').useThrottledRefHistory
const useTimeAgo: typeof import('@vueuse/core').useTimeAgo
const useTimeAgoIntl: typeof import('@vueuse/core').useTimeAgoIntl
const useTimeout: typeof import('@vueuse/core').useTimeout
const useTimeoutFn: typeof import('@vueuse/core').useTimeoutFn
const useTimeoutPoll: typeof import('@vueuse/core').useTimeoutPoll
const useTimestamp: typeof import('@vueuse/core').useTimestamp
const useTitle: typeof import('@vueuse/core').useTitle
const useToNumber: typeof import('@vueuse/core').useToNumber
const useToString: typeof import('@vueuse/core').useToString
const useToggle: typeof import('@vueuse/core').useToggle
const useTransition: typeof import('@vueuse/core').useTransition
const useUrlSearchParams: typeof import('@vueuse/core').useUrlSearchParams
const useUserMedia: typeof import('@vueuse/core').useUserMedia
const useVModel: typeof import('@vueuse/core').useVModel
const useVModels: typeof import('@vueuse/core').useVModels
const useVibrate: typeof import('@vueuse/core').useVibrate
const useVirtualList: typeof import('@vueuse/core').useVirtualList
const useWakeLock: typeof import('@vueuse/core').useWakeLock
const useWebNotification: typeof import('@vueuse/core').useWebNotification
const useWebSocket: typeof import('@vueuse/core').useWebSocket
const useWebWorker: typeof import('@vueuse/core').useWebWorker
const useWebWorkerFn: typeof import('@vueuse/core').useWebWorkerFn
const useWindowFocus: typeof import('@vueuse/core').useWindowFocus
const useWindowScroll: typeof import('@vueuse/core').useWindowScroll
const useWindowSize: typeof import('@vueuse/core').useWindowSize
const watch: typeof import('vue').watch
const watchArray: typeof import('@vueuse/core').watchArray
const watchAtMost: typeof import('@vueuse/core').watchAtMost
const watchDebounced: typeof import('@vueuse/core').watchDebounced
const watchDeep: typeof import('@vueuse/core').watchDeep
const watchEffect: typeof import('vue').watchEffect
const watchIgnorable: typeof import('@vueuse/core').watchIgnorable
const watchImmediate: typeof import('@vueuse/core').watchImmediate
const watchOnce: typeof import('@vueuse/core').watchOnce
const watchPausable: typeof import('@vueuse/core').watchPausable
const watchPostEffect: typeof import('vue').watchPostEffect
const watchSyncEffect: typeof import('vue').watchSyncEffect
const watchThrottled: typeof import('@vueuse/core').watchThrottled
const watchTriggerable: typeof import('@vueuse/core').watchTriggerable
const watchWithFilter: typeof import('@vueuse/core').watchWithFilter
const whenever: typeof import('@vueuse/core').whenever
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
}
================================================
FILE: src/components/ActionsMenu.vue
================================================
================================================
FILE: src/components/Bag.vue
================================================
Bag
{{ bags.length }} icons picked
$emit('select', e)" />
No icons yet ;)
================================================
FILE: src/components/CollectionEntries.vue
================================================
================================================
FILE: src/components/CollectionEntry.vue
================================================
{{ collection.author?.name }}
{{ collection.license?.title }}
{{ collection.total }} icons
================================================
FILE: src/components/ColorPicker.vue
================================================
================================================
FILE: src/components/CustomSelect.vue
================================================
{{ optgroup.label }}
{{ option.label }}
================================================
FILE: src/components/DarkSwitcher.vue
================================================
================================================
FILE: src/components/Drawer.vue
================================================
{{ collection.name }}
{{
collection.id === 'recent'
? `${recentIconIds.length} icons`
: collection.id !== 'all'
? `${collection.total} icons`
: `${collections.length} iconsets`
}}
================================================
FILE: src/components/FAB.vue
================================================
================================================
FILE: src/components/Footer.vue
================================================
================================================
FILE: src/components/HelpPage.vue
================================================
How to use the icon?
Copy per Icon
You can copy the icon as SVG to paste in almost any editor (Figma, Sketch, Illustrator, etc.), or copy as component to use in your web apps.
Iconify Runtime
Iconify provides a runtime solution that fetches icons on the go.
Refer its documentation for more details.
Atomic CSS
Created by the author of Icônes. With the power of UnoCSS, you can use the icons with Pure CSS using @unocss/preset-icons.
Check out this blog post for more.
Components
Created by the author of Icônes, unplugin-icons is a on-demand
solution to generate icons as components on the fly.
Check out this blog post for the story behind.
================================================
FILE: src/components/Icon.vue
================================================
================================================
FILE: src/components/IconButton.vue
================================================
{{ text }}
================================================
FILE: src/components/IconDetail.vue
================================================
Collection:
{{ collection.name }}
{{ groupName }}
Download
================================================
FILE: src/components/IconSet.vue
================================================
{{ collection.author?.name }}
{{ icons.length }} icons
{{ activeMode === 'select' ? 'Multiple select' : 'Name copying mode' }}
Copied
================================================
FILE: src/components/Icons.vue
================================================
================================================
FILE: src/components/InstallIconSet.vue
================================================
Install Iconify Iconset
{{ selectedPackageManager }}
{{ selectedPackageManager !== 'npm' ? ' add ' : ' i ' }} -D
@iconify-json/{{ props.collection.id }}
Copied
================================================
FILE: src/components/Modal.vue
================================================
================================================
FILE: src/components/ModalDialog.vue
================================================
================================================
FILE: src/components/Navbar.vue
================================================
================================================
FILE: src/components/Notification.vue
================================================
================================================
FILE: src/components/Progress.vue
================================================
{{ progressMessage }}
================================================
FILE: src/components/SearchBar.vue
================================================
================================================
FILE: src/components/SettingsCollectionsList.vue
================================================
================================================
FILE: src/components/SnippetPreview.vue
================================================
================================================
FILE: src/components/WithNavbar.vue
================================================
================================================
FILE: src/components/electron/NavElectron.vue
================================================
================================================
FILE: src/components/electron/NavPlaceholder.vue
================================================
================================================
FILE: src/components/electron/SearchElectron.vue
================================================
================================================
FILE: src/components.d.ts
================================================
/* eslint-disable */
// @ts-nocheck
// biome-ignore lint: disable
// oxlint-disable
// ------
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
ActionsMenu: typeof import('./components/ActionsMenu.vue')['default']
Bag: typeof import('./components/Bag.vue')['default']
CollectionEntries: typeof import('./components/CollectionEntries.vue')['default']
CollectionEntry: typeof import('./components/CollectionEntry.vue')['default']
ColorPicker: typeof import('./components/ColorPicker.vue')['default']
CustomSelect: typeof import('./components/CustomSelect.vue')['default']
DarkSwitcher: typeof import('./components/DarkSwitcher.vue')['default']
Drawer: typeof import('./components/Drawer.vue')['default']
FAB: typeof import('./components/FAB.vue')['default']
Footer: typeof import('./components/Footer.vue')['default']
HelpPage: typeof import('./components/HelpPage.vue')['default']
Icon: typeof import('./components/Icon.vue')['default']
IconButton: typeof import('./components/IconButton.vue')['default']
IconDetail: typeof import('./components/IconDetail.vue')['default']
Icons: typeof import('./components/Icons.vue')['default']
IconSet: typeof import('./components/IconSet.vue')['default']
InstallIconSet: typeof import('./components/InstallIconSet.vue')['default']
Modal: typeof import('./components/Modal.vue')['default']
ModalDialog: typeof import('./components/ModalDialog.vue')['default']
Navbar: typeof import('./components/Navbar.vue')['default']
NavElectron: typeof import('./components/electron/NavElectron.vue')['default']
NavPlaceholder: typeof import('./components/electron/NavPlaceholder.vue')['default']
Notification: typeof import('./components/Notification.vue')['default']
Progress: typeof import('./components/Progress.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SearchBar: typeof import('./components/SearchBar.vue')['default']
SearchElectron: typeof import('./components/electron/SearchElectron.vue')['default']
SettingsCollectionsList: typeof import('./components/SettingsCollectionsList.vue')['default']
SnippetPreview: typeof import('./components/SnippetPreview.vue')['default']
WithNavbar: typeof import('./components/WithNavbar.vue')['default']
}
}
================================================
FILE: src/data/index.ts
================================================
import type { IconifyJSON } from 'iconify-icon'
import { notNullish } from '@antfu/utils'
import { AsyncFzf } from 'fzf'
import { addCollection } from 'iconify-icon'
import { isLocalMode, staticPath } from '../env'
import { loadCollection, saveCollection } from '../store/indexedDB'
import {
favoritedCollectionIds,
isExcludedCollection,
isFavoritedCollection,
isRecentCollection,
recentCollectionIds,
} from '../store/localstorage'
import { inProgress, progressMessage } from '../store/progress'
import infoJSON from './collections-info.json'
import { variantCategories } from './variant-category'
export const specialTabs = ['all', 'recent']
export type PresentType = 'favorite' | 'recent' | 'normal'
export interface CollectionInfo {
id: string
name: string
author?: { name: string, url: string }
license?: { title: string, url: string }
url?: string
sampleIcons?: string[]
category?: string
palette?: string
total?: number
prepacked?: IconifyJSON
/**
* The icon set was deprecated and is no longer available
*/
hidden?: boolean
}
export interface CollectionMeta extends CollectionInfo {
icons: string[]
categories?: Record
variants?: Record
}
const loadedMeta = ref([])
const installed = ref([])
export const collections = infoJSON.map(c => Object.freeze(c as any as CollectionInfo))
export const enabledCollections = computed(() => collections.filter(c => !isExcludedCollection(c)))
export const categories = Array.from(new Set(collections.map(i => i.category).filter(notNullish)))
export const isSearchOpen = ref(false)
export const categorySearch = ref('')
const fzf = new AsyncFzf(collections, {
casing: 'case-insensitive',
fuzzy: 'v1',
selector: v => `${v.name} ${v.id} ${v.category} ${v.author}`,
})
export const filteredCollections = ref(enabledCollections.value)
watch([categorySearch, enabledCollections], ([q]) => {
if (!q) {
filteredCollections.value = enabledCollections.value
}
else {
fzf.find(q).then((result) => {
filteredCollections.value = result.map(i => i.item)
}).catch(() => {
// The search is canceled
})
}
})
export const sortedCollectionsInfo = computed(() =>
filteredCollections.value
.sort((a, b) => favoritedCollectionIds.value.indexOf(b.id) - favoritedCollectionIds.value.indexOf(a.id)),
)
export const favoritedCollections = computed(() =>
filteredCollections.value.filter(i => isFavoritedCollection(i.id))
.sort((a, b) => favoritedCollectionIds.value.indexOf(b.id) - favoritedCollectionIds.value.indexOf(a.id)),
)
export const recentCollections = computed(() =>
filteredCollections.value.filter(i => isRecentCollection(i.id))
.sort((a, b) => recentCollectionIds.value.indexOf(b.id) - recentCollectionIds.value.indexOf(a.id)),
)
export function isInstalled(id: string) {
return installed.value.includes(id)
}
export function isMetaLoaded(id: string) {
return !!loadedMeta.value.find(i => i.id === id)
}
// install the preview icons on the homepage
export function preInstall() {
for (const collection of collections) {
if (collection.prepacked)
addCollection(collection.prepacked as any)
}
}
export async function tryInstallFromLocal(id: string) {
if (specialTabs.includes(id))
return false
if (isLocalMode)
return true
if (installed.value.includes(id))
return true
const result = await loadCollection(id)
if (!result || !result.data)
return false
const data = result.data
addCollection(data)
installed.value.push(id)
return true
}
// load full iconset
export async function downloadAndInstall(id: string) {
if (specialTabs.includes(id))
return false
if (installed.value.includes(id))
return true
const data = Object.freeze(await fetch(`${staticPath}/collections/${id}.json`).then(r => r.json()))
addCollection(data)
installed.value.push(id)
if (!isLocalMode)
saveCollection(id, data) // async
return true
}
export async function cacheCollection(id: string) {
progressMessage.value = 'Downloading...'
inProgress.value = true
await nextTick()
await downloadAndInstall(id)
inProgress.value = false
}
export async function getCollectionMeta(id: string): Promise {
let meta = loadedMeta.value.find(i => i.id === id)
if (meta)
return meta
meta = await fetch(`${staticPath}/collections/${id}-meta.json`).then(r => r.json())
if (!meta)
return null
meta.variants ||= getVariantCategories(meta)
meta = Object.freeze(meta)
loadedMeta.value.push(meta)
return meta
}
function getVariantCategories(collection: CollectionMeta) {
const variantsRule = variantCategories[collection.id]
if (!variantsRule)
return
const variants: Record = {}
for (const icon of collection.icons) {
const name = variantsRule.find(i => typeof i[1] === 'string' ? icon.endsWith(i[1]) : i[1].test(icon))?.[0] || 'Regular'
if (!variants[name])
variants[name] = []
variants[name].push(icon)
}
return variants
}
export async function getFullMeta() {
if (loadedMeta.value.length === collections.length)
return loadedMeta.value
loadedMeta.value = Object.freeze(
await fetch(`${staticPath}/collections-meta.json`).then(r => r.json()),
)
return loadedMeta.value
}
preInstall()
================================================
FILE: src/data/search-alias.ts
================================================
export const searchAlias: string[][] = [
['account', 'person', 'profile', 'user'],
['add', 'create', 'new', 'plus'],
['alert', 'bell', 'notification', 'notify', 'reminder'],
['approve', 'like', 'recommend', 'thumbs-up'],
['left', 'previous'],
['next', 'right'],
['attach', 'connect', 'link'],
['bag', 'basket', 'cart'],
['bookmark', 'tag', 'label'],
['building', 'home', 'house'],
['calendar', 'date', 'event'],
['cancel', 'close'],
['delete', 'remove', 'trash'],
['chat', 'conversation', 'message'],
['clock', 'time', 'timer', 'alarm'],
['cog', 'gear', 'preferences', 'settings'],
['directory', 'folder'],
['disapprove', 'dislike', 'thumbs-down'],
['document', 'file', 'paper'],
['earth', 'globe', 'world', 'planet', 'global'],
['email', 'envelope', 'mail'],
['eye', 'view', 'visible'],
['favorite', 'heart', 'love'],
['feed', 'rss', 'subscribe', 'subscription'],
['list', 'menu'],
['lock', 'secure', 'security'],
['unlock', 'lock-open'],
['log-in', 'login', 'sign-in'],
['log-out', 'logout', 'sign-out'],
['magnifier', 'search', 'find', 'magnify'],
['photo', 'picture', 'image'],
['refresh', 'reload', 'update', 'sync'],
['speaker', 'audio', 'volume', 'sound'],
['speed', 'fast'],
['accessibility', 'ally', 'a11y'],
['edit', 'pen', 'pencil', 'write'],
['moon', 'night', 'dark'],
['sun', 'day'],
['bulb', 'idea'],
['pin', 'location', 'map', 'marker'],
['bot', 'robot', 'android'],
['db', 'database'],
['external', 'launch'],
['airplane', 'flight'],
['chart', 'graph'],
['monitor', 'screen'],
['video', 'film'],
['support', 'help', 'question'],
['mute', 'silence', 'sound-off', 'volume-off'],
['code', 'development', 'program', 'terminal', 'braces'],
['phone', 'call'],
['car', 'vehicle', 'transport', 'taxi'],
]
================================================
FILE: src/data/variant-category.ts
================================================
// @keep-sorted
export const variantCategories: Record = {
'academicons': [
['Square', '-square'],
],
'akar-icons': [
['Fill', '-fill'],
],
'ant-design': [
['Outlined', '-outlined'],
['Filled', '-filled'],
['Twotone', '-twotone'],
],
'basil': [
['Outline', '-outline'],
['Solid', '-solid'],
],
'bi': [
['Fill', '-fill'],
],
'clarity': [
['Line', '-line'],
['Solid', '-solid'],
['Outline Badged', '-outline-badged'],
['Solid Badged', '-solid-badged'],
['Outline Alerted', '-outline-alerted'],
['Solid Alerted', '-solid-alerted'],
],
'eos-icons': [
['Outlined', '-outlined'],
],
'fluent': [
['Filled 20', '-20-filled'],
['Regular 20', '-20-regular'],
['Filled 24', '-24-filled'],
['Regular 24', '-24-regular'],
['Filled 16', '-16-filled'],
['Regular 16', '-16-regular'],
['Filled 48', '-48-filled'],
['Regular 48', '-48-regular'],
['Filled 28', '-28-filled'],
['Regular 28', '-28-regular'],
['Filled 32', '-32-filled'],
['Regular 32', '-32-regular'],
['Filled 12', '-12-filled'],
['Regular 12', '-12-regular'],
['Filled 10', '-10-filled'],
['Regular 10', '-10-regular'],
],
'heroicons': [
['20 Solid', '-20-solid'],
['Solid', '-solid'],
],
'ic': [
['Outline', /^outline-/],
['Round', /^round-/],
['Sharp', /^sharp-/],
['Twotone', /^twotone-/],
['Baseline', /^baseline-/],
],
'iconamoon': [
['Bold', '-bold'],
['Duotone', '-duotone'],
['Fill', '-fill'],
['Light', '-light'],
['Thin', '-thin'],
],
'iconoir': [
['Solid', '-solid'],
],
'ion': [
['Outline', '-outline'],
['Sharp', '-sharp'],
['Regular', ''],
],
'line-md': [
['Twotone', '-twotone'],
['Twotone Transition', '-twotone-transition'],
['Loop', '-loop'],
['Filled', '-filled'],
],
'majesticons': [
['Line', '-line'],
],
'maki': [
['11px', '-11'],
],
'material-symbols-light': [
['Outline Rounded', '-outline-rounded'],
['Outline Sharp', '-outline-sharp'],
['Outline', '-outline'],
['Rounded', '-rounded'],
['Sharp', '-sharp'],
],
'material-symbols': [
['Outline Rounded', '-outline-rounded'],
['Outline', '-outline'],
['Rounded', '-rounded'],
['Sharp', '-sharp'],
],
'mdi': [
['Outline', '-outline'],
],
'mingcute': [
['Fill', '-fill'],
['Line', '-line'],
],
'mynaui': [
['Solid', '-solid'],
],
'octicon': [
['16px', '-16'],
],
'ph': [
['Bold', '-bold'],
['Duotone', '-duotone'],
['Fill', '-fill'],
['Light', '-light'],
['Thin', '-thin'],
],
'ri': [
['Fill', '-fill'],
['Line', '-line'],
],
'si': [
['Fill', '-fill'],
['Line', '-line'],
['Duotone', '-duotone'],
],
'solar': [
['Bold', '-bold'],
['Duotone', '-duotone'],
['Broken', '-broken'],
['Twotone', '-twotone'],
['Outline', '-outline'],
['Linear', '-linear'],
],
'tabler': [
['Filled', '-filled'],
['Lines', '-lines'],
],
'teenyicons': [
['Outline', '-outline'],
['Solid', '-solid'],
],
'twemoji': [
['Medium Dark Skin Tone', '-medium-dark-skin-tone'],
['Medium Light Skin Tone', '-medium-light-skin-tone'],
['Dark Skin Tone', '-dark-skin-tone'],
['Light Skin Tone', '-light-skin-tone'],
['Medium Skin Tone', '-medium-skin-tone'],
],
'typcn': [
['Outline', '-outline'],
['Thick', '-thick'],
],
'zondicons': [
['Outline', '-outline'],
['Solid', '-solid'],
],
}
================================================
FILE: src/env.ts
================================================
export const isElectron = import.meta.env.MODE === 'electron'
export const isVSCode = location.protocol === 'vscode-webview:'
export const isLocalMode = isElectron || isVSCode
export const basePath = isVSCode ? window.baseURI : '/'
export const staticPath = isVSCode
? window.staticURI
: (isElectron && import.meta.env.PROD) ? '../../app.asar/dist' : ''
================================================
FILE: src/hooks/color.ts
================================================
import { themeColor } from '../store'
export function useThemeColor() {
const style = computed(() => ({
'--theme-color': themeColor.value,
}))
return {
style,
}
}
================================================
FILE: src/hooks/index.ts
================================================
export * from './color'
export * from './search'
================================================
FILE: src/hooks/search.ts
================================================
import type { Ref } from 'vue'
import type { CollectionMeta } from '../data'
import { asyncExtendedMatch, AsyncFzf } from 'fzf'
import { computed, markRaw, ref, watch } from 'vue'
import { specialTabs } from '../data'
import { searchAlias } from '../data/search-alias'
import { cleanupQuery } from '../utils/query'
export function useSearch(collection: Ref) {
const route = useRoute()
const router = useRouter()
const category = computed({
get() {
return route.query.category as string || ''
},
set(value: string) {
router.replace({ query: cleanupQuery({ ...route.query, category: value }) })
},
})
const variant = computed({
get() {
return route.query.variant as string || ''
},
set(value: string) {
router.replace({ query: cleanupQuery({ ...route.query, variant: value }) })
},
})
const search = computed({
get() {
return route.query.s as string || ''
},
set(value: string) {
router.replace({ query: cleanupQuery({ ...route.query, s: value }) })
},
})
const isAll = computed(() => collection.value && specialTabs.includes(collection.value.id))
const searchParts = computed(() => search.value.trim().toLowerCase().split(' ').filter(Boolean))
const aliasedSearchCandidates = computed(() => {
const options = new Set([
searchParts.value.join(' '),
])
searchParts.value.forEach((i, idx, arr) => {
const alias = searchAlias.find(a => a.includes(i))
if (alias?.length) {
alias.forEach((a) => {
options.add([...arr.slice(0, idx), a, arr.slice(idx + 1)].filter(Boolean).join(' ').trim())
})
}
})
return [...options]
})
// Matching any character used in extended match
// https://github.com/junegunn/fzf#search-syntax
const useExtendedMatch = computed(() => /[ '^$!]/.test(search.value))
const iconSource = computed(() => {
if (!collection.value)
return []
return (category.value && variant.value)
? arrayIntersection(
collection.value.categories?.[category.value] || [],
collection.value.variants?.[variant.value] || [],
)
: category.value
? (collection.value.categories?.[category.value] || [])
: variant.value
? (collection.value.variants?.[variant.value] || [])
: collection.value.icons
})
const fzf = computed(() => {
return markRaw(new AsyncFzf(iconSource.value, {
casing: 'case-insensitive',
match: asyncExtendedMatch,
}))
})
const fzfFast = computed(() => {
return markRaw(new AsyncFzf(iconSource.value, {
casing: 'case-insensitive',
// v1 is faster
// https://fzf.netlify.app/docs/latest#async-finder-considering-other-options-first
fuzzy: 'v1',
}))
})
const icons = ref([])
function runSearch() {
const finder = (useExtendedMatch.value || aliasedSearchCandidates.value.length > 1)
? fzf
: fzfFast
const searchString = aliasedSearchCandidates.value.join(' | ')
finder.value.find(searchString)
.then((result) => {
icons.value = result.map(i => i.item)
})
.catch(() => {
// The search is canceled
})
}
const debouncedSearch = useDebounceFn(runSearch, 200)
watch([category, variant, () => collection.value?.id], () => {
runSearch()
})
watchEffect(() => {
if (!search.value) {
icons.value = iconSource.value
return
}
if (isAll.value && !useExtendedMatch.value) {
icons.value = iconSource.value
.filter(i => aliasedSearchCandidates.value.some(s => i.includes(s)))
return
}
debouncedSearch()
})
return {
collection,
search,
category,
variant,
icons,
}
}
// @unocss-include
export function getSearchHighlightHTML(
text: string,
search: string,
baseClass = 'color-fade',
activeClass = 'text-primary',
) {
const start = text.indexOf(search || '')
if (!search || start < 0)
return `${text}`
const end = start + search.length
return `${text.slice(0, start)}${text.slice(start, end)}${text.slice(end)}`
}
export function arrayIntersection(a: T[], b: T[]) {
return a.filter(i => b.includes(i))
}
================================================
FILE: src/html.d.ts
================================================
// for UnoCSS attributify mode compact in Volar
// refer: https://github.com/johnsoncodehk/volar/issues/1077#issuecomment-1145361472
declare module '@vue/runtime-dom' {
interface HTMLAttributes {
[key: string]: any
}
}
declare module '@vue/runtime-core' {
interface AllowedComponentProps {
[key: string]: any
}
}
export {}
================================================
FILE: src/main.css
================================================
body {
padding: 0;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
html.dark {
background: #181818;
color-scheme: dark;
}
html.dark .shiki,
html.dark .shiki span {
color: var(--shiki-dark);
}
html:not(.dark) .shiki,
html:not(.dark) .shiki span {
color: var(--shiki-light);
}
.btn {
--uno: border border-base rounded shadow-sm outline-none px-4 py-1 text-gray-600 text-sm transition-all bg-base
hover-(bg-gray-50 shadow) dark-(border-dark-200 text-gray-300) dark-hover-(border-primary bg-dark-100 text-primary)
focus-(shadow outline-none);
}
.btn.small {
--uno: px-2 py-1 text-sm;
}
.dragging {
-webkit-app-region: drag;
}
.non-dragging {
-webkit-app-region: no-drag;
}
.overflow-overlay {
overflow: auto;
overflow: overlay;
}
.overflow-y-overlay {
overflow-y: auto;
overflow-y: overlay;
}
.overflow-x-overlay {
overflow-x: auto;
overflow-x: overlay;
}
/* Scrollbar */
* {
-webkit-overflow-scrolling: touch;
-ms-overflow-style: -ms-autohiding-scrollbar;
scrollbar-width: 6px;
scrollbar-color: transparent;
}
::-webkit-scrollbar {
height: 6px;
width: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(128, 128, 128, 0.2);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:active {
background: rgba(128, 128, 128, 0.5);
border-radius: 3px;
}
/* Tootip */
.v-popper--theme-tooltip .v-popper__inner,
.v-popper--theme-menu .v-popper__inner {
--uno: important-bg-base;
}
.v-popper--theme-tooltip .v-popper__arrow-outer,
.v-popper--theme-menu .v-popper__arrow-outer {
--uno: important-border-none;
}
.v-popper--theme-tooltip .v-popper__inner,
.v-popper--theme-menu .v-popper__inner {
--uno: border border-base shadow-lg;
}
.v-popper--theme-menu .v-popper__arrow-outer {
visibility: hidden;
}
.v-popper--theme-menu .v-popper__arrow-inner {
visibility: hidden;
}
/* fallback black svg in dark mode */
.icons-item svg,
.dark .icons-item [fill='#000'],
.dark .icons-item [fill='#000000'],
.dark .icons-item [fill='black'] {
fill: currentColor;
}
.dark .icons-item [stroke='#000'],
.dark .icons-item [stroke='#000000'],
.dark .icons-item [stroke='black'] {
stroke: currentColor;
}
/* Color Mode transition */
::view-transition-old(root),
::view-transition-new(root) {
animation: none;
mix-blend-mode: normal;
}
::view-transition-old(root) {
z-index: 1;
}
::view-transition-new(root) {
z-index: 2147483646;
}
.dark::view-transition-old(root) {
z-index: 2147483646;
}
.dark::view-transition-new(root) {
z-index: 1;
}
================================================
FILE: src/main.ts
================================================
import { createApp } from 'vue'
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
import routes from '~pages'
import App from './App.vue'
import { basePath, isElectron } from './env'
import '@unocss/reset/tailwind.css'
import 'floating-vue/dist/style.css'
import './utils/electron'
import './main.css'
import 'uno.css'
const app = createApp(App)
const router = createRouter({
history: isElectron ? createWebHashHistory(basePath) : createWebHistory(basePath),
routes,
})
if (!isElectron && PWA) {
router.isReady().then(async () => {
const { registerSW } = await import('virtual:pwa-register')
registerSW({ immediate: true })
})
}
app.use(router)
app.mount('#app')
================================================
FILE: src/pages/[...all].vue
================================================
Not found
================================================
FILE: src/pages/collection/[id].vue
================================================
Loading...
================================================
FILE: src/pages/index.vue
================================================
Search for all icons...
{{ isMacOS ? '⌘' : 'Ctrl' }} + Enter
There is no result corresponding to your search query.
================================================
FILE: src/pages/settings.vue
================================================
Collections
Manage collections to be listed in the home page and search results.
================================================
FILE: src/shims.d.ts
================================================
interface Window {
// for vscode
baseURI?: string
staticURI?: string
}
declare const vscode: any
declare const __BUILD_TIME__: string
declare const PWA: boolean
declare module '*.vue' {
import type { defineComponent } from './vue'
const Component: ReturnType
export default Component
}
================================================
FILE: src/store/collection.ts
================================================
import type { CollectionMeta } from '../data'
import {
collections,
downloadAndInstall,
getCollectionMeta,
getFullMeta,
isInstalled,
isMetaLoaded,
tryInstallFromLocal,
} from '../data'
import { isLocalMode } from '../env'
import { useSearch } from '../hooks'
import { isExcludedCollection, recentIconIds } from './localstorage'
const currentCollectionId = ref('')
const loaded = ref(false)
const installed = ref(false)
const collection = shallowRef(null)
export const getSearchResults = createSharedComposable(() => {
return useSearch(collection)
})
export function useCurrentCollection() {
return collection
}
export function isCurrentCollectionLoading() {
return computed(() => !loaded.value)
}
const recentIconsCollection = computed((): CollectionMeta => ({
id: 'recent',
name: 'Recent',
icons: recentIconIds.value,
categories: Object.fromEntries(
Array.from(new Set(
recentIconIds.value.map(i => i.split(':')[0]),
))
.map(id => [collections.find(i => i.id === id)?.name || id, recentIconIds.value.filter(i => i.startsWith(`${id}:`))]),
),
}))
export async function setCurrentCollection(id: string) {
currentCollectionId.value = id
if (!id) {
loaded.value = false
installed.value = false
collection.value = null
return collection.value
}
loaded.value = isMetaLoaded(id)
installed.value = isInstalled(id)
if (!installed.value) {
if (isLocalMode)
installed.value = await downloadAndInstall(id)
else
installed.value = await tryInstallFromLocal(id)
}
if (id === 'all') {
const meta = await getFullMeta()
collection.value = {
id: 'all',
name: 'All',
icons: meta.flatMap((c) => {
if (isExcludedCollection(c))
return []
return c.icons.map(i => `${c.id}:${i}`)
}),
}
loaded.value = true
}
else if (id === 'recent') {
collection.value = recentIconsCollection.value
loaded.value = true
}
else {
collection.value = await getCollectionMeta(id)
loaded.value = true
}
return collection.value
}
================================================
FILE: src/store/dark.ts
================================================
export const isDark = useDark({
storageKey: 'icones-schema',
})
================================================
FILE: src/store/dialog.ts
================================================
export const showHelp = ref(false)
export const showCaseSelect = ref(false)
================================================
FILE: src/store/index.ts
================================================
export * from './collection'
export * from './dark'
export * from './dialog'
export * from './localstorage'
export * from './packing'
export * from './progress'
================================================
FILE: src/store/indexedDB.ts
================================================
import type { Table } from 'dexie'
import Dexie from 'dexie'
const db = new Dexie('icones')
db.version(1).stores({
collections: 'id, data',
})
const collections: Table = (db as any).collections
export async function loadCollection(id: string) {
return await collections.where({ id }).first()
}
export async function saveCollection(id: string, data: any) {
return await collections.put({ id, data }, 'id')
}
export default db
================================================
FILE: src/store/localstorage.ts
================================================
import type { CollectionInfo } from '../data'
import type { IdCase } from '../utils/case'
import { idCases } from '../utils/case'
const RECENT_COLLECTION_CAPACITY = 10
const RECENT_ICONS_CAPACITY = 100
export type ActiveMode = 'normal' | 'select' | 'copy'
export const themeColor = useStorage('icones-theme-color', '#329672')
export const iconSize = useStorage('icones-icon-size', '2xl')
export const previewColor = useStorage('icones-preview-color', '#888888')
export const copyPreviewColor = useStorage('icones-copy-preview-color', false)
export const listType = useStorage('icones-list-type', 'grid')
export const favoritedCollectionIds = useStorage('icones-fav-collections', [])
export const recentCollectionIds = useStorage('icones-recent-collections', [])
export const recentIconIds = useStorage('icones-recent-icons', [])
export const bags = useStorage('icones-bags', [])
export const activeMode = useStorage('active-mode', 'normal')
export const preferredCase = useStorage('icones-preferfed-case', 'iconify')
export const drawerCollapsed = useStorage('icones-drawer-collapsed', false)
export const selectedPackageManager = useStorage('icones-package-manager', 'pnpm')
export const excludedCollectionIds = useStorage('icones-excluded-collections', [])
export const excludedCategories = useStorage('icones-excluded-categories', [
'Archive / Unmaintained',
'Deprecated / Unavailable',
])
export function getTransformedId(icon: string) {
return idCases[preferredCase.value]?.(icon) || icon
}
export function isFavoritedCollection(id: string) {
return favoritedCollectionIds.value.includes(id)
}
export function isExcludedCollection(collection: CollectionInfo) {
return excludedCollectionIds.value.includes(collection.id) || excludedCategories.value.includes(collection.category || '')
}
export function isExcludedCategory(category: string | undefined) {
return category && excludedCategories.value.includes(category)
}
export function isRecentCollection(id: string) {
return recentCollectionIds.value.includes(id)
}
export function pushRecentCollection(id: string) {
recentCollectionIds.value = [id, ...recentCollectionIds.value.filter(i => i !== id)].slice(0, RECENT_COLLECTION_CAPACITY)
}
export function removeRecentCollection(id: string) {
recentCollectionIds.value = recentCollectionIds.value.filter(i => i !== id)
}
export function isRecentIcon(id: string) {
return recentIconIds.value.includes(id)
}
export function pushRecentIcon(id: string) {
recentIconIds.value = [id, ...recentIconIds.value.filter(i => i !== id)].slice(0, RECENT_ICONS_CAPACITY)
}
export function removeRecentIcon(id: string) {
recentIconIds.value = recentIconIds.value.filter(i => i !== id)
}
export function toggleFavoriteCollection(id: string) {
const index = favoritedCollectionIds.value.indexOf(id)
if (index >= 0)
favoritedCollectionIds.value.splice(index, 1)
else
favoritedCollectionIds.value.push(id)
}
export function toggleExcludedCollection(id: string) {
const index = excludedCollectionIds.value.indexOf(id)
if (index >= 0)
excludedCollectionIds.value.splice(index, 1)
else
excludedCollectionIds.value.push(id)
}
export function toggleExcludedCategory(category: string) {
const index = excludedCategories.value.indexOf(category)
if (index >= 0)
excludedCategories.value.splice(index, 1)
else
excludedCategories.value.push(category)
}
export function addToBag(id: string) {
if (!bags.value.includes(id))
bags.value.push(id)
}
export function removeFromBag(id: string) {
const index = bags.value.indexOf(id)
if (index >= 0)
bags.value.splice(index, 1)
}
export function inBag(id: string) {
return bags.value.includes(id)
}
export function toggleBag(id: string) {
const index = bags.value.indexOf(id)
if (index >= 0)
bags.value.splice(index, 1)
else
bags.value.push(id)
}
export function clearBag() {
bags.value = []
}
================================================
FILE: src/store/packing.ts
================================================
export const isPacking = ref(false)
export const packingProgress = ref(0)
================================================
FILE: src/store/progress.ts
================================================
export const inProgress = ref(false)
export const progress = ref(0)
export const progressMessage = ref('')
================================================
FILE: src/sw.ts
================================================
import { getIcons } from '@iconify/utils'
import { cacheNames, clientsClaim } from 'workbox-core'
import { cleanupOutdatedCaches, createHandlerBoundToURL, precacheAndRoute } from 'workbox-precaching'
import { NavigationRoute, registerRoute } from 'workbox-routing'
declare let self: ServiceWorkerGlobalScope
// self.__WB_MANIFEST is default injection point
const swManifest = self.__WB_MANIFEST
precacheAndRoute(swManifest)
// clean old assets
cleanupOutdatedCaches()
// to allow work offline
registerRoute(new NavigationRoute(
createHandlerBoundToURL('index.html'),
))
self.skipWaiting()
clientsClaim()
function buildCollectionResponseHeaders(cachedResponse: Response) {
const age = cachedResponse.headers.get('age')
const date = cachedResponse.headers.get('date')
const etag = cachedResponse.headers.get('etag')
const contentType = cachedResponse.headers.get('content-type')
const cacheControl = cachedResponse.headers.get('cache-control')
const headers: Record = {
'access-control-allow-headers': 'Origin, X-Requested-With, Content-Type, Accept, Accept-Encoding',
'access-control-allow-methods': 'GET, OPTIONS',
'access-control-allow-origin': '*',
'access-control-max-age': '86400',
'cache-control': 'public, max-age=604800, min-refresh=604800, immutable',
'content-type': 'application/json; charset=utf-8',
'cross-origin-resource-policy': 'cross-origin',
}
if (age)
headers.age = age
if (date)
headers.date = date
if (etag)
headers.etag = etag
if (contentType)
headers['content-type'] = contentType
if (cacheControl)
headers['cache-control'] = cacheControl
return headers
}
const swManifestMap = new Map(
swManifest.map((entry) => {
if (typeof entry === 'string') {
const e = entry[0] === '/' ? entry : `/${entry}`
return [e, e]
}
else {
const e = entry.url[0] === '/' ? entry.url : `/${entry.url}`
return [e, entry.revision ? `${e}?__WB_REVISION__=${entry.revision}` : e]
}
}),
)
async function getCollection(request: Request, name: string, icons: string[]) {
try {
const cache = await caches.open(cacheNames.precache)
const collectionUrl = `/collections/${name}.json`
const url = swManifestMap.get(collectionUrl) ?? collectionUrl
let cachedResponse = await cache.match(url)
if (!cachedResponse) {
cachedResponse = await fetch(url)
await cache.put(url, cachedResponse.clone())
}
const collection = await cachedResponse.json()
return new Response(JSON.stringify(getIcons(collection, icons)), {
status: cachedResponse.status,
statusText: cachedResponse.statusText,
headers: buildCollectionResponseHeaders(cachedResponse),
})
}
catch {
return await fetch(request)
}
}
const fetchRegex = /^https:\/\/(api\.iconify\.design|api\.simplesvg\.com|api\.unisvg\.com)\/(.*)\.json\?icons=(.*)$/
self.addEventListener('fetch', (e) => {
const url = e.request.url
const match = url.match(fetchRegex)
if (match) {
e.respondWith(getCollection(
e.request,
match[2],
match[3].replaceAll('%2C', ',').split(','),
))
}
})
================================================
FILE: src/utils/case.ts
================================================
export const idCases = {
bare(id: string) {
return id.replace(/^.*:/, '')
},
barePascal(id: string) {
return id.replace(/^.*:/, '').replace(/(?:^|[-_:]+)(\w)/g, (_, c) => c.toUpperCase())
},
iconify(id: string) {
return id
},
dash(id: string) {
return id.replace(/:/g, '-')
},
slash(id: string) {
return id.replace(/:/g, '/')
},
doubleHyphen(id: string) {
return id.replace(/:/g, '--')
},
camel(id: string) {
return id.replace(/[-_:]+(\w)/g, (_, c) => c.toUpperCase())
},
pascal(id: string) {
return id.replace(/(?:^|[-_:]+)(\w)/g, (_, c) => c.toUpperCase())
},
component(id: string) {
return `<${id.replace(/(?:^|[-_:]+)(\w)/g, (_, c) => c.toUpperCase())}/>`
},
componentKebab(id: string) {
return `<${id.replace(/:/g, '-')}/>`
},
unocssColon(id: string) {
return `i-${id}`
},
unocss(id: string) {
return `i-${id.replace(/:/g, '-')}`
},
iconifyTailwind(id: string) {
return `icon-[${id.replace(/:/g, '--')}]`
},
}
export type IdCase = keyof typeof idCases
================================================
FILE: src/utils/dataUrlToBlob.ts
================================================
export function dataUrlToBlob(dataurl: string) {
const parts = dataurl.split(',')
const type = parts[0].split(':')[1].split(';')[0]
const base64 = atob(parts[1])
const arr = new Uint8Array(base64.length)
for (let i = 0; i < base64.length; i++)
arr[i] = base64.charCodeAt(i)
return new Blob([arr], { type })
}
================================================
FILE: src/utils/electron.ts
================================================
import hotkeys from 'hotkeys-js'
import { isSearchOpen } from '../data'
import { isElectron } from '../env'
if (isElectron) {
hotkeys('ctrl+f, command+f', (e) => {
e.preventDefault()
isSearchOpen.value = !isSearchOpen.value
})
}
================================================
FILE: src/utils/icons.ts
================================================
import type { BuiltInParserName as PrettierParser } from 'prettier'
import type { CollectionInfo } from '../data'
import { isVSCode } from '../env'
import { getTransformedId } from '../store'
import { getSvgSymbol } from './pack'
import {
API_ENTRY,
bufferToString,
ClearSvg,
getSvg,
SvgToAstro,
SvgToDataURL,
SvgToJSX,
SvgToQwik,
SvgToReactNative,
SvgToSolid,
SvgToSvelte,
SvgToTSX,
SvgToVue,
toComponentName,
} from './svg'
import { svgToPngDataUrl } from './svgToPng'
export interface Snippet {
name: string
tag?: string
lang: string // for shiki
prettierParser: PrettierParser // for prettier
}
export { toComponentName }
export async function Download(blob: Blob, name: string) {
if (isVSCode) {
blob.arrayBuffer().then(
buffer => vscode.postMessage({
command: 'download',
name,
text: bufferToString(buffer),
}),
)
}
else {
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = name
a.click()
a.remove()
}
}
export const SnippetMap: Record> = {
Snippets: {
'svg': { name: 'SVG', lang: 'html', prettierParser: 'html' },
'svg-symbol': { name: 'SVG Symbol', lang: 'html', prettierParser: 'html' },
'png': { name: 'PNG', lang: 'html', prettierParser: 'html' },
'html': { name: 'Iconify', lang: 'html', prettierParser: 'html' },
'pure-jsx': { name: 'JSX', lang: 'jsx', prettierParser: 'typescript' },
},
Components: {
'vue': { name: 'Vue', lang: 'vue', prettierParser: 'vue' },
'vue-ts': { name: 'Vue', tag: 'TS', lang: 'vue', prettierParser: 'vue' },
'jsx': { name: 'React', lang: 'jsx', prettierParser: 'typescript' },
'tsx': { name: 'React', tag: 'TS', lang: 'tsx', prettierParser: 'typescript' },
'svelte': { name: 'Svelte', lang: 'svelte', prettierParser: 'typescript' },
'qwik': { name: 'Qwik', lang: 'tsx', prettierParser: 'typescript' },
'solid': { name: 'Solid', lang: 'tsx', prettierParser: 'typescript' },
'astro': { name: 'Astro', lang: 'astro', prettierParser: 'typescript' },
'react-native': { name: 'React Native', lang: 'tsx', prettierParser: 'typescript' },
'unplugin': { name: 'Unplugin Icons', lang: 'tsx', prettierParser: 'typescript' },
'unocss': { name: 'UnoCSS', lang: 'html', prettierParser: 'html' },
'unocss-attributify': { name: 'UnoCSS', tag: 'attributify', lang: 'html', prettierParser: 'html' },
},
Links: {
url: { name: 'URL', lang: 'html', prettierParser: 'html' },
data_url: { name: 'Data URL', lang: 'html', prettierParser: 'html' },
},
}
export async function getIconSnippet(
collections: CollectionInfo[],
icon: string,
type: string,
snippet = true,
color = 'currentColor',
): Promise {
if (!icon)
return
let url = `${API_ENTRY}/${icon}.svg`
if (color !== 'currentColor')
url = `${url}?color=${encodeURIComponent(color)}`
switch (type) {
case 'id':
return getTransformedId(icon)
case 'url':
return url
case 'html':
return ``
case 'css':
return `background: url('${url}') no-repeat center center / contain;`
case 'svg':
return await getSvg(collections, icon, '32', color)
case 'png':
return await svgToPngDataUrl(await getSvg(collections, icon, '32', color))
case 'svg-symbol':
return await getSvgSymbol(collections, icon, '32', color)
case 'data_url':
return SvgToDataURL(await getSvg(collections, icon, undefined, color))
case 'pure-jsx':
return ClearSvg(await getSvg(collections, icon, undefined, color))
case 'jsx':
return SvgToJSX(await getSvg(collections, icon, undefined, color), toComponentName(icon), snippet)
case 'tsx':
return SvgToTSX(await getSvg(collections, icon, undefined, color), toComponentName(icon), snippet)
case 'qwik':
return SvgToQwik(await getSvg(collections, icon, undefined, color), toComponentName(icon), snippet)
case 'vue':
return SvgToVue(await getSvg(collections, icon, undefined, color), toComponentName(icon))
case 'vue-ts':
return SvgToVue(await getSvg(collections, icon, undefined, color), toComponentName(icon), true)
case 'solid':
return SvgToSolid(await getSvg(collections, icon, undefined, color), toComponentName(icon), snippet)
case 'svelte':
return SvgToSvelte(await getSvg(collections, icon, undefined, color))
case 'astro':
return SvgToAstro(await getSvg(collections, icon, undefined, color))
case 'react-native':
return SvgToReactNative(await getSvg(collections, icon, undefined, color), toComponentName(icon), snippet)
case 'unplugin':
return `import ${toComponentName(icon)} from '~icons/${icon.split(':')[0]}/${icon.split(':')[1]}'`
case 'unocss':
return ``
case 'unocss-attributify':
return ``
}
}
export function getIconDownloadLink(icon: string) {
return `${API_ENTRY}/${icon}.svg?download=true&inline=false&height=auto`
}
================================================
FILE: src/utils/pack-worker-client.ts
================================================
import PackerWorker from './worker?worker'
export const packerWorker = new PackerWorker({
name: 'IconesPackWorker',
})
================================================
FILE: src/utils/pack.ts
================================================
import type { CollectionInfo } from '../data'
import type { PackType } from './svg'
import { Download } from './icons'
import { getSvg, LoadIconSvgs } from './svg'
export async function getSvgSymbol(
collections: CollectionInfo[],
icon: string,
size = '1em',
color = 'currentColor',
) {
const svgMarkup = await getSvg(collections, icon, size, color)
const symbolElem = document.createElementNS('http://www.w3.org/2000/svg', 'symbol')
const node = document.createElement('div') // Create any old element
node.innerHTML = svgMarkup
// Grab the inner HTML and move into a symbol element
symbolElem.innerHTML = node.querySelector('svg')!.innerHTML
symbolElem.setAttribute('viewBox', node.querySelector('svg')!.getAttribute('viewBox')!)
symbolElem.id = icon.replace(/:/, '-') // Simple slugify for quick symbol lookup
return symbolElem?.outerHTML
}
export async function PackSVGSprite(
collections: CollectionInfo[],
icons: string[],
options: any = {},
) {
if (!icons.length)
return
const data = await LoadIconSvgs(collections, icons)
let symbols = ''
for (const { name } of data)
symbols += `${await getSvgSymbol(collections, name, options.size, options.color)}\n`
const svg = ``
const blob = new Blob([svg], { type: 'image/svg+xml' })
Download(blob, 'sprite.svg')
}
export async function PackIconFont(
collections: CollectionInfo[],
icons: string[],
options: any = {},
) {
if (!icons.length)
return
const { packerWorker } = await import('./pack-worker-client')
return new Promise((resolve, reject) => {
packerWorker.addEventListener('message', (event) => {
if (event.data.error) {
reject(event.data.error)
return
}
const { blob, name } = event.data as { blob: ArrayBuffer, name: string }
Download(
new Blob([blob]),
name,
)
resolve()
}, { once: true })
const arrayBuffer = createArrayBufferFromCollections(collections)
packerWorker.postMessage({
operation: 'pack-font-zip',
collections: arrayBuffer,
payload: {
icons: toRaw(icons),
...toRaw(options),
},
}, [arrayBuffer])
})
}
export async function PackSvgZip(
collections: CollectionInfo[],
icons: string[],
name: string,
) {
if (!icons.length)
return
const { packerWorker } = await import('./pack-worker-client')
return new Promise((resolve, reject) => {
packerWorker.addEventListener('message', (event) => {
if (event.data.error) {
reject(event.data.error)
return
}
const { blob } = event.data as { blob: ArrayBuffer }
Download(
new Blob([blob]),
`${name}.zip`,
)
resolve()
}, { once: true })
const arrayBuffer = createArrayBufferFromCollections(collections)
packerWorker.postMessage({
operation: 'pack-svg-zip',
collections: arrayBuffer,
payload: {
icons: toRaw(icons),
},
}, [arrayBuffer])
})
}
export async function PackJsonZip(
collections: CollectionInfo[],
icons: string[],
name: string,
) {
if (!icons.length)
return
const { packerWorker } = await import('./pack-worker-client')
return new Promise((resolve, reject) => {
packerWorker.addEventListener('message', (event) => {
if (event.data.error) {
reject(event.data.error)
return
}
const { blob } = event.data as { blob: ArrayBuffer }
Download(
new Blob([blob]),
`${name}.zip`,
)
resolve()
}, { once: true })
const arrayBuffer = createArrayBufferFromCollections(collections)
packerWorker.postMessage({
operation: 'pack-json-zip',
collections: arrayBuffer,
payload: {
icons: toRaw(icons),
name,
},
}, [arrayBuffer])
})
}
export async function PackZip(
collections: CollectionInfo[],
icons: string[],
name: string,
type: PackType = 'svg',
) {
if (!icons.length)
return
const { packerWorker } = await import('./pack-worker-client')
return new Promise((resolve, reject) => {
packerWorker.addEventListener('message', (event) => {
if (event.data.error) {
reject(event.data.error)
return
}
const { blob } = event.data as { blob: ArrayBuffer }
Download(
new Blob([blob]),
`${name}-${type}.zip`,
)
resolve()
}, { once: true })
const arrayBuffer = createArrayBufferFromCollections(collections)
packerWorker.postMessage({
operation: 'pack-zip',
collections: arrayBuffer,
payload: {
icons: toRaw(icons),
name,
type,
},
}, [arrayBuffer])
})
}
function createArrayBufferFromCollections(
collections: CollectionInfo[],
) {
return new TextEncoder().encode(JSON.stringify(collections)).buffer
}
================================================
FILE: src/utils/query.ts
================================================
export function cleanupQuery(query: Record) {
for (const key of Object.keys(query)) {
if (!query[key])
delete query[key]
}
return query
}
================================================
FILE: src/utils/sample.ts
================================================
export function sample(arr: T[], num: number) {
return Array.from({ length: num }, () => arr[Math.floor(arr.length * Math.random())])
}
================================================
FILE: src/utils/shiki.ts
================================================
import type { HighlighterCore } from 'shiki/core'
import { createHighlighterCore } from 'shiki/core'
import { createJavaScriptRegexEngine } from 'shiki/engine/javascript'
export const shiki = computedAsync(async (onCancel) => {
const shiki = await createHighlighterCore({
engine: createJavaScriptRegexEngine(),
themes: [
() => import('shiki/themes/vitesse-dark.mjs'),
() => import('shiki/themes/vitesse-light.mjs'),
],
langs: [
() => import('shiki/langs/html.mjs'),
() => import('shiki/langs/jsx.mjs'),
() => import('shiki/langs/tsx.mjs'),
() => import('shiki/langs/vue.mjs'),
() => import('shiki/langs/astro.mjs'),
() => import('shiki/langs/svelte.mjs'),
],
})
onCancel(() => shiki?.dispose())
return shiki
})
export function highlight(code: string, lang: string) {
if (!shiki.value)
return code
return shiki.value.codeToHtml(code, {
lang,
defaultColor: false,
themes: {
dark: 'vitesse-dark',
light: 'vitesse-light',
},
})
}
================================================
FILE: src/utils/svg/base64.ts
================================================
/* eslint-disable eslint-comments/no-unlimited-disable */
/* eslint-disable */
// @ts-expect-error ignore
const Base64={_keyStr:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",encode:function(e){var t="";var n,r,i,s,o,u,a;var f=0;e=Base64._utf8_encode(e);while(f>2;o=(n&3)<<4|r>>4;u=(r&15)<<2|i>>6;a=i&63;if(isNaN(r)){u=a=64}else if(isNaN(i)){a=64}t=t+this._keyStr.charAt(s)+this._keyStr.charAt(o)+this._keyStr.charAt(u)+this._keyStr.charAt(a)}return t},decode:function(e){var t="";var n,r,i;var s,o,u,a;var f=0;e=e.replace(/[^A-Za-z0-9\+\/\=]/g,"");while(f>4;r=(o&15)<<4|u>>2;i=(u&3)<<6|a;t=t+String.fromCharCode(n);if(u!=64){t=t+String.fromCharCode(r)}if(a!=64){t=t+String.fromCharCode(i)}}t=Base64._utf8_decode(t);return t},_utf8_encode:function(e){e=e.replace(/\r\n/g,"\n");var t="";for(var n=0;n127&&r<2048){t+=String.fromCharCode(r>>6|192);t+=String.fromCharCode(r&63|128)}else{t+=String.fromCharCode(r>>12|224);t+=String.fromCharCode(r>>6&63|128);t+=String.fromCharCode(r&63|128)}}return t},_utf8_decode:function(e){var t="";var n=0;var r=c1=c2=0;while(n191&&r<224){c2=e.charCodeAt(n+1);t+=String.fromCharCode((r&31)<<6|c2&63);n+=2}else{c2=e.charCodeAt(n+1);c3=e.charCodeAt(n+2);t+=String.fromCharCode((r&15)<<12|(c2&63)<<6|c3&63);n+=3}}return t}}
export default Base64
================================================
FILE: src/utils/svg/bufferToString.ts
================================================
export function bufferToString(buffer: ArrayBuffer) {
return String.fromCharCode.apply(null, new Uint16Array(buffer) as any)
}
================================================
FILE: src/utils/svg/helpers.ts
================================================
import type { Node } from 'ultrahtml'
import type { CollectionInfo } from '../../data'
import { encodeSvgForCss } from '@iconify/utils'
import { parse, transformSync } from 'ultrahtml'
import Base64 from './base64'
import { HtmlToJSX } from './htmlToJsx'
import { getSvg } from './loader'
import { prettierCode } from './prettier'
export type PackType = 'svg' | 'tsx' | 'jsx' | 'vue' | 'solid' | 'qwik' | 'svelte' | 'astro' | 'react-native' | 'json'
export function normalizeZipFleName(svgName: string): string {
return svgName.replace(':', '-')
}
export function toComponentName(icon: string) {
return icon.split(/[:\-_]/).filter(Boolean).map(s => s[0].toUpperCase() + s.slice(1).toLowerCase()).join('')
}
export function ClearSvg(svgCode: string, reactJSX?: boolean) {
const result = transformSync(parse(svgCode).children[0] as Node, [
(node: Node): Node => {
if (node.name !== 'svg')
return node
const attributes = node.attributes || {}
// keep only 'viewBox', 'width', 'height', 'focusable', 'xmlns', 'xlink' attributes
const allowedAttributes = ['viewBox', 'width', 'height', 'focusable', 'xmlns', 'xlink']
for (const key of Object.keys(attributes)) {
if (!allowedAttributes.includes(key)) {
delete attributes[key]
}
}
node.attributes = attributes
return node
},
])
return HtmlToJSX(result, reactJSX)
}
export function SvgToDataURL(svg: string) {
const base64 = `data:image/svg+xml;base64,${Base64.encode(svg)}`
const plain = `data:image/svg+xml,${encodeSvgForCss(svg)}`
// Return the shorter of the two data URLs
return base64.length < plain.length ? base64 : plain
}
export function SvgToJSX(svg: string, name: string, snippet: boolean) {
const code = `
export function ${name}(props) {
return (
${ClearSvg(svg, true).replace(/