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
================================================
<h1 align="center">
Icônes
</h1>
<p align="center">Icon Explorer with <b>Instant</b> searching, powered by <a href="https://github.com/iconify/iconify" target="_blank">Iconify</a> </p>
<p align="center">
<a href="https://icones.js.org">Go to App</a>
</p>
<p align="center">
<sub><em>Electron is coming...</em></sub>
</p>





<p align="center">
<sub><em>Dark Mode is now Live!</em></sub>
</p>

<p align="center">
<a href="https://cdn.jsdelivr.net/gh/antfu/static/sponsors.svg">
<img src='https://cdn.jsdelivr.net/gh/antfu/static/sponsors.svg'/>
</a>
</p>
### 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<https://github.com/antfu>",
"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
================================================
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Icônes</title>
<link rel="icon" href="/favicon.svg" />
<link rel="icon" href="/favicon-dark.svg" media="(prefers-color-scheme: light)" />
<link rel="search" type="application/opensearchdescription+xml" href="/search.xml" title="Icônes" />
</head>
<body class="dragging bg-base color-base">
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
================================================
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<https://github.com/antfu>",
"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
================================================
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/">
<ShortName>Icônes</ShortName>
<Description>⚡️ Iconify Icon Explorer</Description>
<InputEncoding>UTF-8</InputEncoding>
<Image width="192" height="192">https://icones.js.org/android-chrome-192x192.png</Image>
<Image width="512" height="512">https://icones.js.org/android-chrome-192x192.png</Image>
<Image width="16" height="16">https://icones.js.org/favicon.svg</Image>
<Url type="text/html" template="https://icones.js.org/collection/all?s={searchTerms}"/>
</OpenSearchDescription>
================================================
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<string, any>, keys: string[]) {
const obj: Record<string, any> = {}
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
================================================
<script setup lang='ts'>
import { useThemeColor } from './hooks'
const { style } = useThemeColor()
</script>
<template>
<div class="flex flex-col h-screen overflow-hidden bg-base" :style="style">
<div class="h-full flex-auto overflow-overlay">
<RouterView />
</div>
<Progress />
</div>
</template>
================================================
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
================================================
<script setup lang='ts'>
import type { PropType } from 'vue'
import type { CollectionMeta } from '../data'
import { cacheCollection, downloadAndInstall, isInstalled } from '../data'
import { isElectron } from '../env'
import { activeMode, iconSize, inProgress, isFavoritedCollection, listType, progressMessage, toggleFavoriteCollection } from '../store'
import { PackIconFont, PackJsonZip, PackSvgZip } from '../utils/pack'
const props = defineProps({
collection: {
type: Object as PropType<CollectionMeta>,
required: true,
},
})
const menu = ref(
listType.value === 'list'
? 'list'
: iconSize.value === 'text-4xl'
? 'large'
: 'small',
)
async function packIconFont() {
if (!props.collection)
return
progressMessage.value = 'Downloading...'
inProgress.value = true
await nextTick()
await downloadAndInstall(props.collection.id)
progressMessage.value = 'Packing up...'
await nextTick()
await PackIconFont(
[props.collection],
props.collection.icons.map(i => `${props.collection!.id}:${i}`),
{ fontName: props.collection.name, fileName: props.collection.id },
)
inProgress.value = false
}
async function packSvgs() {
if (!props.collection)
return
progressMessage.value = 'Downloading...'
inProgress.value = true
await nextTick()
await downloadAndInstall(props.collection.id)
progressMessage.value = 'Packing up...'
await nextTick()
await PackSvgZip(
[props.collection],
props.collection.icons.map(i => `${props.collection!.id}:${i}`),
props.collection.id,
)
inProgress.value = false
}
async function packJson() {
if (!props.collection)
return
progressMessage.value = 'Downloading...'
inProgress.value = true
await nextTick()
await downloadAndInstall(props.collection.id)
progressMessage.value = 'Packing up...'
await nextTick()
await PackJsonZip(
[props.collection],
props.collection.icons.map(i => `${props.collection!.id}:${i}`),
props.collection.id,
)
inProgress.value = false
}
async function cache() {
if (!props.collection)
return
await cacheCollection(props.collection.id)
}
watch(
menu,
async (current, prev) => {
switch (current) {
case 'small':
iconSize.value = 'text-2xl'
listType.value = 'grid'
return
case 'large':
iconSize.value = 'text-4xl'
listType.value = 'grid'
return
case 'list':
iconSize.value = 'text-3xl'
listType.value = 'list'
return
case 'select':
activeMode.value = 'select'
break
case 'copy':
activeMode.value = 'copy'
break
case 'download_iconfont':
packIconFont()
break
case 'download_svgs':
packSvgs()
break
case 'download_json':
packJson()
break
case 'cache':
cache()
break
}
await nextTick()
menu.value = prev
},
{ flush: 'pre' },
)
const installed = computed(() => {
return props.collection && isInstalled(props.collection.id)
})
const favorited = computed(() => isFavoritedCollection(props.collection.id))
// const options = computed(() => [
// {
// label: 'Size',
// children: [
// { label: 'Small', value: 'small' },
// { label: 'Large', value: 'large' },
// { label: 'List', value: 'list' },
// ],
// },
// {
// label: 'Modes',
// children: [
// { label: 'Multiple select', value: 'select' },
// { label: 'Name copying mode', value: 'copy' },
// ],
// },
// /*
// TODO: due to this function requires to download and pack
// the full set, we should make some UI to aware users
// in browser version.
// */
// props.collection.id !== 'all'
// ? {
// label: 'Downloads',
// children: [
// (!isElectron && !installed) ? { label: 'Cache in Browser', value: 'cache' } : null,
// { label: 'Iconfont', value: 'download_iconfont', disabled: inProgress.value },
// { label: 'SVGs Zip', value: 'download_svgs', disabled: inProgress.value },
// { label: 'JSON', value: 'download_json', disabled: inProgress.value },
// ].filter(Boolean),
// }
// : null,
// ].filter(Boolean))
</script>
<template>
<div flex="~ gap3" text-xl items-center>
<DarkSwitcher />
<RouterLink
icon-button
i-carbon-settings
title="Settings"
to="/settings"
/>
<button
v-if="collection.id !== 'all'"
icon-button
:class="favorited ? 'i-carbon:star-filled' : 'i-carbon:star'"
title="Toggle Favorite"
@click="toggleFavoriteCollection(collection.id)"
/>
<!-- Download State -->
<div
v-if="installed && !isElectron"
icon-button class="!op50"
i-carbon-cloud-auditing
title="Cached in browser"
/>
<!-- Menu -->
<div icon-button cursor-pointer relative i-carbon-menu title="Menu">
<select
v-model="menu"
absolute w-full dark:bg-dark-100 text-base top-0 right-0 opacity-0 z-10
>
<optgroup label="Size">
<option value="small">
Small
</option>
<option value="large">
Large
</option>
<option value="list">
List
</option>
</optgroup>
<optgroup label="Modes">
<option value="select">
Multiple select
</option>
<option value="copy">
Name copying mode
</option>
</optgroup>
<!--
TODO: due to this function requires to download and pack
the full set, we should make some UI to aware users
in browser version.
-->
<optgroup v-if="collection.id !== 'all'" label="Downloads">
<option v-if="!isElectron && !installed" value="cache">
Cache in Browser
</option>
<option value="download_iconfont" :disabled="inProgress">
Iconfont
</option>
<option value="download_svgs" :disabled="inProgress">
SVGs Zip
</option>
<option value="download_json" :disabled="inProgress">
JSON
</option>
</optgroup>
</select>
</div>
<!-- TODO: improve design of custom select -->
<!-- <CustomSelect v-model="menu" :options="options">
<div icon-button cursor-pointer relative i-carbon-menu title="Menu" />
</CustomSelect> -->
</div>
</template>
================================================
FILE: src/components/Bag.vue
================================================
<script setup lang='ts'>
import type { PackType } from '../utils/svg'
import { collections } from '../data'
import { bags, clearBag } from '../store'
import { PackIconFont, PackSVGSprite, PackZip } from '../utils/pack'
const emit = defineEmits<{
(event: 'close'): void
(event: 'select', value: string): void
}>()
const showPackOption = ref(false)
function clear() {
// eslint-disable-next-line no-alert
if (confirm('Are you sure to remove all icons from the bag?')) {
clearBag()
emit('close')
}
}
async function packIconFont() {
// TODO: customzie
await PackIconFont(
collections,
bags.value,
)
}
async function packSVGSprite() {
await PackSVGSprite(
collections,
bags.value,
)
}
async function PackSvgs(type: PackType = 'svg') {
await PackZip(
collections,
bags.value,
'icones-bags',
type,
)
}
</script>
<template>
<div class="h-full flex flex-col w-screen md:w-96 xl:w-128">
<div
class="
py-3 px-6 flex flex-none border-b border-base
"
>
<div>
<NavPlaceholder class="md:hidden" />
<div class="text-lg">
Bag
</div>
<div class="opacity-50 text-xs">
{{ bags.length }} icons picked
</div>
</div>
<div class="flex-auto" />
<IconButton v-if="bags.length" class="text-xl mr-4 flex-none" icon="carbon:delete" @click="clear" />
<IconButton class="text-2xl flex-none" icon="carbon:close" @click="$emit('close')" />
</div>
<template v-if="bags.length">
<div class="flex-auto overflow-y-overflow py-3 px-1">
<Icons :icons="bags" @select="(e: any) => $emit('select', e)" />
</div>
<div
v-show="showPackOption"
class="relative flex-none border-t border-base py-3 px-6 text-2xl opacity-75"
>
<IconButton class="absolute top-0 right-0 p-3 text-2xl flex-none leading-none" icon="carbon:close" @click="showPackOption = false" />
<button class="btn small mr-1 mb-1 opacity-75" @click="PackSvgs('svg')">
SVG
</button>
<button class="btn small mr-1 mb-1 opacity-75" @click="PackSvgs('vue')">
Vue
</button>
<button class="btn small mr-1 mb-1 opacity-75" @click="PackSvgs('jsx')">
React
</button>
<button class="btn small mr-1 mb-1 opacity-75" @click="PackSvgs('tsx')">
React<sup class="opacity-50 -mr-1">TS</sup>
</button>
<button class="btn small mr-1 mb-1 opacity-75" @click="PackSvgs('svelte')">
Svelte
</button>
<button class="btn small mr-1 mb-1 opacity-75" @click="PackSvgs('qwik')">
Qwik
</button>
<button class="btn small mr-1 mb-1 opacity-75" @click="PackSvgs('solid')">
Solid
</button>
<button class="btn small mr-1 mb-1 opacity-75" @click="PackSvgs('astro')">
Astro
</button>
<button class="btn small mr-1 mb-1 opacity-75" @click="PackSvgs('react-native')">
React Native
</button>
<button class="btn small mr-1 mb-1 opacity-75" @click="PackSvgs('json')">
JSON
</button>
</div>
<div
class="
flex-none border-t border-base py-3 px-6 text-2xl opacity-75
"
>
<IconButton class="p-1 cursor-pointer hover:text-primary" icon="carbon:download" text="Download Zip" :active="true" @click="showPackOption = true" />
<IconButton class="p-1 cursor-pointer hover:text-primary" icon="carbon:function" text="Generate Icon Fonts" :active="true" @click="packIconFont" />
<IconButton class="p-1 cursor-pointer hover:text-primary" icon="carbon:apps" text="Download SVG Sprite" :active="true" @click="packSVGSprite" />
</div>
</template>
<template v-else>
<div class="text-center px-4 py-8 text-gray-500 italic font-light text-sm">
No icons yet ;)
</div>
</template>
</div>
</template>
================================================
FILE: src/components/CollectionEntries.vue
================================================
<script setup lang="ts">
import type { CollectionInfo, PresentType } from '../data'
defineProps<{
collections: CollectionInfo[]
type?: PresentType
}>()
</script>
<template>
<div class="collections-list grid gap2" p2>
<CollectionEntry
v-for="collection of collections"
:key="collection.id"
:type="type"
:collection="collection"
/>
</div>
</template>
<style>
.collections-list {
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
}
</style>
================================================
FILE: src/components/CollectionEntry.vue
================================================
<script setup lang="ts">
import type { CollectionInfo, PresentType } from '../data'
import { isFavoritedCollection, removeRecentCollection, toggleFavoriteCollection } from '../store'
defineProps<{
collection: CollectionInfo
type?: PresentType
}>()
</script>
<template>
<RouterLink
:key="collection.id"
p3 relative
border="~ base"
:class="{ 'border-dashed': collection.hidden }"
class="grid grid-cols-[1fr_90px] gap2 items-center color-base transition-all translate-z-0 group"
hover="text-primary !border-primary shadow"
:to="`/collection/${collection.id}`"
>
<div ml2>
<div class="flex-auto text-lg leading-1em my1" :class="{ 'line-through group-hover:no-underline': collection.hidden }">
{{ collection.name }}
<span inline-flex align-top flex="items-center gap-0.5" m="l--0.5">
<div v-if="isFavoritedCollection(collection.id)" op80 text-xs i-carbon-star-filled />
<div v-if="collection.hidden" op80 text-xs text-orange i-carbon:information-disabled />
</span>
</div>
<div flex="~ col auto" opacity-50 text-xs>
<span>{{ collection.author?.name }}</span>
<span op50>{{ collection.license?.title }}</span>
<span m1 />
<span>{{ collection.total }} icons</span>
</div>
</div>
<Icons
:icons="collection.sampleIcons"
:namespace="`${collection.id}:`"
color-class=""
size="xl"
spacing="m-1"
class="ma justify-center opacity-75 flex-wrap pointer-events-none"
/>
<div
absolute top--1px right--1px
flex="~ items-center"
op0 group-hover="op100 transition-all"
un-children="op-64 hover:op-100"
>
<button
border="~ primary" p2 bg-base
:title="isFavoritedCollection(collection.id) ? 'Remove from favorites' : 'Add to favorites'"
:class="{ 'border-dashed': collection.hidden }"
@click.prevent="toggleFavoriteCollection(collection.id)"
>
<div v-if="isFavoritedCollection(collection.id)" i-carbon-star-filled />
<div v-else i-carbon-star />
</button>
<button
v-if="type === 'recent'"
border="~ primary" p2 bg-base ml--1px
:title="type === 'recent' ? 'Remove from recent' : type === 'favorite' || isFavoritedCollection(collection.id) ? 'Remove from favorites' : 'Add to favorites'"
:class="{ 'border-dashed': collection.hidden }"
@click.prevent="removeRecentCollection(collection.id)"
>
<div i-carbon-delete />
</button>
</div>
</RouterLink>
</template>
================================================
FILE: src/components/ColorPicker.vue
================================================
<script setup lang="ts">
defineProps({
value: {
type: String,
required: true,
},
})
const emit = defineEmits(['update:value'])
</script>
<template>
<div class="relative">
<div>
<slot />
</div>
<input
class="absolute top-0 bottom-0 left-0 right-0 opacity-0 w-full h-full cursor-pointer"
:value="value"
type="color"
@input="e => emit('update:value', (e.target as any).value)"
>
</div>
</template>
================================================
FILE: src/components/CustomSelect.vue
================================================
<script setup lang='ts'>
defineProps<{
options: {
label: string
children: {
label: string
value: string
disabled?: boolean
}[]
}[]
modelValue: string
}>()
const emit = defineEmits(['update:modelValue'])
const visible = ref(false)
const target = ref(null)
onClickOutside(target, () => visible.value = false)
</script>
<template>
<div ref="target" relative>
<div @click="visible = true">
<slot />
</div>
<div
v-if="visible"
class="absolute rounded-md w-60 border-gray-300 border-1 dark:bg-dark-100 text-base top-20px right-0 z-10 shadow-md text-gray-500 px-4 py-2 bg-base"
>
<template v-for="(optgroup) in options" :key="optgroup.label">
<div text-gray-600 font-semibold py-1>
{{ optgroup.label }}
</div>
<div
v-for="(option) in optgroup.children"
:key="option.value"
class="cursor-pointer mx-2 text-sm leading-6 py-1 pl-2 dark:hover-bg-gray-800 hover-bg-gray-100 hover-rounded"
:class="{
'color-primary': option.value === modelValue,
'cursor-not-allowed opacity-50': option?.disabled,
}"
@click="{ emit('update:modelValue', option.value); visible = false; }"
>
{{ option.label }}
</div>
</template>
</div>
</div>
</template>
================================================
FILE: src/components/DarkSwitcher.vue
================================================
<script setup lang="ts">
import { isDark } from '../store'
const isAppearanceTransition = typeof document !== 'undefined'
// @ts-expect-error: Transition API
&& document.startViewTransition
&& !window.matchMedia('(prefers-reduced-motion: reduce)').matches
function toggleDark(event?: MouseEvent) {
if (!isAppearanceTransition || !event) {
isDark.value = !isDark.value
return
}
const x = event.clientX
const y = event.clientY
const endRadius = Math.hypot(
Math.max(x, innerWidth - x),
Math.max(y, innerHeight - y),
)
const transition = document.startViewTransition(async () => {
isDark.value = !isDark.value
await nextTick()
})
transition.ready.then(() => {
const clipPath = [
`circle(0px at ${x}px ${y}px)`,
`circle(${endRadius}px at ${x}px ${y}px)`,
]
document.documentElement.animate(
{
clipPath: isDark.value
? [...clipPath].reverse()
: clipPath,
},
{
duration: 400,
easing: 'ease-in',
fill: 'forwards',
pseudoElement: isDark.value
? '::view-transition-old(root)'
: '::view-transition-new(root)',
},
)
})
}
</script>
<template>
<button
icon-button
dark:i-carbon-moon i-carbon:sun
@click="toggleDark"
/>
</template>
================================================
FILE: src/components/Drawer.vue
================================================
<script setup lang='ts'>
import { categorySearch, filteredCollections, sortedCollectionsInfo, specialTabs } from '../data'
import { isElectron } from '../env'
import { isFavoritedCollection, recentIconIds, toggleFavoriteCollection } from '../store'
const route = useRoute()
const current = computed(() => route.path.split('/').slice(-1)[0])
const collections = computed(() => {
const _collections = categorySearch.value
? filteredCollections.value
: [
{ id: 'all', name: 'All' },
{ id: 'recent', name: 'Recent' },
...sortedCollectionsInfo.value,
]
return _collections.map(collection => ({
...collection,
to: {
name: 'collection-id',
params: { id: collection.id },
query: {
s: route.query.s,
},
},
}))
})
</script>
<template>
<div border="r base" relative>
<NavPlaceholder class="mb-4" />
<div
v-if="!isElectron"
sticky top-0 bg-base z-1
>
<div flex="~ justify-between" border="b base">
<button
icon-button text-xl px-4 py-3
@click="$router.replace('/')"
>
<div i-carbon:arrow-left />
</button>
</div>
<!-- Searching -->
<SearchBar
v-model:search="categorySearch"
placeholder="Search category..."
input-class="text-xs"
:border="false"
class="border-b border-base"
/>
</div>
<!-- Collections -->
<RouterLink
v-for="collection in collections"
:key="collection.id"
class="px-3 py-1 flex border-b border-base"
:to="collection.to"
>
<div
class="flex-auto py-1"
:class="collection.id === current ? 'text-primary' : ''"
>
<div class="text-base leading-tight">
{{ collection.name }}
<span v-if="collection.hidden" m="l--0.5" op80 text-xs text-orange inline-block align-top i-carbon:information-disabled />
</div>
<div class="text-xs block opacity-50 mt-1">
{{
collection.id === 'recent'
? `${recentIconIds.length} icons`
: collection.id !== 'all'
? `${collection.total} icons`
: `${collections.length} iconsets`
}}
</div>
</div>
<button
v-if="!specialTabs.includes(collection.id)"
icon-button
:class="isFavoritedCollection(collection.id) ? 'op50 hover:op100' : 'op0 hover:op50' "
class="flex-none text-lg p0.5 -mr-1 hover:text-primary flex"
@click="toggleFavoriteCollection(collection.id)"
>
<div :class="isFavoritedCollection(collection.id) ? 'i-carbon-star-filled' : 'i-carbon-star'" ma />
</button>
</RouterLink>
</div>
</template>
================================================
FILE: src/components/FAB.vue
================================================
<script setup lang='ts'>
defineProps({
icon: {
type: String,
required: true,
},
number: {
type: Number,
default: 0,
},
})
</script>
<template>
<div
class="
p-4 m-6 fixed bottom-0 right-0 shadow-lg bg-white rounded-full text-2xl cursor-pointer border border-transparent hover:bg-gray-50
dark:bg-dark-100 dark:border dark:border-dark-300 dark:hover:bg-dark-200
"
>
<Icon class="block text-gray-700 dark:text-gray-400" :icon="icon" />
<div
v-if="number"
class="absolute top-0 right-0 -mt-1 -mr-1 bg-primary text-white text-xs leading-none rounded-full shadow text-center"
style="padding: 5px; min-width: 22px"
>
{{ number }}
</div>
</div>
</template>
================================================
FILE: src/components/Footer.vue
================================================
<script setup lang="ts">
const buildTime = __BUILD_TIME__
const timeAgo = useTimeAgo(new Date(buildTime))
</script>
<template>
<footer class="text-center text-sm pt-8 pb-6">
<p class="color-fade">
built by
<a
class="opacity-75 hover:opacity-100"
href="https://github.com/antfu"
target="_blank"
>@antfu</a>,
powered by
<a
class="opacity-75 hover:opacity-100"
href="https://iconify.design"
target="_blank"
>Iconify</a>
</p>
<div color-fade mt-1 op50 italic>
Last update: {{ buildTime }} ({{ timeAgo }})
</div>
</footer>
</template>
================================================
FILE: src/components/HelpPage.vue
================================================
<template>
<div class="p-6 w-120 help-page">
<p class="mb-2 opacity-75">
How to use the icon?
</p>
<h2 class="bold text-lg mb-1">
Copy per Icon
</h2>
<p>
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.
</p>
<h2 class="bold text-lg mt-5 mb-1">
Iconify Runtime
</h2>
<p>
Iconify provides a runtime solution that fetches icons on the go.
Refer its <a href="https://iconify.design/" target="_blank">documentation</a> for more details.
</p>
<h2 class="bold text-lg mt-5 mb-1">
Atomic CSS
</h2>
<p>
Created by the author of <b>Icônes</b>. With the power of <a href="https://github.com/antfu/unocss">UnoCSS</a>, you can use the icons with <b>Pure CSS</b> using <a href="https://github.com/unocss/unocss/tree/main/packages-presets/preset-icons" target="_blank"><code>@unocss/preset-icons</code></a>.
</p>
<p class="mt-2">
Check out <a href="https://antfu.me/posts/icons-in-pure-css" target="_blank">this blog post</a> for more.
</p>
<h2 class="bold text-lg mt-5 mb-1">
Components
</h2>
<p>
Created by the author of <b>Icônes</b>, <a href="https://github.com/antfu/unplugin-icons" target="_blank"><code>unplugin-icons</code></a> is a on-demand
solution to generate icons as components on the fly.
</p>
<p class="mt-2">
Check out <a href="https://antfu.me/posts/journey-with-icons-continues" target="_blank">this blog post</a> for the story behind.
</p>
</div>
</template>
<style lang="postcss">
.help-page p {
@apply text-black/60 dark:text-white/60;
}
.help-page a {
@apply text-primary opacity-75 hover:opacity-100;
}
</style>
================================================
FILE: src/components/Icon.vue
================================================
<script lang="ts">
import { loadIcon } from 'iconify-icon'
import { LRUCache } from 'lru-cache'
const cache = new LRUCache<string, HTMLElement>({
max: 1_000,
})
const mounted = new WeakSet<HTMLElement>()
function getIcon(name: string) {
const el = cache.get(name)
if (el) {
if (!mounted.has(el)) {
mounted.add(el)
return el
}
}
const icon = document.createElement('iconify-icon')
icon.setAttribute('icon', name)
cache.set(name, icon)
mounted.add(icon)
return icon
}
function unmountIcon(name: string, icon: HTMLElement) {
mounted.delete(icon)
cache.set(name, icon)
}
</script>
<script setup lang="ts">
const props = defineProps({
icon: {
type: String,
required: true,
},
class: {
type: String,
default: '',
},
outerClass: {
type: String,
default: '',
},
})
const el = ref<HTMLDivElement>()
let node: HTMLElement | undefined
const widthStyle = ref<string | undefined>()
watchEffect(() => {
if (node)
node.className = props.class
})
onMounted(() => {
const icon = props.icon
node = getIcon(props.icon)
el.value?.appendChild(node)
loadIcon(icon).then((data) => {
widthStyle.value = `width: ${(data.width ?? 16) / (data.height ?? 16)}em;`
}).catch(console.error)
})
onBeforeUnmount(() => {
if (node)
unmountIcon(props.icon, node)
})
</script>
<template>
<div ref="el" class="icon-container" :class="[props.class, props.outerClass]" :style="widthStyle" />
</template>
<style>
iconify-icon {
min-width: 1em;
min-height: 1em;
display: block;
}
.icon-container {
display: inline-block;
vertical-align: middle;
line-height: 1em !important;
box-sizing: content-box;
}
</style>
================================================
FILE: src/components/IconButton.vue
================================================
<script setup lang='ts'>
defineProps({
icon: {
type: String,
required: true,
},
active: {
type: Boolean,
default: false,
},
none: {
type: Boolean,
default: false,
},
text: {
type: String,
default: '',
},
to: {
type: String,
default: '',
},
})
</script>
<template>
<component
:is="to ? 'RouterLink' : 'button'"
:to="to"
class="icon-button m-auto"
:class="none ? '' : active ? 'opacity-100 hover:opacity-100' : 'opacity-25 hover:opacity-50'"
>
<Icon
:key="icon"
:icon="icon"
class="inline-block align-middle"
/>
<div
v-if="text"
class="text-xs ml-2 inline-block align-middle"
>
{{ text }}
</div>
</component>
</template>
================================================
FILE: src/components/IconDetail.vue
================================================
<script setup lang='ts'>
import { collections } from '../data'
import { activeMode, copyPreviewColor, getTransformedId, inBag, preferredCase, previewColor, pushRecentIcon, showCaseSelect, showHelp, toggleBag } from '../store'
import { idCases } from '../utils/case'
import { dataUrlToBlob } from '../utils/dataUrlToBlob'
import { Download, getIconSnippet, SnippetMap, toComponentName } from '../utils/icons'
import InstallIconSet from './InstallIconSet.vue'
const props = defineProps({
icon: {
type: String,
required: true,
},
showCollection: {
type: Boolean,
required: true,
},
})
const emit = defineEmits(['close', 'copy', 'next', 'prev'])
const caseSelector = ref<HTMLDivElement>()
const transformedId = computed(() => getTransformedId(props.icon))
const color = computed(() => copyPreviewColor.value ? previewColor.value : 'currentColor')
onClickOutside(caseSelector, () => {
showCaseSelect.value = false
})
onKeyStroke('ArrowLeft', (e) => {
if (!props.icon)
return
emit('prev')
e.preventDefault()
})
onKeyStroke('ArrowRight', (e) => {
if (!props.icon)
return
emit('next')
e.preventDefault()
})
async function copyText(text?: string) {
if (text) {
try {
await navigator.clipboard.writeText(text)
return true
}
catch {
}
}
return false
}
async function copyPng(dataUrl: string): Promise<boolean> {
try {
const blob = dataUrlToBlob(dataUrl)
const item = new ClipboardItem({ 'image/png': blob })
await navigator.clipboard.write([item])
return true
}
catch (e) {
console.error('Failed to copy png error', e)
return false
}
}
async function copy(type: string) {
pushRecentIcon(props.icon)
const svg = await getIconSnippet(collections, props.icon, type, true, color.value)
if (!svg)
return
emit(
'copy',
type === 'png'
? await copyPng(svg)
: await copyText(svg),
)
}
async function download(type: string) {
pushRecentIcon(props.icon)
const text = await getIconSnippet(collections, props.icon, type, false, color.value)
if (!text)
return
const ext = (type === 'solid' || type === 'qwik' || type === 'react-native') ? 'tsx' : type
const name = `${toComponentName(props.icon)}.${ext}`
const blob = type === 'png'
? dataUrlToBlob(text)
: new Blob([text], { type: 'text/plain;charset=utf-8' })
Download(blob, name)
}
function toggleSelectingMode() {
switch (activeMode.value) {
case 'select':
activeMode.value = 'normal'
break
default:
activeMode.value = 'select'
emit('close')
break
}
}
const collection = computed(() => {
const id = props.icon.split(':')[0]
return collections.find(i => i.id === id)
})
</script>
<template>
<div class="p-2 flex flex-col flex-wrap md:flex-row md:text-left relative">
<IconButton
class="absolute top-0 right-0 p-3 text-2xl flex-none leading-none" icon="carbon:close"
@click="$emit('close')"
/>
<div :style="{ color: previewColor }">
<ColorPicker v-model:value="previewColor" class="inline-block">
<Icon :key="icon" outer-class="p-4 text-8xl" :icon="icon" />
</ColorPicker>
</div>
<div class="px-6 py-2 mb-2 md:px-2 md:py-4">
<button class="op35 hover:text-primary hover:op100 text-sm !outline-none" @click="showHelp = !showHelp">
How to use the icon?
</button>
<div class="flex op75 relative font-mono">
{{ transformedId }}
<IconButton icon="carbon:copy" class="ml-2" @click="copy('id')" />
<IconButton icon="carbon:chevron-up" class="ml-2" @click="showCaseSelect = !showCaseSelect" />
<div class="flex-auto" />
<div
v-if="showCaseSelect" ref="caseSelector"
class="absolute left-0 bottom-1.8em text-sm rounded shadow p-2 bg-base dark:border dark:border-dark-200"
>
<div
v-for="[k, v] of Object.entries(idCases)" :key="k" class="flex items-center p-1 cursor-pointer"
:class="k === preferredCase ? 'text-primary' : ''" @click="preferredCase = k as any"
>
<Icon
icon="carbon:checkmark" class="text-primary text-lg" outer-class="mr-1"
:class="k === preferredCase ? '' : 'opacity-0'"
/>
<span class="flex-auto mr-2">{{ v(icon) }}</span>
</div>
</div>
</div>
<div v-if="collection?.license">
<a class="text-xs opacity-50 hover:opacity-100" :href="collection.license.url" target="_blank">{{
collection.license.title }}</a>
</div>
<p v-if="showCollection && collection" class="flex mb-1 op50 text-sm">
Collection:
<RouterLink
class="ml-1 text-gray-600 hover:text-gray-700 dark:text-gray-300 dark:hover:text-gray-200"
:to="`/collection/${collection.id}`"
>
{{ collection.name }}
</RouterLink>
</p>
<div>
<button
class="
inline-block leading-1em border border-base my-2 mr-2 font-sans pl-2 pr-3 py-1 rounded-full text-sm cursor-pointer
hover:bg-gray-50 dark:hover:bg-dark-200
" :class="inBag(icon) ? 'text-primary' : 'op50'" @click="toggleBag(icon)"
>
<template v-if="inBag(icon)">
<Icon class="inline-block text-lg align-middle" icon="carbon:shopping-bag" />
<span class="inline-block align-middle ml1">in bag</span>
</template>
<template v-else>
<Icon class="inline-block text-lg align-middle" icon="carbon:add" />
<span class="inline-block align-middle ml1">add to bag</span>
</template>
</button>
<button
v-if="inBag(icon)" class="
inline-block leading-1em border border-base my-2 mr-2 font-sans pl-2 pr-3 py-1 rounded-full text-sm cursor-pointer
hover:bg-gray-50 dark:hover:bg-dark-200
" :class="activeMode === 'select' ? 'text-primary' : 'op50'" @click="toggleSelectingMode"
>
<Icon class="inline-block text-lg align-middle" icon="carbon:list-checked" />
<span class="inline-block align-middle ml1">multiple select</span>
</button>
<button
class="
inline-block leading-1em border border-base my-2 mr-2 font-sans pl-2 pr-3 py-1 rounded-full text-sm cursor-pointer
hover:bg-gray-50 dark:hover:bg-dark-200
" :class="copyPreviewColor ? 'text-primary' : 'op50'" @click="copyPreviewColor = !copyPreviewColor"
>
<Icon v-if="!copyPreviewColor" class="inline-block text-lg align-middle" icon="carbon:checkbox" />
<Icon v-else class="inline-block text-lg align-middle" icon="carbon:checkbox-checked" />
<span class="inline-block align-middle ml1">copy with color</span>
</button>
</div>
<div class="flex flex-wrap mt-2">
<template v-for="(group, groupName) in SnippetMap" :key="groupName">
<div class="mr-4">
<div class="my-1 op50 text-sm">
{{ groupName }}
</div>
<div class="flex gap-1">
<template v-for="(snippet, type) in group" :key="`${icon}-${groupName}-${type}`">
<SnippetPreview
:collection="collection"
:icon="icon"
:snippet="snippet"
:type="type"
:color="color"
>
<button class="btn small opacity-75" @click="copy(type)">
{{ snippet.name }}<sup v-if="snippet.tag" class="opacity-50 -mr-1">{{ snippet.tag }}</sup>
</button>
</SnippetPreview>
</template>
</div>
</div>
</template>
<div class="mr-4">
<div class="my-1 op50 text-sm">
Download
</div>
<button class="btn small mr-1 mb-1 opacity-75" @click="download('svg')">
SVG
</button>
<button class="btn small mr-1 mb-1 opacity-75" @click="download('png')">
PNG
</button>
<button class="btn small mr-1 mb-1 opacity-75" @click="download('vue')">
Vue
</button>
<button class="btn small mr-1 mb-1 opacity-75" @click="download('jsx')">
React
</button>
<button class="btn small mr-1 mb-1 opacity-75" @click="download('tsx')">
React<sup class="opacity-50 -mr-1">TS</sup>
</button>
<button class="btn small mr-1 mb-1 opacity-75" @click="download('svelte')">
Svelte
</button>
<button class="btn small mr-1 mb-1 opacity-75" @click="download('qwik')">
Qwik
</button>
<button class="btn small mr-1 mb-1 opacity-75" @click="download('solid')">
Solid
</button>
<button class="btn small mr-1 mb-1 opacity-75" @click="download('astro')">
Astro
</button>
<button class="btn small mr-1 mb-1 opacity-75" @click="download('react-native')">
React Native
</button>
</div>
<div class="mr-4">
<div class="my-1 op50 text-sm">
View on
</div>
<a
v-if="collection" class="btn small mr-1 mb-1 opacity-75 inline-flex"
:href="`https://icon-sets.iconify.design/${collection.id}/?icon-filter=${icon.split(':')[1]}`" target="_blank"
>
Iconify
</a>
<a
v-if="collection" class="btn small mr-1 mb-1 opacity-75 inline-flex"
:href="`https://uno.antfu.me/?s=i-${icon.replace(':', '-')}`" target="_blank"
>
UnoCSS
</a>
</div>
</div>
<InstallIconSet v-if="collection" :collection="collection" />
</div>
</div>
</template>
================================================
FILE: src/components/IconSet.vue
================================================
<!-- eslint-disable no-console -->
<script setup lang='ts'>
import { cacheCollection, specialTabs } from '../data'
import { isLocalMode } from '../env'
import { activeMode, bags, drawerCollapsed, getSearchResults, iconSize, isCurrentCollectionLoading, listType, showHelp, toggleBag } from '../store'
import { getIconSnippet } from '../utils/icons'
import { cleanupQuery } from '../utils/query'
const route = useRoute()
const router = useRouter()
const showBag = ref(false)
const copied = ref(false)
const current = computed({
get() {
return (route.query.icon as string) || ''
},
set(value) {
router.replace({ query: cleanupQuery({ ...route.query, icon: value }) })
},
})
const max = ref(isLocalMode ? 500 : 200)
const searchbar = ref<{ input: HTMLElement }>()
const { search, icons, category, collection, variant } = getSearchResults()
const loading = isCurrentCollectionLoading()
const maxMap = new Map<string, number>()
const id = computed(() => collection.value?.id)
const url = computed(() => collection.value?.url || collection.value?.author?.url)
const npm = computed(() => (id.value != null && !specialTabs.includes(id.value)) ? `https://npmx.dev/package/@iconify-json/${id.value}` : '')
const namespace = computed(() => (id.value != null && !specialTabs.includes(id.value)) ? `${id.value}:` : '')
function onCopy(status: boolean) {
copied.value = status
setTimeout(() => {
copied.value = false
}, 2000)
}
function toggleCategory(cat: string) {
if (category.value === cat)
category.value = ''
else
category.value = cat
}
function toggleVariant(v: string) {
if (variant.value === v)
variant.value = ''
else
variant.value = v
}
async function copyText(text?: string) {
if (text) {
try {
await navigator.clipboard.writeText(text)
return true
}
catch {
}
}
return false
}
async function onSelect(icon: string) {
switch (activeMode.value) {
case 'select':
toggleBag(icon)
break
case 'copy':
onCopy(await copyText(await getIconSnippet(
[collection.value!],
icon,
'id',
true,
) || icon))
break
default:
current.value = icon
break
}
}
function loadMore() {
max.value += 100
maxMap.set(namespace.value, max.value)
}
async function loadAll() {
if (!namespace.value)
return
await cacheCollection(collection.value!.id)
max.value = icons.value.length
maxMap.set(namespace.value, max.value)
}
function next(delta = 1) {
const name = current.value.startsWith(namespace.value)
? current.value.slice(namespace.value.length)
: current.value
const index = icons.value.indexOf(name)
if (index === -1)
return
const newOne = icons.value[index + delta]
if (newOne)
current.value = namespace.value + newOne
}
watch(
() => namespace.value,
() => max.value = maxMap.get(namespace.value) || 200,
)
function focusSearch() {
searchbar.value?.input.focus()
}
onMounted(focusSearch)
watch(router.currentRoute, focusSearch, { immediate: true })
router.afterEach(() => {
focusSearch()
})
onKeyStroke('/', (e) => {
e.preventDefault()
focusSearch()
})
onKeyStroke('Escape', () => {
if (current.value !== '') {
current.value = ''
focusSearch()
}
})
const categoriesContainer = ref<HTMLElement | null>(null)
const { x } = useScroll(categoriesContainer)
useEventListener(categoriesContainer, 'wheel', (e: WheelEvent) => {
e.preventDefault()
if (e.deltaX)
x.value += e.deltaX
else
x.value += e.deltaY
}, {
passive: false,
})
</script>
<template>
<WithNavbar>
<div class="flex flex-auto h-full overflow-hidden">
<Drawer
h-full overflow-y-overlay flex-none hidden md:block
:w="drawerCollapsed ? '0px' : '250px'"
transition-all duration-300
/>
<button
fixed top="50%" flex="~ items-end justify-center" w-5 h-8
icon-button transition-all duration-300
border="t r b base rounded-r-full" z-10 max-md:hidden
title="Toggle Sidebar"
:style="{ left: drawerCollapsed ? '0px' : '250px' }"
@click="drawerCollapsed = !drawerCollapsed"
>
<div
i-carbon-chevron-left
icon-button ml--1
transition duration-300 ease-in-out
:class="drawerCollapsed ? 'transform rotate-180' : ''"
/>
</button>
<!-- Loading -->
<div
v-if="collection && loading"
class="h-full w-full flex-auto relative bg-base bg-opacity-75 content-center transition-opacity duration-100"
:class="loading ? '' : 'opacity-0 pointer-events-none'"
>
<div class="absolute text-gray-800 dark:text-dark-500" style="top:50%;left:50%;transform:translate(-50%,-50%)">
Loading...
</div>
</div>
<div v-else-if="collection" h-full w-full relative max-h-full grid="~ rows-[max-content_1fr]" of-hidden>
<div pt-5 flex="~ col gap-2">
<div class="flex px-8">
<!-- Left -->
<div class="flex-auto px-2">
<NavPlaceholder class="md:hidden" />
<div class="text-gray-900 text-xl flex items-center select-none dark:text-gray-200">
<div class="whitespace-no-wrap of-hidden">
{{ collection.name }}
</div>
<!-- Information icons -->
<div ml-1 flex="~ items-center gap-1">
<div v-if="collection.hidden" i-carbon:information-disabled text="orange sm" title="The icon set was deprecated and is no longer available" />
<a
v-if="url"
class="flex items-center text-base opacity-25 hover:opacity-100"
:href="url"
target="_blank"
>
<Icon icon="la:external-link-square-alt-solid" />
</a>
<a
v-if="npm"
class="flex items-center text-base opacity-25 hover:opacity-100"
:href="npm"
target="_blank"
>
<Icon icon="la:npm" />
</a>
</div>
<div class="flex-auto" />
</div>
<div class="text-xs block opacity-50">
{{ collection.author?.name }}
</div>
<div v-if="collection.license">
<a
class="text-xs opacity-50 hover:opacity-100"
:href="collection.license.url"
target="_blank"
>{{ collection.license.title }}</a>
</div>
</div>
<!-- Right -->
<div class="flex flex-col">
<ActionsMenu :collection="collection" />
<div class="flex-auto" />
</div>
</div>
<!-- Categories -->
<div v-if="collection.categories" ref="categoriesContainer" class="mx-8 flex flex-wrap gap-2 select-none">
<div
v-for="c of Object.keys(collection.categories).sort()"
:key="c"
class="
whitespace-nowrap text-sm inline-block px-2 border border-base rounded-full hover:bg-gray-50 cursor-pointer
dark:border-dark-200 dark:hover:bg-dark-200
"
:class="c === category ? 'text-primary border-primary dark:border-primary' : 'opacity-75'"
@click="toggleCategory(c)"
>
{{ c }}
</div>
</div>
<!-- Searching -->
<SearchBar
ref="searchbar"
v-model:search="search"
class="mx-8 hidden md:flex"
/>
<!-- Variants --->
<div v-if="collection.variants" class="mx-8 mb-2 flex flex-wrap gap-2 select-none items-center">
<div text-sm op50>
Variants
</div>
<div
v-for="c of Object.keys(collection.variants).sort()"
:key="c"
class="
whitespace-nowrap text-sm inline-block px-2 border border-base rounded-full hover:bg-gray-50 cursor-pointer
dark:border-dark-200 dark:hover:bg-dark-200
"
:class="c === variant ? 'text-primary border-primary dark:border-primary' : 'opacity-75'"
@click="toggleVariant(c)"
>
{{ c }}
</div>
</div>
</div>
<div of-y-scroll of-x-hidden>
<!-- Icons -->
<div class="px-5 pt-2 pb-4 text-center">
<Icons
:key="namespace"
:icons="icons.slice(0, max)"
:selected="bags"
:class="iconSize"
:display="listType"
:search="search"
:namespace="namespace"
@select="onSelect"
/>
<button v-if="icons.length > max" class="btn mx-1 my-3" @click="loadMore">
Load More
</button>
<button v-if="icons.length > max && namespace" class="btn mx-1 my-3" @click="loadAll">
Load All ({{ icons.length - max }})
</button>
<p class="color-fade text-sm pt-4">
{{ icons.length }} icons
</p>
</div>
<Footer />
</div>
</div>
<template v-if="collection">
<!-- Bag Fab -->
<FAB
v-if="bags.length"
icon="carbon:shopping-bag"
:number="bags.length"
@click="showBag = true"
/>
<!-- Bag -->
<Modal :value="showBag" direction="right" @close="showBag = false">
<Bag
@close="showBag = false"
@select="onSelect"
/>
</Modal>
<!-- Details -->
<Modal :value="!!current" @close="current = ''">
<IconDetail
:icon="current" :show-collection="specialTabs.includes(collection.id)"
@close="current = ''"
@copy="onCopy"
@next="next(1)"
@prev="next(-1)"
/>
</Modal>
<!-- Help -->
<ModalDialog :value="showHelp" @close="showHelp = false">
<HelpPage />
</ModalDialog>
<!-- Mode -->
<div
class="fixed top-0 right-0 pl-4 pr-2 py-1 rounded-l-full bg-primary text-white shadow mt-16 cursor-pointer transition-transform duration-300 ease-in-out"
:style="activeMode !== 'normal' ? {} : { transform: 'translateX(120%)' }"
@click="activeMode = 'normal'"
>
{{ activeMode === 'select' ? 'Multiple select' : 'Name copying mode' }}
<Icon icon="carbon:close" class="inline-block text-xl align-text-bottom" />
</div>
<SearchElectron />
<Notification :value="copied">
<Icon icon="mdi:check" class="inline-block mr-2 font-xl align-middle" />
<span class="align-middle">Copied</span>
</Notification>
</template>
</div>
</WithNavbar>
</template>
================================================
FILE: src/components/Icons.vue
================================================
<script setup lang="ts">
import type { PropType } from 'vue'
import { Tooltip } from 'floating-vue'
import { getSearchHighlightHTML, useThemeColor } from '../hooks'
defineProps({
icons: {
type: Array as PropType<any[]>,
default: () => [],
},
selected: {
type: Array,
default: () => [],
},
size: {
type: String,
default: '2xl',
},
spacing: {
type: String,
default: 'm-2',
},
search: {
type: String,
default: '',
},
display: {
type: String,
default: 'grid',
},
namespace: {
type: String,
default: '',
},
colorClass: {
type: String,
default: 'text-dark-600 dark:text-dark-900',
},
})
defineEmits<{
(event: 'select', id: string): void
}>()
const { style } = useThemeColor()
</script>
<template>
<div class="non-dragging flex flex-wrap select-none justify-center" :class="`text-${size} ${colorClass}`">
<div
v-for="icon of icons " :key="icon" class="non-dragging icons-item relative"
:class="[spacing, selected.includes(namespace + icon) ? 'active' : '']"
@click="$emit('select', namespace + icon)"
>
<div v-if="display === 'list'" class="icon-border flex gap-1">
<Icon
:key="icon" class="non-dragging leading-none" :cache="true"
:icon="namespace + icon"
/>
<span
class="text-sm px-1 m-auto"
v-html="getSearchHighlightHTML(icon, search)"
/>
</div>
<Tooltip v-else placement="bottom">
<Icon
:key="icon" class="non-dragging leading-none icon-border h-1em" :cache="true"
:icon="namespace + icon"
/>
<template #popper>
<div :style="style" class="leading-none border-none z-100 text-primary opacity-75 text-sm">
{{ icon }}
</div>
</template>
</Tooltip>
</div>
</div>
</template>
<style>
.icons-item:hover,
.icons-item.active {
color: var(--theme-color);
}
.icon-border {
position: relative;
}
.icon-border.active::after {
content: '';
position: absolute;
top: -3px;
left: -3px;
right: -3px;
bottom: -3px;
border-radius: 4px;
background: var(--theme-color);
opacity: 0.1;
}
.icon-border:hover::before {
content: '';
position: absolute;
top: -4px;
left: -4px;
right: -4px;
bottom: -4px;
border-radius: 4px;
border: 1px solid var(--theme-color);
opacity: 0.4;
}
</style>
================================================
FILE: src/components/InstallIconSet.vue
================================================
<script setup lang="ts">
import type { PropType } from 'vue'
import type { CollectionMeta } from '../data'
import { ref } from 'vue'
import { selectedPackageManager } from '../store'
const props = defineProps({
collection: {
type: Object as PropType<CollectionMeta>,
required: true,
},
})
const managers = ['pnpm', 'npm', 'yarn', 'bun'] as const
const icons = {
npm: 'i-logos:npm-icon',
pnpm: 'i-logos:pnpm',
yarn: 'i-logos:yarn',
bun: 'i-logos:bun',
}
function selectManager(packageName: string) {
selectedPackageManager.value = packageName
}
const status = ref(false)
async function copyText() {
const text = `${selectedPackageManager.value} ${selectedPackageManager.value !== 'npm' ? 'add' : 'i'} -D @iconify-json/${props.collection.id}`
status.value = true
setTimeout(() => {
status.value = false
}, 2000)
if (text) {
try {
await navigator.clipboard.writeText(text)
return true
}
catch {}
}
return false
}
</script>
<template>
<div lt-md:hidden>
<a
href="https://iconify.design/docs/icons/json.html" target="_blank"
class="block w-fit my-1 text-sm mt6 op50 hover:op100 hover:text-primary"
>
Install Iconify Iconset
</a>
<div class="border-1 border-base rounded w-fit min-w-100 mt1">
<div flex="~ gap-4 items-center" p3 border="b base">
<label
v-for="manager in managers" :key="manager"
flex="~ items-center gap-2"
:class="[manager === selectedPackageManager ? 'op100' : 'op25']"
@change="selectManager(manager)"
>
<input type="radio" name="manager" :value="manager" hidden>
<div :class="icons[manager]" />
<div mt--1>{{ manager }}</div>
</label>
</div>
<div flex="~ gap-2 items-center" p3>
<code flex-auto>
<span style="color:#80A665;">{{ selectedPackageManager }}</span>
<span style="color:#DBD7CAEE;" />
<span style="color:#B8A965;">{{ selectedPackageManager !== 'npm' ? ' add ' : ' i ' }} -D </span>
<span style="color:#DBD7CAEE;" /><span style="color:#DBD7CAEE;" />
<span style="color:#C98A7D;">@iconify-json/{{ props.collection.id }}</span>
</code>
<IconButton icon="carbon:copy" @click="copyText" />
</div>
<Notification :value="status">
<Icon icon="mdi:check" class="inline-block mr-2 font-xl align-middle" />
<span class="align-middle">Copied</span>
</Notification>
</div>
</div>
</template>
================================================
FILE: src/components/Modal.vue
================================================
<script setup lang='ts'>
const props = withDefaults(defineProps<{
value?: boolean
direction?: string
}>(), {
value: false,
direction: 'bottom',
})
const emit = defineEmits(['close'])
const { width, height } = useWindowSize()
const isSmall = computed(() => width.value < 600 || height.value < 450)
const positionClass = computed(() => {
if (isSmall.value)
return 'bottom-0 left-0 right-0 top-0 of-auto'
switch (props.direction) {
case 'bottom':
return 'bottom-0 left-0 right-0 border-t'
case 'top':
return 'top-0 left-0 right-0 border-b'
case 'left':
return 'bottom-0 left-0 top-0 border-r'
case 'right':
return 'bottom-0 top-0 right-0 border-l'
default:
return ''
}
})
const transform = computed(() => {
switch (props.direction) {
case 'bottom':
return 'translateY(100%)'
case 'top':
return 'translateY(-100%)'
case 'left':
return 'translateX(-100%)'
case 'right':
return 'translateX(100%)'
default:
return ''
}
})
</script>
<template>
<div
fixed top-0 bottom-0 left-0 right-0 z-40
:class="value ? '' : 'pointer-events-none'"
>
<div
bg-base bottom-0 left-0 right-0 top-0 absolute transition-opacity duration-500 ease-out
:class="value ? 'opacity-85' : 'opacity-0'"
@click="emit('close')"
/>
<div
bg-base border border-base absolute transition-all duration-200 ease-out
:class="positionClass"
:style="value ? {} : { transform }"
>
<slot />
</div>
</div>
</template>
================================================
FILE: src/components/ModalDialog.vue
================================================
<script setup lang='ts'>
withDefaults(defineProps<{
value?: boolean
direction?: string
}>(), {
value: false,
direction: 'bottom',
})
const emit = defineEmits(['close'])
</script>
<template>
<div
class="fixed top-0 bottom-0 left-0 right-0 z-60"
:class="value ? '' : 'pointer-events-none'"
>
<div
class="
bg-base bottom-0 left-0 right-0 top-0 absolute transition-opacity duration-500 ease-out
"
:class="value ? 'opacity-85' : 'opacity-0'"
@click="emit('close')"
/>
<div
class="
bg-base absolute transition-all duration-200 ease-out shadow rounded-md transform
border border-base left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2
"
:class="value ? 'opacity-100' : 'opacity-0'"
>
<slot />
</div>
</div>
</template>
================================================
FILE: src/components/Navbar.vue
================================================
<script lang="ts">
import { isElectron } from '../env'
import { getSearchResults, isDark } from '../store'
export default defineComponent({
setup() {
const route = useRoute()
return {
...getSearchResults(),
isElectron,
isDark,
showNav: computed(() => !route.path.startsWith('/collection')),
isHomepage: computed(() => route.path === '/'),
}
},
})
</script>
<template>
<NavElectron
v-if="isElectron && !isHomepage"
/>
<nav
class="dragging"
flex="~ gap4 none"
p4 relative bg-base z-10 border="b base" text-xl
:class="showNav ? '' : 'md:hidden'"
>
<!-- In Collections -->
<template v-if="!isHomepage && !isElectron">
<RouterLink
class="non-dragging"
icon-button flex-none
i-carbon:arrow-left
to="/"
/>
</template>
<!-- Homepage Only -->
<template v-if="showNav">
<!-- <RouterLink
class="non-dragging"
i-carbon:search icon-button flex-none
to="/collection/all"
/> -->
<div flex-auto />
<h1
absolute top-0 left-0 right-0 bottom-0 flex items-center justify-center
text-xl font-light tracking-2px pointer-events-none
>
Icônes
</h1>
<a
class="non-dragging"
i-carbon-logo-github icon-button flex-none
href="https://github.com/antfu/icones"
target="_blank"
title="GitHub"
/>
<RouterLink
class="non-dragging"
i-carbon-settings icon-button flex-none
to="/settings"
title="Settings"
/>
<DarkSwitcher flex-none />
</template>
<!-- Searching -->
<SearchBar
v-if="collection"
v-model:search="search"
class="flex w-full"
:style="false"
:icon="false"
/>
</nav>
</template>
================================================
FILE: src/components/Notification.vue
================================================
<script setup lang='ts'>
withDefaults(
defineProps<{ value?: boolean }>(),
{ value: false },
)
</script>
<template>
<div
class="fixed top-0 left-0 right-0 z-50 text-center"
:class="value ? '' : 'pointer-events-none overflow-hidden'"
>
<div
class="
px-3 py-1 rounded inline-block m-3 transition-all duration-300 text-primary
bg-base border border-base
flex flex-inline items-center
"
:style="value ? {} : { transform: 'translateY(-150%)' }"
:class="value ? 'shadow' : 'shadow-none'"
>
<slot />
</div>
</div>
</template>
================================================
FILE: src/components/Progress.vue
================================================
<script setup lang='ts'>
import { inProgress, progressMessage } from '../store'
</script>
<template>
<div
border="~ base"
class="fixed bottom-0 left-0 bg-base color-base rounded-tr shadow px-3 py-1 text-sm flex"
:style="inProgress ? {} : { transform: 'translateY(120%)' }"
>
<Icon icon="carbon:circle-dash" class="rotating m-auto mr-1 text-lg" />
<span class="m-auto px-1 blinking">{{ progressMessage }}</span>
</div>
</template>
<style>
@keyframes rotating {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@keyframes blinking {
from {
opacity: 1;
}
50% {
opacity: 0.3;
}
to {
opacity: 1;
}
}
.rotating {
animation: rotating 2s linear infinite;
}
.blinking {
animation: blinking 2s linear infinite;
}
</style>
================================================
FILE: src/components/SearchBar.vue
================================================
<script setup lang='ts'>
defineProps({
search: {
type: String,
default: undefined,
},
placeholder: {
type: String,
default: 'Search...',
},
style: {
type: Boolean,
default: true,
},
border: {
type: Boolean,
default: true,
},
icon: {
type: [String, Boolean],
default: 'carbon:search',
},
inputClass: {
type: String,
default: '',
},
})
const emit = defineEmits(['update:search', 'onKeydown'])
const input = ref<HTMLInputElement>()
defineExpose({ input })
function onKeydown(event: any) {
emit('onKeydown', event)
}
const update = useDebounceFn((event: any) => {
emit('update:search', event.target.value)
}, 250)
function clear() {
emit('update:search', '')
}
</script>
<template>
<div :class="style ? ['md:flex md:shadow md:rounded outline-none md:py-1 py-3 px-4', { 'border-b border-x border-b md:border-t border-base': border }] : ''">
<Icon v-if="icon" :icon="icon" class="m-auto flex-none opacity-60" />
<form action="/collection/all" class="flex-auto" role="search" method="get" @submit.prevent>
<input
ref="input"
:value="search"
aria-label="Search"
:class="inputClass"
class="text-base outline-none w-full py-1 px-4 m-0 bg-transparent"
name="s"
:placeholder="placeholder"
autofocus
autocomplete="off"
@input="update"
@keydown="onKeydown"
>
</form>
<button class="flex items-center opacity-60 hover:opacity-80">
<Icon v-if="search" icon="carbon:close" class="m-auto text-lg -mr-1" @click="clear" />
</button>
<slot name="actions" />
</div>
</template>
================================================
FILE: src/components/SettingsCollectionsList.vue
================================================
<script setup lang="ts">
import type { CollectionInfo } from '../data'
import { isInstalled } from '../data'
import { isElectron } from '../env'
import { isExcludedCategory, isExcludedCollection, isFavoritedCollection, toggleExcludedCollection, toggleFavoriteCollection } from '../store'
defineProps<{
collections: readonly CollectionInfo[]
}>()
</script>
<template>
<div>
<div
v-for="c, idx of collections" :key="c.id" flex="~ gap-2" py1 px2 items-center
border="~ base" mt--1px break-inside-avoid
:class="idx === 0 ? 'border-t' : ''"
>
<RouterLink
:to="`/collection/${c.id}`"
flex-auto
:class="isExcludedCollection(c) ? 'op25 line-through' : ''"
>
{{ c.name }}
</RouterLink>
<div />
<div
v-if="isInstalled(c.id) && !isElectron"
icon-button class="!op50"
i-carbon-cloud-auditing
title="Cached in browser"
/>
<button
v-if="!isExcludedCollection(c)"
icon-button
:class="isFavoritedCollection(c.id) ? 'i-carbon:star-filled text-yellow' : 'i-carbon:star'"
title="Toggle Favorite"
@click="toggleFavoriteCollection(c.id)"
/>
<button
v-if="!isExcludedCategory(c.category)"
icon-button
:class="isExcludedCollection(c) ? 'i-carbon:view-off text-rose' : 'i-carbon:view'"
title="Toggle Visible"
@click="toggleExcludedCollection(c.id)"
/>
</div>
</div>
</template>
================================================
FILE: src/components/SnippetPreview.vue
================================================
<script lang='ts' setup>
import type { CollectionInfo } from '../data'
import type { Snippet } from '../utils/icons'
import { Menu } from 'floating-vue'
import { collections } from '../data'
import { getIconSnippet } from '../utils/icons'
import { highlight } from '../utils/shiki'
import { prettierCode } from '../utils/svg'
const props = defineProps<{
collection?: CollectionInfo
icon: string
snippet: Snippet
type: string
color: string
}>()
const code = ref<string>('')
async function onShow() {
if (!code.value) {
code.value = await getIconSnippet(
props.collection ? [props.collection] : collections,
props.icon,
props.type,
false,
props.color,
) || ''
}
}
const highlightCode = computedAsync(async () => {
const c = code.value
const formatted = (await prettierCode(c, props.snippet.prettierParser)).trim()
return highlight(formatted, props.snippet.lang)
})
</script>
<template>
<Menu :delay="0" placement="top" distance="10" @show="onShow">
<slot />
<template #popper>
<div color-base px3 py2 border="b base" bg-gray:5>
Snippet Preview
<span op50>Click the button to copy</span>
</div>
<div w-full max-h-50 of-auto>
<div h-fit max-w-70 sm:max-w-100 md:max-w-120 lg:max-w-150 p2 text-sm v-html="highlightCode" />
</div>
</template>
</Menu>
</template>
================================================
FILE: src/components/WithNavbar.vue
================================================
<template>
<div class="flex h-screen flex-col overflow-hidden">
<Navbar />
<div class="flex-auto flex flex-col of-hidden">
<slot />
</div>
</div>
</template>
================================================
FILE: src/components/electron/NavElectron.vue
================================================
<template>
<div
class="electron-nav dragging cursor-pointer flex-none flex justify-start items-center fixed top-0 left-0 z-50 border-base bg-base border-r border-b rounded-br opacity-0 hover:opacity-100 transition-all duration-300"
>
<div class="mac-controls flex-none" />
<IconButton
v-if="$route.path !== '/'" to="/"
class="text-lg px-2 flex-none ma"
icon="carbon:chevron-left"
/>
</div>
</template>
<style>
.electron-nav {
height: 38px;
}
.mac-controls {
width: 70px;
}
.electron-nav .icon-button svg {
margin-top: -2px;
}
</style>
================================================
FILE: src/components/electron/NavPlaceholder.vue
================================================
<script setup lang="ts">
import { isElectron } from '../../env'
</script>
<template>
<div v-if="isElectron" class="flex-none" style="height: 27px;">
<slot />
</div>
</template>
================================================
FILE: src/components/electron/SearchElectron.vue
================================================
<script setup lang="ts">
import { isSearchOpen } from '../../data'
import { isElectron } from '../../env'
import { getSearchResults } from '../../store'
const { search, collection } = getSearchResults()
const input = ref<HTMLInputElement | null>(null)
watch(isSearchOpen, (v) => {
if (input.value) {
if (v) {
input.value.focus()
}
else {
input.value.blur()
search.value = ''
}
}
})
</script>
<template>
<Notification v-if="isElectron" class="text-right md:hidden" :value="isSearchOpen">
<div v-if="collection" class="flex text-base">
<Icon icon="carbon:search" class="m-auto flex-none opacity-60" />
<input
ref="input"
v-model="search"
class="text-base outline-none py-1 px-4 flex-auto m-0 bg-transparent"
placeholder="Search..."
@keydown.esc="isSearchOpen = false"
>
<Icon icon="carbon:close" class="m-auto text-lg -mr-1 opacity-60" @click="isSearchOpen = false" />
</div>
</Notification>
</template>
================================================
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<string, string[]>
variants?: Record<string, string[]>
}
const loadedMeta = ref<CollectionMeta[]>([])
const installed = ref<string[]>([])
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<CollectionInfo[]>(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<CollectionMeta | null> {
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<string, string[]> = {}
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<string, [string, string | RegExp][]> = {
'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<any>(() => ({
'--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<CollectionMeta | null>) {
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<string[]>([])
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 `<span class="${baseClass}">${text}</span>`
const end = start + search.length
return `<span class="${baseClass}">${text.slice(0, start)}<b class="${activeClass}">${text.slice(start, end)}</b>${text.slice(end)}</span>`
}
export function arrayIntersection<T>(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
================================================
<template>
<div>
Not found
</div>
</template>
================================================
FILE: src/pages/collection/[id].vue
================================================
<script setup lang='ts'>
import { pushRecentCollection, setCurrentCollection, useCurrentCollection } from '../../store'
const props = defineProps<{
id: string
}>()
watch(
() => props.id,
() => setCurrentCollection(props.id),
{ immediate: true },
)
onUnmounted(() => setCurrentCollection(''))
const collection = useCurrentCollection()
onMounted(() => {
pushRecentCollection(props.id)
})
</script>
<template>
<WithNavbar v-if="!collection">
<div class="py-8 px-4 text-gray-700 text-center dark:text-dark-700">
Loading...
</div>
</WithNavbar>
<IconSet v-else />
</template>
================================================
FILE: src/pages/index.vue
================================================
<script setup lang='ts'>
import type { PresentType } from '../data'
import { categories, categorySearch, favoritedCollections, filteredCollections, recentCollections } from '../data'
const searchbar = ref<{ input: HTMLElement }>()
const categorized = ref(getIconList(categorySearch.value))
const availableCategories = computed(() => categorized.value.filter(c => c.collections.length > 0))
let categorizeDebounceTimer: NodeJS.Timeout | null = null
watch([categorySearch, favoritedCollections, recentCollections], ([newVal]) => {
if (categorizeDebounceTimer)
clearTimeout(categorizeDebounceTimer)
categorizeDebounceTimer = setTimeout(() => {
categorizeDebounceTimer = null
categorized.value = getIconList(newVal)
}, 500)
})
function getIconList(searchString: string) {
if (searchString) {
return [
{
name: 'Result',
type: 'result' as PresentType,
collections: filteredCollections.value,
},
]
}
else {
return [
{
name: 'Favorites',
type: 'favorite' as PresentType,
collections: favoritedCollections.value,
},
{
name: 'Recent',
type: 'recent' as PresentType,
collections: recentCollections.value,
},
...categories.map(category => ({
name: category,
type: 'normal' as PresentType,
collections: filteredCollections.value.filter(collection => collection.category === category),
})),
]
}
}
const router = useRouter()
onKeyStroke('/', (e) => {
e.preventDefault()
router.replace('/collection/all')
})
onMounted(() => searchbar.value?.input.focus())
const platform = (navigator as any).userAgentData?.platform || navigator.platform || ''
const isMacOS = platform.toUpperCase().includes('MAC')
function onKeydown(e: KeyboardEvent) {
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
router.replace(`/collection/all?s=${categorySearch.value}`)
categorySearch.value = ''
}
}
</script>
<template>
<WithNavbar>
<div of-hidden grid="~ rows-[max-content_1fr]">
<!-- Searching -->
<div md:mx-6 md:mt-6>
<SearchBar
ref="searchbar"
v-model:search="categorySearch"
placeholder="Search category..."
flex
@on-keydown="onKeydown"
/>
<RouterLink
:class="categorySearch ? '' : 'op0 pointer-events-none'"
px4 py2 w-full mt--1px text-sm z--1 h-10
flex="~ gap-2" items-center
border="~ base rounded-b"
hover="text-primary !border-primary shadow"
:to="`/collection/all?s=${categorySearch}`"
>
<div i-carbon-direction-right-01 scale-y--100 op50 />
Search for all icons...
<div>
<kbd text-sm border="~ base rounded" px1>{{ isMacOS ? '⌘' : 'Ctrl' }}</kbd> + <kbd text-sm border="~ base rounded" px1>Enter</kbd>
</div>
</RouterLink>
</div>
<div of-y-auto relative space-y-6>
<!-- Category listing -->
<template v-for="c of availableCategories" :key="c.name">
<div px4>
<div px-2 text-op-50 text-lg sticky top-0 bg-base z-1>
{{ c.name }}
</div>
<CollectionEntries
of-hidden
:collections="c.collections"
:type="c.type"
/>
</div>
</template>
<div
v-if="availableCategories.length === 0"
class="flex flex-col flex-grow w-full py-6 justify-center items-center"
>
<Icon icon="ph:x-circle-bold" class="text-4xl mb-2 opacity-20" />
<span class="text-lg opacity-60">There is no result corresponding to your search query.</span>
</div>
<Footer />
</div>
</div>
</WithNavbar>
</template>
================================================
FILE: src/pages/settings.vue
================================================
<script setup lang="ts">
import type { PresentType } from '../data'
import { categories, collections } from '../data'
import { isExcludedCategory, toggleExcludedCategory } from '../store'
const categorizedCollections = computed(() => categories.map(category => ({
name: category,
type: 'normal' as PresentType,
collections: collections.filter(collection => collection.category === category),
})))
</script>
<template>
<WithNavbar>
<div py-4 of-hidden grid="~ rows-[max-content_1fr]">
<!-- <h1 text-xl>
Features
</h1>
<input id="toggle-dark-mode" v-model="darkMode" type="checkbox"> -->
<div px4>
<h1 text-xl>
Collections
</h1>
<p op50 mb5>
Manage collections to be listed in the home page and search results.
</p>
</div>
<div of-y-auto w-full px4 pb4 class="masonry">
<div v-for="c of categorizedCollections" :key="c.name" mb-10>
<div flex py1 px2 break-inside-avoid>
<h1 font-bold op75 flex-auto>
{{ c.name }}
</h1>
<button
icon-button
:class="isExcludedCategory(c.name) ? 'i-carbon:view-off text-red' : 'i-carbon:view'"
title="Toggle Visible"
@click="toggleExcludedCategory(c.name)"
/>
</div>
<SettingsCollectionsList :collections="c.collections" />
</div>
</div>
</div>
</WithNavbar>
</template>
<style>
.masonry {
list-style: none;
column-gap: 1em;
column-count: 1;
}
@screen sm {
.masonry {
column-count: 2;
}
}
@screen md {
.masonry {
column-count: 3;
}
}
@screen lg {
.masonry {
column-count: 4;
}
}
@screen xl {
.masonry {
column-count: 5;
}
}
</style>
================================================
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<typeof defineComponent>
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<CollectionMeta | null>(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<string[]>('icones-fav-collections', [])
export const recentCollectionIds = useStorage<string[]>('icones-recent-collections', [])
export const recentIconIds = useStorage<string[]>('icones-recent-icons', [])
export const bags = useStorage<string[]>('icones-bags', [])
export const activeMode = useStorage<ActiveMode>('active-mode', 'normal')
export const preferredCase = useStorage<IdCase>('icones-preferfed-case', 'iconify')
export const drawerCollapsed = useStorage<boolean>('icones-drawer-collapsed', false)
export const selectedPackageManager = useStorage<string>('icones-package-manager', 'pnpm')
export const excludedCollectionIds = useStorage<string[]>('icones-excluded-collections', [])
export const excludedCategories = useStorage<string[]>('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<string, string> = {
'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<string, string>(
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<string, Record<string, Snippet>> = {
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<string | undefined> {
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 `<span class="iconify" data-icon="${icon}" data-inline="false"${color === 'currentColor' ? '' : ` style="color: ${color}"`}></span>`
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 `<div class="i-${icon}" />`
case 'unocss-attributify':
return `<div i-${icon} />`
}
}
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 = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
${symbols}
</defs>
</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<void>((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<void>((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<void>((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<void>((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<string, string | undefined | null>) {
for (const key of Object.keys(query)) {
if (!query[key])
delete query[key]
}
return query
}
================================================
FILE: src/utils/sample.ts
================================================
export function sample<T>(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<HighlighterCore>(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<e.length){n=e.charCodeAt(f++);r=e.charCodeAt(f++);i=e.charCodeAt(f++);s=n>>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<e.length){s=this._keyStr.indexOf(e.charAt(f++));o=this._keyStr.indexOf(e.charAt(f++));u=this._keyStr.indexOf(e.charAt(f++));a=this._keyStr.indexOf(e.charAt(f++));n=s<<2|o>>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;n<e.length;n++){var r=e.charCodeAt(n);if(r<128){t+=String.fromCharCode(r)}else if(r>127&&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(n<e.length){r=e.charCodeAt(n);if(r<128){t+=String.fromCharCode(r);n++}else if(r>191&&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(/<svg (.*?)>/, '<svg $1 {...props}>')}
)
}`
if (snippet)
return prettierCode(code, 'babel-ts')
else
return prettierCode(`import React from 'react'\n${code}\nexport default ${name}`, 'babel-ts')
}
export function SvgToTSX(svg: string, name: string, snippet: boolean, reactJSX = true) {
let code = `
export function ${name}(props: SVGProps<SVGSVGElement>) {
return (
${ClearSvg(svg, reactJSX).replace(/<svg (.*?)>/, '<svg $1 {...props}>')}
)
}`
code = snippet ? code : `import React, { SVGProps } from 'react'\n${code}\nexport default ${name}`
return prettierCode(code, 'babel-ts')
}
export function SvgToQwik(svg: string, name: string, snippet: boolean) {
let code = `
export function ${name}(props: QwikIntrinsicElements['svg'], key: string) {
return (
${ClearSvg(svg, false).replace(/<svg (.*?)>/, '<svg $1 {...props} key={key}>')}
)
}`
code = snippet ? code : `import type { QwikIntrinsicElements } from '@builder.io/qwik'\n${code}\nexport default ${name}`
return prettierCode(code, 'babel-ts')
}
export function SvgToVue(svg: string, name: string, isTs?: boolean) {
const content = `
<template>
${ClearSvg(svg)}
</template>
<script>
export default {
name: '${name}'
}
</script>`
const code = isTs ? content.replace('<script>', '<script lang="ts">') : content
return prettierCode(code, 'vue')
}
export function SvgToSolid(svg: string, name: string, snippet: boolean) {
let code = `
export function ${name}(props: JSX.IntrinsicElements['svg']) {
return (
${svg.replace(/<svg (.*?)>/, '<svg $1 {...props}>')}
)
}`
code = snippet ? code : `import type { JSX } from 'solid-js'\n${code}\nexport default ${name}`
return prettierCode(code, 'babel-ts')
}
export function SvgToSvelte(svg: string) {
return `${svg.replace(/<svg (.*?)>/, '<svg $1 {...$$$props}>')}`
}
export function SvgToAstro(svg: string) {
return `
---
const props = Astro.props
---
${svg.replace(/<svg (.*?)>/, '<svg $1 {...props}>')}
`
}
export function SvgToReactNative(svg: string, name: string, snippet: boolean) {
function replaceTags(svg: string, replacements: {
from: string
to: string
}[]): string {
let result = svg
replacements.forEach(({ from, to }) => {
result = result.replace(new RegExp(`<${from}(.*?)>`, 'g'), `<${to}$1>`)
.replace(new RegExp(`</${from}>`, 'g'), `</${to}>`)
})
return result
}
function generateImports(usedComponents: string[]): string {
// Separate Svg from the other components
const svgIndex = usedComponents.indexOf('Svg')
if (svgIndex !== -1)
usedComponents.splice(svgIndex, 1)
// Join all other component names with a comma and wrap them in curly braces
const componentsString = usedComponents.length > 0 ? `{ ${usedComponents.join(', ')} }` : ''
// Return the consolidated import statement, ensuring Svg is imported as a default import
return `import Svg, ${componentsString} from 'react-native-svg';`
}
const replacements: {
from: string
to: string
}[] = [
{ from: 'svg', to: 'Svg' },
{ from: 'path', to: 'Path' },
{ from: 'g', to: 'G' },
{ from: 'circle', to: 'Circle' },
{ from: 'rect', to: 'Rect' },
{ from: 'line', to: 'Line' },
{ from: 'polyline', to: 'Polyline' },
{ from: 'polygon', to: 'Polygon' },
{ from: 'ellipse', to: 'Ellipse' },
{ from: 'text', to: 'Text' },
{ from: 'tspan', to: 'Tspan' },
{ from: 'textPath', to: 'TextPath' },
{ from: 'defs', to: 'Defs' },
{ from: 'use', to: 'Use' },
{ from: 'symbol', to: 'Symbol' },
{ from: 'linearGradient', to: 'LinearGradient' },
{ from: 'radialGradient', to: 'RadialGradient' },
{ from: 'stop', to: 'Stop' },
]
const reactNativeSvgCode = replaceTags(ClearSvg(svg, true), replacements)
.replace(/className=/g, '')
.replace(/href=/g, 'xlinkHref=')
.replace(/clip-path=/g, 'clipPath=')
.replace(/fill-opacity=/g, 'fillOpacity=')
.replace(/stroke-width=/g, 'strokeWidth=')
.replace(/stroke-linecap=/g, 'strokeLinecap=')
.replace(/stroke-linejoin=/g, 'strokeLinejoin=')
.replace(/stroke-miterlimit=/g, 'strokeMiterlimit=')
const svgComponents = replacements.map(({ to }) => to)
const imports = generateImports(svgComponents.filter(component => reactNativeSvgCode.includes(component)))
let code = `
${imports}
export function ${name}(props) {
return (
${reactNativeSvgCode}
)
}`
if (!snippet)
code = `import React from 'react';\n${code}\nexport default ${name}`
return prettierCode(code, 'babel-ts')
}
export async function LoadIconSvgs(
collections: CollectionInfo[],
icons: string[],
) {
return await Promise.all(
icons
.filter(Boolean)
.sort()
.map(async (name) => {
return {
name,
svg: await getSvg(collections, name),
}
}),
)
}
================================================
FILE: src/utils/svg/htmlToJsx.ts
================================================
function transformToReactJSX(jsx: string) {
const reactJSX = jsx
.replace(/(class|(stroke-\w+)|(\w+:\w+))=/g, (i) => {
if (i === 'class=')
return 'className='
return i.split(/[:\-]/)
.map((i, idx) => idx === 0
? i.toLowerCase()
: i[0].toUpperCase() + i.slice(1).toLowerCase())
.join('')
})
// transform HTML-style comment to JSX-style comment
.replaceAll('<!--', '{/*')
.replaceAll('-->', '*/}')
return reactJSX
}
export function HtmlToJSX(html: string, reactJSX = false) {
const jsx = html.replace(/([\w-]+)=/g, (i) => {
const words = i.split('-')
if (words.length === 1 || words[0] === 'stroke')
return i
return words
.map((i, idx) => idx === 0
? i.toLowerCase()
: i[0].toUpperCase() + i.slice(1).toLowerCase())
.join('')
})
return reactJSX ? transformToReactJSX(jsx) : jsx
}
================================================
FILE: src/utils/svg/index.ts
================================================
export { default } from './base64'
export { bufferToString } from './bufferToString'
export type { PackType } from './helpers'
export {
ClearSvg,
LoadIconSvgs,
normalizeZipFleName,
SvgToAstro,
SvgToDataURL,
SvgToJSX,
SvgToQwik,
SvgToReactNative,
SvgToSolid,
SvgToSvelte,
SvgToTSX,
SvgToVue,
toComponentName,
} from './helpers'
export { HtmlToJSX } from './htmlToJsx'
export { API_ENTRY, getLicenseComment, getSvg, getSvgLocal } from './loader'
export { prettierCode } from './prettier'
================================================
FILE: src/utils/svg/loader.ts
================================================
import type { CollectionInfo } from '../../data'
import { buildIcon, loadIcon } from 'iconify-icon'
export const API_ENTRY = 'https://api.iconify.design'
export async function getLicenseComment(collections: CollectionInfo[], icon: string) {
const [id] = icon.split(':')
const collection = collections.find(i => i.id === id)
if (!collection) {
return ''
}
return `<!-- Icon from ${collection?.name} by ${collection?.author?.name} - ${collection?.license?.url} -->`
}
export async function getSvgLocal(
collections: CollectionInfo[],
icon: string,
size = '1em',
color = 'currentColor',
) {
const data = await loadIcon(icon)
if (!data)
return
const built = buildIcon(data, { height: size })
if (!built)
return
const license = await getLicenseComment(collections, icon)
const xlink = built.body.includes('xlink:') ? ' xmlns:xlink="http://www.w3.org/1999/xlink"' : ''
return `<svg xmlns="http://www.w3.org/2000/svg"${xlink} ${Object.entries(built.attributes).map(([k, v]) => `${k}="${v}"`).join(' ')}>${license}${built.body}</svg>`.replaceAll('currentColor', color)
}
export async function getSvg(
collections: CollectionInfo[],
icon: string,
size = '1em',
color = 'currentColor',
) {
const local = await getSvgLocal(collections, icon, size, color)
if (local)
return local
const mode = import.meta.env.DEV && !PWA ? 'no-cors' : undefined
return await fetch(`${API_ENTRY}/${icon}.svg?inline=false&height=${size}&color=${encodeURIComponent(color)}`, {
mode,
}).then(r => r.text()) || ''
}
================================================
FILE: src/utils/svg/prettier.ts
================================================
import type { BuiltInParserName } from 'prettier'
import { isElectron } from '../../env'
export async function prettierCode(code: string, parser: BuiltInParserName) {
if (!isElectron)
return code
try {
const format = await import('prettier').then(r => r.format)
return format(code, {
parser,
semi: false,
singleQuote: true,
})
}
catch {
return code
}
}
================================================
FILE: src/utils/svgToPng.ts
================================================
export async function svgToPngDataUrl(svg: string) {
const scaleFactor = 16
const canvas = document.createElement('canvas')
const imgPreview = document.createElement('img')
imgPreview.setAttribute('style', 'position: absolute; top: -9999px')
document.body.appendChild(imgPreview)
const canvasCtx = canvas.getContext('2d')!
const svgBlob: Blob = new Blob([svg], { type: 'image/svg+xml;charset=utf-8' })
const svgDataUrl = URL.createObjectURL(svgBlob)
return new Promise<string>((resolve) => {
imgPreview.onload = async () => {
const img = new Image()
const dimensions: { width: number, height: number } = await getDimensions(imgPreview.src)
Object.assign(canvas, {
width: dimensions.width * scaleFactor,
height: dimensions.height * scaleFactor,
})
img.crossOrigin = 'anonymous'
img.src = imgPreview.src
img.onload = () => {
canvasCtx.drawImage(img, 0, 0, canvas.width, canvas.height)
const imgData = canvas.toDataURL('image/png')
resolve(imgData)
}
function getDimensions(
src: string,
): Promise<{ width: number, height: number }> {
return new Promise((resolve) => {
const _img = new Image()
_img.src = src
_img.onload = () => {
resolve({
width: _img.naturalWidth,
height: _img.naturalHeight,
})
}
})
}
}
imgPreview.src = svgDataUrl
})
.finally(() => {
document.body.removeChild(imgPreview)
})
}
================================================
FILE: src/utils/worker/index.ts
================================================
/// <reference lib="webworker" />
import type { CollectionInfo } from '../../data'
import type { PackType } from '../svg'
import type { PackOperation, WorkerPackMessage } from './types'
import { downloadZip } from 'client-zip'
import { getSvg, LoadIconSvgs, normalizeZipFleName, SvgToAstro, SvgToJSX, SvgToQwik, SvgToReactNative, SvgToSolid, SvgToSvelte, SvgToTSX, SvgToVue, toComponentName } from '../svg'
globalThis.onmessage = async (event: MessageEvent<WorkerPackMessage<PackOperation>>) => {
const message = event.data
let blob: Blob | undefined
let name: string | undefined
try {
const collections: CollectionInfo[] = JSON.parse(
new TextDecoder().decode(message.collections),
)
if (isPackZipMessage(message)) {
blob = await downloadZip(
PreparePackZip(
collections,
message.payload.icons,
message.payload.name,
message.payload.type,
),
).blob()
}
else if (isPackJsonZipMessage(message)) {
blob = await downloadZip(
PrepareIconSvgs(
collections,
message.payload.icons,
'json',
message.payload.name,
),
).blob()
}
else if (isPackSvgZipMessage(message)) {
blob = await downloadZip(
PrepareIconSvgs(
collections,
message.payload.icons,
'svg',
),
).blob()
}
else if (isPackFontZipMessage(message)) {
const result = await PackIconFont(
collections,
message.payload.icons,
message.payload.options,
)
if (result) {
([blob, name] = result)
}
}
}
catch (e: any) {
console.error('PackWorker: error while generating the zip', e)
globalThis.postMessage({ error: e && 'message' in e ? e.message : String(e) })
return
}
if (blob) {
try {
const arrayBuffer = await blob.arrayBuffer()
globalThis.postMessage({
blob: arrayBuffer,
name,
}, [arrayBuffer])
}
catch (e: any) {
console.error('PackWorker: error while transferring generated zip', e)
globalThis.postMessage({ error: e && 'message' in e ? e.message : String(e) })
}
}
else {
globalThis.postMessage({ error: 'No blob generated' })
}
}
export function isPackZipMessage(
message: WorkerPackMessage<PackOperation>,
): message is WorkerPackMessage<'pack-zip'> {
return message.operation === 'pack-zip'
}
export function isPackJsonZipMessage(
message: WorkerPackMessage<PackOperation>,
): message is WorkerPackMessage<'pack-json-zip'> {
return message.operation === 'pack-json-zip'
}
export function isPackSvgZipMessage(
message: WorkerPackMessage<PackOperation>,
): message is WorkerPackMessage<'pack-svg-zip'> {
return message.operation === 'pack-svg-zip'
}
export function isPackFontZipMessage(
message: WorkerPackMessage<PackOperation>,
): message is WorkerPackMessage<'pack-font-zip'> {
return message.operation === 'pack-font-zip'
}
async function* PrepareIconSvgs(
collections: CollectionInfo[],
icons: string[],
format: 'svg' | 'json',
name?: string,
) {
if (format === 'json') {
const svgs = await LoadIconSvgs(collections, icons)
yield {
name: `${name}.json`,
input: new Blob([JSON.stringify(svgs, null, 2)], { type: 'application/json; charset=utf-8' }),
}
return
}
for (const icon of icons) {
if (!icon)
continue
const svg = await getSvg(collections, icon)
yield {
name: `${normalizeZipFleName(icon)}.svg`,
input: new Blob([svg], { type: 'image/svg+xml' }),
}
}
}
async function* PreparePackZip(
collections: CollectionInfo[],
icons: string[],
name: string,
type: PackType,
) {
if (type === 'json' || type === 'svg') {
yield* PrepareIconSvgs(collections, icons, type, name)
return
}
const ext = (type === 'solid' || type === 'qwik' || type === 'react-native') ? 'tsx' : type
for (const name of icons) {
if (!name)
continue
const svg = await getSvg(collections, name)
const componentName = toComponentName(normalizeZipFleName(name))
let content: string
switch (type) {
case 'vue':
content = await SvgToVue(svg, componentName)
break
case 'jsx':
content = await SvgToJSX(svg, componentName, false)
break
case 'svelte':
content = SvgToSvelte(svg)
break
case 'astro':
content = SvgToAstro(svg)
break
case 'qwik':
content = await SvgToQwik(svg, componentName, false)
break
case 'react-native':
content = await SvgToReactNative(svg, componentName, false)
break
case 'solid':
content = await SvgToSolid(svg, componentName, false)
break
case 'tsx':
content = await SvgToTSX(svg, componentName, false)
break
default:
continue
}
yield {
name: `${componentName}.${ext}`,
input: new Blob([content], { type: 'text/plain' }),
}
}
}
async function PackIconFont(
collections: CollectionInfo[],
icons: string[],
options: any = {},
) {
if (!icons.length)
return
const [data, { SvgPacker }] = await Promise.all([
LoadIconSvgs(collections, icons),
import('svg-packer'),
])
const result = await SvgPacker({
fontName: 'Iconify Explorer Font',
fileName: 'iconfont',
cssPrefix: 'i',
...options,
icons: data,
})
return [result.zip.blob, result.zip.name] as const
}
================================================
FILE: src/utils/worker/types.ts
================================================
import type { PackType } from '../svg'
export type PackOperation = 'pack-zip' | 'pack-json-zip' | 'pack-svg-zip' | 'pack-font-zip'
export interface PackZipPayload {
icons: string[]
name: string
type: PackType
}
export interface PackJsonZipPayload {
icons: string[]
name: string
}
export interface PackSvgZipPayload {
icons: string[]
}
export interface PackFontZipPayload {
icons: string[]
options: any
}
export interface WorkerPackMessage<O extends PackOperation> {
payload: O extends 'pack-zip' ? PackZipPayload
: O extends 'pack-json-zip' ? PackJsonZipPayload
: O extends 'pack-svg-zip' ? PackSvgZipPayload
: O extends 'pack-font-zip' ? PackFontZipPayload
: never
operation: O
collections: ArrayBuffer
}
export interface WorkerPackResponse {
blob: ArrayBuffer
name?: string
}
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"target": "esnext",
"jsx": "preserve",
"lib": ["DOM", "ESNext", "WebWorker"],
"module": "esnext",
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"types": [
"vite/client",
"vite-plugin-pages/client",
"vite-plugin-pwa/client"
],
"strict": true,
"noUnusedLocals": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true
},
"exclude": [
"dist",
"node_modules"
]
}
================================================
FILE: unocss.config.ts
================================================
import { defineConfig, presetAttributify, presetIcons, presetWind3, transformerDirectives, transformerVariantGroup } from 'unocss'
export default defineConfig({
shortcuts: {
'border-base': 'border-hex-888/25',
'border-dark-only': 'border-transparent dark:border-dark-100',
'bg-base': 'bg-white dark:bg-[#181818]',
'color-base': 'text-gray-900 dark:text-gray-300',
'color-fade': 'text-gray-900:50 dark:text-gray-300:50',
'icon-button': 'op50 hover:op100 my-auto shrink-0',
},
presets: [
presetWind3(),
presetIcons(),
presetAttributify(),
],
transformers: [
transformerVariantGroup(),
transformerDirectives(),
],
theme: {
colors: {
primary: 'var(--theme-color)',
dark: {
100: '#222',
200: '#333',
300: '#444',
400: '#555',
500: '#666',
600: '#777',
700: '#888',
800: '#999',
900: '#aaa',
},
},
},
})
================================================
FILE: vite.config.ts
================================================
import { rmSync } from 'node:fs'
import { join, resolve } from 'node:path'
import process from 'node:process'
import Vue from '@vitejs/plugin-vue'
import dayjs from 'dayjs'
import fg from 'fast-glob'
import { SvgPackerVitePlugin } from 'svg-packer/vite'
import UnoCSS from 'unocss/vite'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { defineConfig } from 'vite'
import electron from 'vite-plugin-electron'
import renderer from 'vite-plugin-electron-renderer'
// @ts-expect-error type resolution
import esmodule from 'vite-plugin-esmodule'
import Pages from 'vite-plugin-pages'
import { VitePWA } from 'vite-plugin-pwa'
export default defineConfig(({ mode }) => {
const isElectron = mode === 'electron'
const isBuild = process.argv.slice(2).includes('build')
if (isElectron)
rmSync('dist-electron', { recursive: true, force: true })
return {
plugins: [
isElectron && electron([
{
entry: 'src/main/index.ts',
vite: {
build: {
minify: isBuild,
outDir: 'dist-electron/main',
},
},
},
]),
isElectron && renderer(),
isElectron && esmodule(['prettier']),
Vue({
customElement: [
'iconify-icon',
],
template: {
compilerOptions: {
isCustomElement: tag => tag === 'iconify-icon',
},
},
}),
Pages({
importMode: 'sync',
}),
Components({
dts: 'src/components.d.ts',
}),
AutoImport({
imports: [
'vue',
'vue-router',
'@vueuse/core',
],
dts: 'src/auto-imports.d.ts',
}),
SvgPackerVitePlugin(),
!isElectron && VitePWA({
strategies: 'injectManifest',
srcDir: 'src',
filename: 'sw.ts',
registerType: 'autoUpdate',
manifest: {
name: 'Icônes',
short_name: 'Icônes',
icons: [
{
src: '/android-chrome-192x192.png',
sizes: '192x192',
type: 'image/png',
},
{
src: '/android-chrome-512x512.png',
sizes: '512x512',
type: 'image/png',
},
],
},
injectManifest: {
// collections-meta.json ~7.5MB
maximumFileSizeToCacheInBytes: 10 * 1024 * 1024,
},
integration: {
configureOptions(viteConfig, options) {
if (viteConfig.command === 'build')
options.includeAssets = fg.sync('**/*.*', { cwd: join(process.cwd(), 'public'), onlyFiles: true })
},
},
devOptions: {
enabled: process.env.SW_DEV === 'true',
/* when using generateSW the PWA plugin will switch to classic */
type: 'module',
navigateFallback: 'index.html',
},
}),
UnoCSS(),
],
define: {
__BUILD_TIME__: JSON.stringify(dayjs().format('YYYY/MM/DD HH:mm')),
PWA: !isElectron && (process.env.NODE_ENV === 'production' || process.env.SW_DEV === 'true'),
},
resolve: {
alias: {
'iconify-icon': resolve(__dirname, 'node_modules/iconify-icon/dist/iconify-icon.mjs'),
},
},
worker: {
format: 'es',
rollupOptions: {
treeshake: true,
},
plugins: () => [
SvgPackerVitePlugin(),
],
},
}
})
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
SYMBOL INDEX (118 symbols across 28 files)
FILE: electron/src/main/index.ts
constant PROJECT_ROOT (line 9) | const PROJECT_ROOT = path.resolve(__dirname, '../..')
function createMainWindow (line 11) | async function createMainWindow() {
FILE: scripts/prepare.ts
function ObjectPick (line 8) | function ObjectPick(source: Record<string, any>, keys: string[]) {
function humanFileSize (line 15) | function humanFileSize(size: number) {
function prepareJSON (line 21) | async function prepareJSON() {
FILE: src/components.d.ts
type GlobalComponents (line 13) | interface GlobalComponents {
FILE: src/data/index.ts
type PresentType (line 20) | type PresentType = 'favorite' | 'recent' | 'normal'
type CollectionInfo (line 22) | interface CollectionInfo {
type CollectionMeta (line 39) | interface CollectionMeta extends CollectionInfo {
function isInstalled (line 92) | function isInstalled(id: string) {
function isMetaLoaded (line 95) | function isMetaLoaded(id: string) {
function preInstall (line 100) | function preInstall() {
function tryInstallFromLocal (line 107) | async function tryInstallFromLocal(id: string) {
function downloadAndInstall (line 129) | async function downloadAndInstall(id: string) {
function cacheCollection (line 147) | async function cacheCollection(id: string) {
function getCollectionMeta (line 155) | async function getCollectionMeta(id: string): Promise<CollectionMeta | n...
function getVariantCategories (line 174) | function getVariantCategories(collection: CollectionMeta) {
function getFullMeta (line 191) | async function getFullMeta() {
FILE: src/hooks/color.ts
function useThemeColor (line 3) | function useThemeColor() {
FILE: src/hooks/search.ts
function useSearch (line 9) | function useSearch(collection: Ref<CollectionMeta | null>) {
function getSearchHighlightHTML (line 143) | function getSearchHighlightHTML(
function arrayIntersection (line 158) | function arrayIntersection<T>(a: T[], b: T[]) {
FILE: src/html.d.ts
type HTMLAttributes (line 4) | interface HTMLAttributes {
type AllowedComponentProps (line 9) | interface AllowedComponentProps {
FILE: src/shims.d.ts
type Window (line 1) | interface Window {
FILE: src/store/collection.ts
function useCurrentCollection (line 24) | function useCurrentCollection() {
function isCurrentCollectionLoading (line 28) | function isCurrentCollectionLoading() {
function setCurrentCollection (line 44) | async function setCurrentCollection(id: string) {
FILE: src/store/indexedDB.ts
function loadCollection (line 12) | async function loadCollection(id: string) {
function saveCollection (line 16) | async function saveCollection(id: string, data: any) {
FILE: src/store/localstorage.ts
constant RECENT_COLLECTION_CAPACITY (line 5) | const RECENT_COLLECTION_CAPACITY = 10
constant RECENT_ICONS_CAPACITY (line 6) | const RECENT_ICONS_CAPACITY = 100
type ActiveMode (line 8) | type ActiveMode = 'normal' | 'select' | 'copy'
function getTransformedId (line 30) | function getTransformedId(icon: string) {
function isFavoritedCollection (line 34) | function isFavoritedCollection(id: string) {
function isExcludedCollection (line 38) | function isExcludedCollection(collection: CollectionInfo) {
function isExcludedCategory (line 42) | function isExcludedCategory(category: string | undefined) {
function isRecentCollection (line 46) | function isRecentCollection(id: string) {
function pushRecentCollection (line 50) | function pushRecentCollection(id: string) {
function removeRecentCollection (line 54) | function removeRecentCollection(id: string) {
function isRecentIcon (line 58) | function isRecentIcon(id: string) {
function pushRecentIcon (line 62) | function pushRecentIcon(id: string) {
function removeRecentIcon (line 66) | function removeRecentIcon(id: string) {
function toggleFavoriteCollection (line 70) | function toggleFavoriteCollection(id: string) {
function toggleExcludedCollection (line 78) | function toggleExcludedCollection(id: string) {
function toggleExcludedCategory (line 86) | function toggleExcludedCategory(category: string) {
function addToBag (line 94) | function addToBag(id: string) {
function removeFromBag (line 99) | function removeFromBag(id: string) {
function inBag (line 105) | function inBag(id: string) {
function toggleBag (line 109) | function toggleBag(id: string) {
function clearBag (line 117) | function clearBag() {
FILE: src/sw.ts
function buildCollectionResponseHeaders (line 23) | function buildCollectionResponseHeaders(cachedResponse: Response) {
function getCollection (line 71) | async function getCollection(request: Request, name: string, icons: stri...
FILE: src/utils/case.ts
method bare (line 2) | bare(id: string) {
method barePascal (line 5) | barePascal(id: string) {
method iconify (line 8) | iconify(id: string) {
method dash (line 11) | dash(id: string) {
method slash (line 14) | slash(id: string) {
method doubleHyphen (line 17) | doubleHyphen(id: string) {
method camel (line 20) | camel(id: string) {
method pascal (line 23) | pascal(id: string) {
method component (line 26) | component(id: string) {
method componentKebab (line 29) | componentKebab(id: string) {
method unocssColon (line 32) | unocssColon(id: string) {
method unocss (line 35) | unocss(id: string) {
method iconifyTailwind (line 38) | iconifyTailwind(id: string) {
type IdCase (line 43) | type IdCase = keyof typeof idCases
FILE: src/utils/dataUrlToBlob.ts
function dataUrlToBlob (line 1) | function dataUrlToBlob(dataurl: string) {
FILE: src/utils/icons.ts
type Snippet (line 24) | interface Snippet {
function Download (line 32) | async function Download(blob: Blob, name: string) {
function getIconSnippet (line 80) | async function getIconSnippet(
function getIconDownloadLink (line 140) | function getIconDownloadLink(icon: string) {
FILE: src/utils/pack.ts
function getSvgSymbol (line 6) | async function getSvgSymbol(
function PackSVGSprite (line 26) | async function PackSVGSprite(
function PackIconFont (line 49) | async function PackIconFont(
function PackSvgZip (line 84) | async function PackSvgZip(
function PackJsonZip (line 118) | async function PackJsonZip(
function PackZip (line 153) | async function PackZip(
function createArrayBufferFromCollections (line 190) | function createArrayBufferFromCollections(
FILE: src/utils/query.ts
function cleanupQuery (line 1) | function cleanupQuery(query: Record<string, string | undefined | null>) {
FILE: src/utils/sample.ts
function sample (line 1) | function sample<T>(arr: T[], num: number) {
FILE: src/utils/shiki.ts
function highlight (line 26) | function highlight(code: string, lang: string) {
FILE: src/utils/svg/bufferToString.ts
function bufferToString (line 1) | function bufferToString(buffer: ArrayBuffer) {
FILE: src/utils/svg/helpers.ts
type PackType (line 10) | type PackType = 'svg' | 'tsx' | 'jsx' | 'vue' | 'solid' | 'qwik' | 'svel...
function normalizeZipFleName (line 12) | function normalizeZipFleName(svgName: string): string {
function toComponentName (line 16) | function toComponentName(icon: string) {
function ClearSvg (line 20) | function ClearSvg(svgCode: string, reactJSX?: boolean) {
function SvgToDataURL (line 44) | function SvgToDataURL(svg: string) {
function SvgToJSX (line 51) | function SvgToJSX(svg: string, name: string, snippet: boolean) {
function SvgToTSX (line 64) | function SvgToTSX(svg: string, name: string, snippet: boolean, reactJSX ...
function SvgToQwik (line 76) | function SvgToQwik(svg: string, name: string, snippet: boolean) {
function SvgToVue (line 88) | function SvgToVue(svg: string, name: string, isTs?: boolean) {
function SvgToSolid (line 103) | function SvgToSolid(svg: string, name: string, snippet: boolean) {
function SvgToSvelte (line 115) | function SvgToSvelte(svg: string) {
function SvgToAstro (line 119) | function SvgToAstro(svg: string) {
function SvgToReactNative (line 129) | function SvgToReactNative(svg: string, name: string, snippet: boolean) {
function LoadIconSvgs (line 207) | async function LoadIconSvgs(
FILE: src/utils/svg/htmlToJsx.ts
function transformToReactJSX (line 1) | function transformToReactJSX(jsx: string) {
function HtmlToJSX (line 18) | function HtmlToJSX(html: string, reactJSX = false) {
FILE: src/utils/svg/loader.ts
constant API_ENTRY (line 4) | const API_ENTRY = 'https://api.iconify.design'
function getLicenseComment (line 6) | async function getLicenseComment(collections: CollectionInfo[], icon: st...
function getSvgLocal (line 15) | async function getSvgLocal(
function getSvg (line 32) | async function getSvg(
FILE: src/utils/svg/prettier.ts
function prettierCode (line 4) | async function prettierCode(code: string, parser: BuiltInParserName) {
FILE: src/utils/svgToPng.ts
function svgToPngDataUrl (line 1) | async function svgToPngDataUrl(svg: string) {
FILE: src/utils/worker/index.ts
function isPackZipMessage (line 81) | function isPackZipMessage(
function isPackJsonZipMessage (line 87) | function isPackJsonZipMessage(
function isPackSvgZipMessage (line 93) | function isPackSvgZipMessage(
function isPackFontZipMessage (line 99) | function isPackFontZipMessage(
function PackIconFont (line 191) | async function PackIconFont(
FILE: src/utils/worker/types.ts
type PackOperation (line 3) | type PackOperation = 'pack-zip' | 'pack-json-zip' | 'pack-svg-zip' | 'pa...
type PackZipPayload (line 5) | interface PackZipPayload {
type PackJsonZipPayload (line 10) | interface PackJsonZipPayload {
type PackSvgZipPayload (line 14) | interface PackSvgZipPayload {
type PackFontZipPayload (line 18) | interface PackFontZipPayload {
type WorkerPackMessage (line 23) | interface WorkerPackMessage<O extends PackOperation> {
type WorkerPackResponse (line 33) | interface WorkerPackResponse {
FILE: vite.config.ts
method configureOptions (line 92) | configureOptions(viteConfig, options) {
Condensed preview — 98 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (189K chars).
[
{
"path": ".github/renovate.json",
"chars": 852,
"preview": "{\n \"extends\": [\n \"config:recommended\"\n ],\n \"rangeStrategy\": \"bump\",\n \"packageRules\": [\n {\n \"description\":"
},
{
"path": ".github/workflows/ci.yml",
"chars": 1406,
"preview": "name: CI\n\non:\n push:\n branches:\n - main\n\n pull_request:\n branches:\n - main\n\njobs:\n lint:\n runs-on:"
},
{
"path": ".gitignore",
"chars": 185,
"preview": "node_modules\nyarn-error.log\ndist\n.idea\n\nsrc/assets/collections.json\n.DS_Store\n\npublic/collections\npublic/lib\nrelease\ncol"
},
{
"path": ".npmrc",
"chars": 75,
"preview": "shamefully-hoist=true\nignore-workspace-root-check=true\nshell-emulator=true\n"
},
{
"path": ".vscode/settings.json",
"chars": 1129,
"preview": "{\n \"cSpell.words\": [\n \"icones\"\n ],\n\n // Enable the ESlint flat config support\n \"eslint.experimental.useFlatConfig"
},
{
"path": "LICENSE",
"chars": 1067,
"preview": "MIT License\n\nCopyright (c) 2020 Anthony Fu\n\nPermission is hereby granted, free of charge, to any person obtaining a copy"
},
{
"path": "README.md",
"chars": 1530,
"preview": "<h1 align=\"center\">\nIcônes\n</h1>\n\n<p align=\"center\">Icon Explorer with <b>Instant</b> searching, powered by <a href=\"htt"
},
{
"path": "electron/electron-builder.json5",
"chars": 741,
"preview": "/**\n * @see https://www.electron.build/configuration/configuration\n */\n{\n \"productName\": \"Icônes\",\n \"appId\": \"me.antfu"
},
{
"path": "electron/eslint.config.js",
"chars": 192,
"preview": "// @ts-check\nimport antfu from '@antfu/eslint-config'\n\nexport default antfu(\n {\n ignores: [\n // eslint ignore g"
},
{
"path": "electron/package.json",
"chars": 956,
"preview": "{\n \"name\": \"icones-electron\",\n \"version\": \"0.0.0\",\n \"appname\": \"Icônes\",\n \"description\": \"Explorer for Iconify with "
},
{
"path": "electron/src/main/index.ts",
"chars": 1689,
"preview": "import path from 'node:path'\nimport { app, BrowserWindow, shell } from 'electron'\nimport installExtension, { VUEJS_DEVTO"
},
{
"path": "electron/src/renderer/index.js",
"chars": 0,
"preview": ""
},
{
"path": "eslint.config.js",
"chars": 340,
"preview": "// @ts-check\nimport antfu from '@antfu/eslint-config'\n\nexport default antfu(\n {\n ignores: [\n '**/src/assets/col"
},
{
"path": "index.html",
"chars": 563,
"preview": "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-w"
},
{
"path": "netlify.toml",
"chars": 152,
"preview": "[build]\npublish = \"dist\"\ncommand = \"pnpm run build\"\n\n[build.environment]\nNODE_VERSION = \"20\"\n\n[[redirects]]\nfrom = \"/*\"\n"
},
{
"path": "package.json",
"chars": 1922,
"preview": "{\n \"name\": \"icones\",\n \"type\": \"module\",\n \"version\": \"0.0.0\",\n \"private\": true,\n \"packageManager\": \"pnpm@10.24.0\",\n "
},
{
"path": "pnpm-workspace.yaml",
"chars": 61,
"preview": "packages:\n - electron\nneverBuiltDependencies:\n - ttf2woff2\n"
},
{
"path": "public/search.xml",
"chars": 621,
"preview": "<OpenSearchDescription xmlns=\"http://a9.com/-/spec/opensearch/1.1/\" xmlns:moz=\"http://www.mozilla.org/2006/browser/searc"
},
{
"path": "scripts/prepare.ts",
"chars": 2537,
"preview": "import path from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport fs from 'fs-extra'\n\nconst __dirname = path."
},
{
"path": "src/App.vue",
"chars": 322,
"preview": "<script setup lang='ts'>\nimport { useThemeColor } from './hooks'\n\nconst { style } = useThemeColor()\n</script>\n\n<template"
},
{
"path": "src/auto-imports.d.ts",
"chars": 19470,
"preview": "/* eslint-disable */\n/* prettier-ignore */\n// @ts-nocheck\n// noinspection JSUnusedGlobalSymbols\n// Generated by unplugin"
},
{
"path": "src/components/ActionsMenu.vue",
"chars": 6577,
"preview": "<script setup lang='ts'>\nimport type { PropType } from 'vue'\nimport type { CollectionMeta } from '../data'\nimport { cach"
},
{
"path": "src/components/Bag.vue",
"chars": 3983,
"preview": "<script setup lang='ts'>\nimport type { PackType } from '../utils/svg'\nimport { collections } from '../data'\nimport { bag"
},
{
"path": "src/components/CollectionEntries.vue",
"chars": 495,
"preview": "<script setup lang=\"ts\">\nimport type { CollectionInfo, PresentType } from '../data'\n\ndefineProps<{\n collections: Collec"
},
{
"path": "src/components/CollectionEntry.vue",
"chars": 2595,
"preview": "<script setup lang=\"ts\">\nimport type { CollectionInfo, PresentType } from '../data'\nimport { isFavoritedCollection, remo"
},
{
"path": "src/components/ColorPicker.vue",
"chars": 459,
"preview": "<script setup lang=\"ts\">\ndefineProps({\n value: {\n type: String,\n required: true,\n },\n})\n\nconst emit = defineEmit"
},
{
"path": "src/components/CustomSelect.vue",
"chars": 1362,
"preview": "<script setup lang='ts'>\ndefineProps<{\n options: {\n label: string\n children: {\n label: string\n value: s"
},
{
"path": "src/components/DarkSwitcher.vue",
"chars": 1321,
"preview": "<script setup lang=\"ts\">\nimport { isDark } from '../store'\n\nconst isAppearanceTransition = typeof document !== 'undefine"
},
{
"path": "src/components/Drawer.vue",
"chars": 2760,
"preview": "<script setup lang='ts'>\nimport { categorySearch, filteredCollections, sortedCollectionsInfo, specialTabs } from '../dat"
},
{
"path": "src/components/FAB.vue",
"chars": 739,
"preview": "<script setup lang='ts'>\ndefineProps({\n icon: {\n type: String,\n required: true,\n },\n number: {\n type: Number"
},
{
"path": "src/components/Footer.vue",
"chars": 642,
"preview": "<script setup lang=\"ts\">\nconst buildTime = __BUILD_TIME__\n\nconst timeAgo = useTimeAgo(new Date(buildTime))\n</script>\n\n<t"
},
{
"path": "src/components/HelpPage.vue",
"chars": 1792,
"preview": "<template>\n <div class=\"p-6 w-120 help-page\">\n <p class=\"mb-2 opacity-75\">\n How to use the icon?\n </p>\n\n "
},
{
"path": "src/components/Icon.vue",
"chars": 1699,
"preview": "<script lang=\"ts\">\nimport { loadIcon } from 'iconify-icon'\nimport { LRUCache } from 'lru-cache'\n\nconst cache = new LRUCa"
},
{
"path": "src/components/IconButton.vue",
"chars": 758,
"preview": "<script setup lang='ts'>\ndefineProps({\n icon: {\n type: String,\n required: true,\n },\n active: {\n type: Boolea"
},
{
"path": "src/components/IconDetail.vue",
"chars": 9921,
"preview": "<script setup lang='ts'>\nimport { collections } from '../data'\nimport { activeMode, copyPreviewColor, getTransformedId, "
},
{
"path": "src/components/IconSet.vue",
"chars": 11143,
"preview": "<!-- eslint-disable no-console -->\n<script setup lang='ts'>\nimport { cacheCollection, specialTabs } from '../data'\nimpor"
},
{
"path": "src/components/Icons.vue",
"chars": 2417,
"preview": "<script setup lang=\"ts\">\nimport type { PropType } from 'vue'\nimport { Tooltip } from 'floating-vue'\nimport { getSearchHi"
},
{
"path": "src/components/InstallIconSet.vue",
"chars": 2539,
"preview": "<script setup lang=\"ts\">\nimport type { PropType } from 'vue'\nimport type { CollectionMeta } from '../data'\nimport { ref "
},
{
"path": "src/components/Modal.vue",
"chars": 1568,
"preview": "<script setup lang='ts'>\nconst props = withDefaults(defineProps<{\n value?: boolean\n direction?: string\n}>(), {\n value"
},
{
"path": "src/components/ModalDialog.vue",
"chars": 828,
"preview": "<script setup lang='ts'>\nwithDefaults(defineProps<{\n value?: boolean\n direction?: string\n}>(), {\n value: false,\n dir"
},
{
"path": "src/components/Navbar.vue",
"chars": 1832,
"preview": "<script lang=\"ts\">\nimport { isElectron } from '../env'\nimport { getSearchResults, isDark } from '../store'\n\nexport defau"
},
{
"path": "src/components/Notification.vue",
"chars": 604,
"preview": "<script setup lang='ts'>\nwithDefaults(\n defineProps<{ value?: boolean }>(),\n { value: false },\n)\n</script>\n\n<template>"
},
{
"path": "src/components/Progress.vue",
"chars": 807,
"preview": "<script setup lang='ts'>\nimport { inProgress, progressMessage } from '../store'\n</script>\n\n<template>\n <div\n border="
},
{
"path": "src/components/SearchBar.vue",
"chars": 1675,
"preview": "<script setup lang='ts'>\ndefineProps({\n search: {\n type: String,\n default: undefined,\n },\n placeholder: {\n t"
},
{
"path": "src/components/SettingsCollectionsList.vue",
"chars": 1496,
"preview": "<script setup lang=\"ts\">\nimport type { CollectionInfo } from '../data'\nimport { isInstalled } from '../data'\nimport { is"
},
{
"path": "src/components/SnippetPreview.vue",
"chars": 1384,
"preview": "<script lang='ts' setup>\nimport type { CollectionInfo } from '../data'\nimport type { Snippet } from '../utils/icons'\nimp"
},
{
"path": "src/components/WithNavbar.vue",
"chars": 180,
"preview": "<template>\n <div class=\"flex h-screen flex-col overflow-hidden\">\n <Navbar />\n <div class=\"flex-auto flex flex-col"
},
{
"path": "src/components/electron/NavElectron.vue",
"chars": 584,
"preview": "<template>\n <div\n class=\"electron-nav dragging cursor-pointer flex-none flex justify-start items-center fixed top-0 "
},
{
"path": "src/components/electron/NavPlaceholder.vue",
"chars": 186,
"preview": "<script setup lang=\"ts\">\nimport { isElectron } from '../../env'\n</script>\n\n<template>\n <div v-if=\"isElectron\" class=\"fl"
},
{
"path": "src/components/electron/SearchElectron.vue",
"chars": 1019,
"preview": "<script setup lang=\"ts\">\nimport { isSearchOpen } from '../../data'\nimport { isElectron } from '../../env'\nimport { getSe"
},
{
"path": "src/components.d.ts",
"chars": 2522,
"preview": "/* eslint-disable */\n// @ts-nocheck\n// biome-ignore lint: disable\n// oxlint-disable\n// ------\n// Generated by unplugin-v"
},
{
"path": "src/data/index.ts",
"chars": 5398,
"preview": "import type { IconifyJSON } from 'iconify-icon'\nimport { notNullish } from '@antfu/utils'\nimport { AsyncFzf } from 'fzf'"
},
{
"path": "src/data/search-alias.ts",
"chars": 1817,
"preview": "export const searchAlias: string[][] = [\n ['account', 'person', 'profile', 'user'],\n ['add', 'create', 'new', 'plus'],"
},
{
"path": "src/data/variant-category.ts",
"chars": 3616,
"preview": "// @keep-sorted\nexport const variantCategories: Record<string, [string, string | RegExp][]> = {\n 'academicons': [\n ["
},
{
"path": "src/env.ts",
"chars": 359,
"preview": "export const isElectron = import.meta.env.MODE === 'electron'\nexport const isVSCode = location.protocol === 'vscode-webv"
},
{
"path": "src/hooks/color.ts",
"chars": 186,
"preview": "import { themeColor } from '../store'\n\nexport function useThemeColor() {\n const style = computed<any>(() => ({\n '--t"
},
{
"path": "src/hooks/index.ts",
"chars": 49,
"preview": "export * from './color'\nexport * from './search'\n"
},
{
"path": "src/hooks/search.ts",
"chars": 4364,
"preview": "import type { Ref } from 'vue'\nimport type { CollectionMeta } from '../data'\nimport { asyncExtendedMatch, AsyncFzf } fro"
},
{
"path": "src/html.d.ts",
"chars": 339,
"preview": "// for UnoCSS attributify mode compact in Volar\n// refer: https://github.com/johnsoncodehk/volar/issues/1077#issuecommen"
},
{
"path": "src/main.css",
"chars": 2595,
"preview": "body {\n padding: 0;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\nhtml.dark {\n backg"
},
{
"path": "src/main.ts",
"chars": 716,
"preview": "import { createApp } from 'vue'\nimport { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'\nimport"
},
{
"path": "src/pages/[...all].vue",
"chars": 54,
"preview": "<template>\n <div>\n Not found\n </div>\n</template>\n"
},
{
"path": "src/pages/collection/[id].vue",
"chars": 607,
"preview": "<script setup lang='ts'>\nimport { pushRecentCollection, setCurrentCollection, useCurrentCollection } from '../../store'\n"
},
{
"path": "src/pages/index.vue",
"chars": 3824,
"preview": "<script setup lang='ts'>\nimport type { PresentType } from '../data'\nimport { categories, categorySearch, favoritedCollec"
},
{
"path": "src/pages/settings.vue",
"chars": 1790,
"preview": "<script setup lang=\"ts\">\nimport type { PresentType } from '../data'\nimport { categories, collections } from '../data'\nim"
},
{
"path": "src/shims.d.ts",
"chars": 325,
"preview": "interface Window {\n // for vscode\n baseURI?: string\n staticURI?: string\n}\n\ndeclare const vscode: any\ndeclare const __"
},
{
"path": "src/store/collection.ts",
"chars": 2113,
"preview": "import type { CollectionMeta } from '../data'\nimport {\n collections,\n downloadAndInstall,\n getCollectionMeta,\n getFu"
},
{
"path": "src/store/dark.ts",
"chars": 66,
"preview": "export const isDark = useDark({\n storageKey: 'icones-schema',\n})\n"
},
{
"path": "src/store/dialog.ts",
"chars": 76,
"preview": "export const showHelp = ref(false)\nexport const showCaseSelect = ref(false)\n"
},
{
"path": "src/store/index.ts",
"chars": 161,
"preview": "export * from './collection'\nexport * from './dark'\nexport * from './dialog'\nexport * from './localstorage'\nexport * fro"
},
{
"path": "src/store/indexedDB.ts",
"chars": 437,
"preview": "import type { Table } from 'dexie'\nimport Dexie from 'dexie'\n\nconst db = new Dexie('icones')\n\ndb.version(1).stores({\n c"
},
{
"path": "src/store/localstorage.ts",
"chars": 4027,
"preview": "import type { CollectionInfo } from '../data'\nimport type { IdCase } from '../utils/case'\nimport { idCases } from '../ut"
},
{
"path": "src/store/packing.ts",
"chars": 74,
"preview": "export const isPacking = ref(false)\nexport const packingProgress = ref(0)\n"
},
{
"path": "src/store/progress.ts",
"chars": 107,
"preview": "export const inProgress = ref(false)\nexport const progress = ref(0)\nexport const progressMessage = ref('')\n"
},
{
"path": "src/sw.ts",
"chars": 3187,
"preview": "import { getIcons } from '@iconify/utils'\nimport { cacheNames, clientsClaim } from 'workbox-core'\nimport { cleanupOutdat"
},
{
"path": "src/utils/case.ts",
"chars": 1062,
"preview": "export const idCases = {\n bare(id: string) {\n return id.replace(/^.*:/, '')\n },\n barePascal(id: string) {\n retu"
},
{
"path": "src/utils/dataUrlToBlob.ts",
"chars": 325,
"preview": "export function dataUrlToBlob(dataurl: string) {\n const parts = dataurl.split(',')\n const type = parts[0].split(':')[1"
},
{
"path": "src/utils/electron.ts",
"chars": 242,
"preview": "import hotkeys from 'hotkeys-js'\nimport { isSearchOpen } from '../data'\nimport { isElectron } from '../env'\n\nif (isElect"
},
{
"path": "src/utils/icons.ts",
"chars": 5230,
"preview": "import type { BuiltInParserName as PrettierParser } from 'prettier'\nimport type { CollectionInfo } from '../data'\nimport"
},
{
"path": "src/utils/pack-worker-client.ts",
"chars": 122,
"preview": "import PackerWorker from './worker?worker'\n\nexport const packerWorker = new PackerWorker({\n name: 'IconesPackWorker',\n}"
},
{
"path": "src/utils/pack.ts",
"chars": 4991,
"preview": "import type { CollectionInfo } from '../data'\nimport type { PackType } from './svg'\nimport { Download } from './icons'\ni"
},
{
"path": "src/utils/query.ts",
"chars": 189,
"preview": "export function cleanupQuery(query: Record<string, string | undefined | null>) {\n for (const key of Object.keys(query))"
},
{
"path": "src/utils/sample.ts",
"chars": 141,
"preview": "export function sample<T>(arr: T[], num: number) {\n return Array.from({ length: num }, () => arr[Math.floor(arr.length "
},
{
"path": "src/utils/shiki.ts",
"chars": 1060,
"preview": "import type { HighlighterCore } from 'shiki/core'\nimport { createHighlighterCore } from 'shiki/core'\nimport { createJava"
},
{
"path": "src/utils/svg/base64.ts",
"chars": 1707,
"preview": "/* eslint-disable eslint-comments/no-unlimited-disable */\n/* eslint-disable */\n// @ts-expect-error ignore\nconst Base64={"
},
{
"path": "src/utils/svg/bufferToString.ts",
"chars": 129,
"preview": "export function bufferToString(buffer: ArrayBuffer) {\n return String.fromCharCode.apply(null, new Uint16Array(buffer) a"
},
{
"path": "src/utils/svg/helpers.ts",
"chars": 6732,
"preview": "import type { Node } from 'ultrahtml'\nimport type { CollectionInfo } from '../../data'\nimport { encodeSvgForCss } from '"
},
{
"path": "src/utils/svg/htmlToJsx.ts",
"chars": 911,
"preview": "function transformToReactJSX(jsx: string) {\n const reactJSX = jsx\n .replace(/(class|(stroke-\\w+)|(\\w+:\\w+))=/g, (i) "
},
{
"path": "src/utils/svg/index.ts",
"chars": 512,
"preview": "export { default } from './base64'\nexport { bufferToString } from './bufferToString'\nexport type { PackType } from './he"
},
{
"path": "src/utils/svg/loader.ts",
"chars": 1557,
"preview": "import type { CollectionInfo } from '../../data'\nimport { buildIcon, loadIcon } from 'iconify-icon'\n\nexport const API_EN"
},
{
"path": "src/utils/svg/prettier.ts",
"chars": 402,
"preview": "import type { BuiltInParserName } from 'prettier'\nimport { isElectron } from '../../env'\n\nexport async function prettier"
},
{
"path": "src/utils/svgToPng.ts",
"chars": 1570,
"preview": "export async function svgToPngDataUrl(svg: string) {\n const scaleFactor = 16\n\n const canvas = document.createElement('"
},
{
"path": "src/utils/worker/index.ts",
"chars": 5519,
"preview": "/// <reference lib=\"webworker\" />\n\nimport type { CollectionInfo } from '../../data'\nimport type { PackType } from '../sv"
},
{
"path": "src/utils/worker/types.ts",
"chars": 837,
"preview": "import type { PackType } from '../svg'\n\nexport type PackOperation = 'pack-zip' | 'pack-json-zip' | 'pack-svg-zip' | 'pac"
},
{
"path": "tsconfig.json",
"chars": 518,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"esnext\",\n \"jsx\": \"preserve\",\n \"lib\": [\"DOM\", \"ESNext\", \"WebWorker\"],\n \""
},
{
"path": "unocss.config.ts",
"chars": 957,
"preview": "import { defineConfig, presetAttributify, presetIcons, presetWind3, transformerDirectives, transformerVariantGroup } fro"
},
{
"path": "vite.config.ts",
"chars": 3506,
"preview": "import { rmSync } from 'node:fs'\nimport { join, resolve } from 'node:path'\nimport process from 'node:process'\nimport Vue"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the antfu-collective/icones GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 98 files (172.1 KB), approximately 48.4k tokens, and a symbol index with 118 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.