Full Code of antfu-collective/icones for AI

main 0bc591826236 cached
98 files
172.1 KB
48.4k tokens
118 symbols
1 requests
Download .txt
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>

![](./screenshots/1.png)
![](./screenshots/2.png)
![](./screenshots/3.png)
![](./screenshots/4.png)
![](./screenshots/5.png)

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

![](./screenshots/6.png)

<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(),
      ],
    },
  }
})
Download .txt
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
Download .txt
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.

Copied to clipboard!