Compare commits

..

No commits in common. "main" and "v6.10.0" have entirely different histories.

8691 changed files with 221648 additions and 21215 deletions

14
.eslintrc.cjs Normal file
View file

@ -0,0 +1,14 @@
module.exports = {
env: {
es2021: true,
node: true,
},
extends: ['eslint:recommended', 'plugin:prettier/recommended'],
parserOptions: {
ecmaVersion: 13,
sourceType: 'module',
},
rules: {
'prettier/prettier': ['error'],
},
};

View file

@ -13,21 +13,21 @@ Before reporting an issue, please search to see if someone has filed a similar i
## Prerequisites
- Version:
- Are you running from source/main:
- Are you using a released build:
- Operating system:
* Version:
* Are you running from source/main:
* Are you using a released build:
* Operating system:
## Step to reproduce
_(Type here)_
*(Type here)*
### Actual behavior
## Any message or error
_(Type here)_
*(Type here)*
## Additional info or screenshots
- Screenshots
* Screenshots

View file

@ -13,6 +13,6 @@ Before creating an icon request, please search to see if someone has requested t
## Icon Request
- Icon name:
- Use case:
- Screenshots of similar icons:
* Icon name:
* Use case:
* Screenshots of similar icons:

View file

@ -1,36 +0,0 @@
name: Setup
description: Setup the environment for the project
inputs:
node-registry:
description: Node.js package registry to set up for auth
required: false
runs:
using: composite
steps:
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 22
registry-url: ${{ inputs.node-registry }}
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Get pnpm store directory
id: pnpm-cache-dir
shell: bash
run: echo "dir=$(pnpm store path)" >> $GITHUB_OUTPUT
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache-dir.outputs.dir }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
shell: bash
run: pnpm install

View file

@ -13,15 +13,37 @@ jobs:
if: github.repository_owner == 'iconoir-icons'
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Setup
uses: ./.github/actions/setup
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 16
- name: Build CSS
run: pnpm run build css
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 7
- name: Get pnpm store directory
id: pnpm-cache
run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- name: Setup pnpm cache
uses: actions/cache@v3
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install
- name: Build
run: pnpm run build
- name: Commit changes
uses: stefanzweifel/git-auto-commit-action@v6
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: Update build artifacts

View file

@ -1,28 +0,0 @@
name: CI
on:
pull_request:
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v46
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup
uses: ./.github/actions/setup
- name: Run Linter
run: pnpm exec eslint ${{ steps.changed-files.outputs.all_changed_files }}

View file

@ -9,20 +9,38 @@ jobs:
release:
name: Release
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v3
with:
# We have to checkout main or PNPM fails. Tag should be on main anyway.
ref: main
- name: Setup
uses: ./.github/actions/setup
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-registry: https://registry.npmjs.org
node-version: 16
registry-url: 'https://registry.npmjs.org'
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 7
- name: Get pnpm store directory
id: pnpm-cache
run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- name: Setup pnpm cache
uses: actions/cache@v3
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install
- name: Generate changelog file
uses: rhysd/changelog-from-release/action@v3
@ -41,7 +59,7 @@ jobs:
TAG_NAME: ${{ github.ref_name }}
- name: Commit release
uses: stefanzweifel/git-auto-commit-action@v6
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: Release Version ${{ github.ref_name }}
branch: main
@ -55,10 +73,21 @@ jobs:
run: pnpm -r publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_CONFIG_PROVENANCE: true
continue-on-error: true
- name: Publish Flutter
uses: k-paxian/dart-package-publisher@v1.6
uses: k-paxian/dart-package-publisher@v1.5.1
with:
credentialJson: ${{ secrets.PUB_CREDENTIAL_JSON }}
relativePath: ./packages/iconoir-flutter
- name: Trigger Website Workflow
uses: actions/github-script@v6
with:
script: |
github.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'website.yaml',
ref: 'main',
})

View file

@ -1,59 +1,79 @@
name: Website
on:
workflow_dispatch:
workflow_run:
workflows:
- Release
types:
- completed
permissions:
actions: read
contents: read
pages: write
id-token: write
concurrency:
group: ${{ github.workflow }}
group: pages
cancel-in-progress: true
on:
workflow_dispatch: {}
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Setup
uses: ./.github/actions/setup
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 16
- name: Build
run: pnpm run build react
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 7
- name: Get pnpm store directory
id: pnpm-cache
run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- name: Setup pnpm cache
uses: actions/cache@v3
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install
- name: Build packages
run: pnpm run dist
- name: Build website
run: pnpm run build
run: ./node_modules/.bin/next build
working-directory: iconoir.com
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Export website
run: ./node_modules/.bin/next export
working-directory: iconoir.com
- name: Setup GitHub Pages
uses: actions/configure-pages@v5
uses: actions/configure-pages@v2
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
uses: actions/upload-pages-artifact@v1
with:
path: ./iconoir.com/out
path: './iconoir.com/out'
deploy:
name: Deploy
needs: build
runs-on: ubuntu-latest
needs: build
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
uses: actions/deploy-pages@v1

8
.gitignore vendored
View file

@ -1,10 +1,2 @@
.DS_Store
node_modules/
dist/
packages/iconoir-flutter/lib/
packages/iconoir-vue/src/*
!packages/iconoir-vue/src/IconoirProvider.vue
!packages/iconoir-vue/src/providerKey.ts

View file

@ -1 +0,0 @@
22

1
.npmrc Normal file
View file

@ -0,0 +1 @@
strict-peer-dependencies=false

3
.prettierrc.json Normal file
View file

@ -0,0 +1,3 @@
{
"singleQuote": true
}

View file

@ -1,3 +0,0 @@
{
"recommendations": ["dbaeumer.vscode-eslint"]
}

51
.vscode/settings.json vendored
View file

@ -1,51 +0,0 @@
{
// Disable the default formatter, use eslint instead
"prettier.enable": false,
"editor.formatOnSave": false,
// Auto fix
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
},
// Silent the stylistic rules in you IDE, but still auto fix them
"eslint.rules.customizations": [
{ "rule": "style/*", "severity": "off", "fixable": true },
{ "rule": "format/*", "severity": "off", "fixable": true },
{ "rule": "*-indent", "severity": "off", "fixable": true },
{ "rule": "*-spacing", "severity": "off", "fixable": true },
{ "rule": "*-spaces", "severity": "off", "fixable": true },
{ "rule": "*-order", "severity": "off", "fixable": true },
{ "rule": "*-dangle", "severity": "off", "fixable": true },
{ "rule": "*-newline", "severity": "off", "fixable": true },
{ "rule": "*quotes", "severity": "off", "fixable": true },
{ "rule": "*semi", "severity": "off", "fixable": true }
],
// Enable eslint for all supported languages
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"html",
"markdown",
"json",
"json5",
"jsonc",
"yaml",
"toml",
"xml",
"gql",
"graphql",
"astro",
"svelte",
"css",
"less",
"scss",
"pcss",
"postcss"
]
}

View file

@ -17,23 +17,23 @@ diverse, inclusive, and healthy community.
Examples of behavior that contributes to a positive environment for our
community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
- Focusing on what is best not just for us as individuals, but for the
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
@ -118,11 +118,11 @@ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].
Community Impact Guidelines were inspired by
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available
at [https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org

View file

@ -1,74 +0,0 @@
# Naming Convention
## Hierarchy
The foundation of the naming convention in Iconoir is:
```
[Object]-[Modifier]-[Container]
```
### [Object]
Contains one or more objects.
### [Modifier]
It's often an additional and unique element added as a secondary object (`-minus`, `-plus`, `-warning`).
### [Container]
A shape used as a container for the object (`-square`, `-circle`).
If a shape is directly part of an object, it's not considered a container.
\* You could notice a difference between icons such as
`user-minus` and `minus-square`. The second one looks different
because in this case the minus symbol is an _[Object]_, followed
by the _[Container]_.
## Styles
An optional rule is regarding icons style. Iconoir is actually offering Regular and Solid icons. With the latter, icons names end with `-solid`. An example here:
- Regular: `check-circle`
- Solid: `check-circle-solid`
## Object-Oriented Naming
Exceptions apart, icons follow an object-oriented naming and
should not embed actions in their names.
Examples:
- `user-minus` is correct.
- `remove-user` would be wrong.
## Physical Actions
Icons that represent a physical action or movement
can embed that action in their name.
Examples:
- `walking`, `running`, `vehicle-fast`, `crane-lifting` are correct.
## Most-Used Modifiers and Shapes
### Modifiers
`-plus`, `-minus`, `-warning`, `-check`, `-xmark`, `-tag`,
`-ban`, `-slash`
- `-plus-in` and `minus-in`: Differently from `-plus` and `-minus`, these are used when the icon
has a bigger plus or minus icon in the center or inside the main object.
- `-no-access`: It's regularly used when the icon has a restrict
symbol in a corner.
### Shapes
`-square`, `-circle`
## Exceptions
If you spot an icon that is not following any of the rules,
please open a [new issue](https://github.com/iconoir-icons/iconoir/issues/new/choose) on GitHub.

View file

@ -1,70 +1,77 @@
# Iconoir
[![Version](https://img.shields.io/github/v/release/iconoir-icons/iconoir?style=flat-square)](https://github.com/iconoir-icons/iconoir/releases)
[![Project Stars](https://img.shields.io/github/stars/iconoir-icons/iconoir?style=flat-square)](https://github.com/iconoir-icons/iconoir)
[![License](https://img.shields.io/github/license/iconoir-icons/iconoir?style=flat-square)](https://github.com/iconoir-icons/iconoir/blob/main/LICENSE)
[![Discord](https://img.shields.io/discord/998909400234348615?color=5865f2&label=discord&style=flat-square)](https://discord.gg/txXcKCAmKW)
<div>
<a href="https://github.com/lucaburgio/iconoir/releases">
<img src="https://img.shields.io/github/v/release/lucaburgio/iconoir?style=flat-square" alt="Version" />
</a>
<a href="https://github.com/lucaburgio/iconoir">
<img src="https://img.shields.io/github/stars/lucaburgio/iconoir?style=flat-square" alt="Project Stars" />
</a>
<a href="https://www.npmjs.com/package/iconoir-react">
<img src="https://img.shields.io/npm/dm/iconoir-react?color=98E8F3&label=react&style=flat-square" alt="React Library" />
</a>
<a href="https://github.com/lucaburgio/iconoir/blob/main/LICENSE">
<img src="https://img.shields.io/github/license/lucaburgio/iconoir?style=flat-square" alt="License" />
</a>
<a href="https://discord.gg/txXcKCAmKW">
<img src="https://img.shields.io/discord/998909400234348615?color=5865f2&label=Discord&style=flat-square" alt="Discord" />
</a>
</div>
## What is Iconoir?
Iconoir is an open-source library with 1600+ unique SVG icons, designed on a 24x24 pixels grid.
Iconoir is an open-source library with 1300+ unique SVG icons, designed on a 24x24 pixels grid. No premium icons, no email sign-up, no newsletters.
<a href="https://iconoir.com"><strong>Browse at iconoir.com &rarr;</strong></a>
## Basic Usage
You can download any icon of the pack directly from https://iconoir.com or get them from this repository.
The icons are also available via the [`iconoir`](https://www.npmjs.com/package/iconoir) NPM package:
| npm | Yarn | pnpm | Bun |
| --------------- | ------------------ | ------------------ | ----------------- |
| `npm i iconoir` | `yarn add iconoir` | `pnpm add iconoir` | `bun add iconoir` |
Additionally, the icons are available via the `iconoir` NPM package:
```bash
yarn add iconoir
# or
npm i iconoir
```
Example usage:
```js
import Iconoir from 'iconoir/icons/iconoir.svg';
import Iconoir from 'iconoir/icons/iconoir.svg'
```
## React
A React library is available under the name `iconoir-react`.
For more details, see the package [README](./packages/iconoir-react).
A React library is available to install under the name `iconoir-react`. For more details, see the package [README](./packages/iconoir-react).
## React Native
A React Native library is available under the name `iconoir-react-native`.
For more details, see the package [README](./packages/iconoir-react-native).
A React Native library is available to install under the name `iconoir-react-native`. For more details, see the package [README](./packages/iconoir-react-native).
## Vue
A Vue library is available under the name `@iconoir/vue`.
For more details, see the package [README](./packages/iconoir-vue).
A Vue library is available to install under the name `@iconoir/vue`. For more details, see the package [README](./packages/iconoir-vue).
## Flutter
A Flutter library is available under the name `iconoir_flutter`.
For more details, see the package [README](./packages/iconoir-flutter).
A Flutter library is available to install under the name `iconoir_flutter`. For more details, see the package [README](./packages/iconoir-flutter).
## Framer
Iconoir is happily part of [Framer](https://framer.com).
To start using the icons: On the top menu, `Insert` > `Graphics` > `Iconoir`.
Iconoir is happily part of [Framer](https://framer.com) now. To start using the icons: On the top menu, `Insert` > `Graphics` > `Iconoir`.
You can switch between icons from the right sidebar in the editor.
## CSS
Import the CSS file:
Import the CSS File:
```html
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/iconoir-icons/iconoir@main/css/iconoir.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/iconoir-icons/iconoir@main/css/iconoir.css">
```
Here is an example in HTML:
@ -72,7 +79,6 @@ Here is an example in HTML:
```html
<i class="iconoir-hand-brake"></i>
```
The class must always be "iconoir-" and then the name of the icon. You can find the names of the icons [here](https://iconoir.com).
The icons are `display: inline-block` and default to the current font size. You can control this
@ -86,10 +92,10 @@ The library is available in the Figma community [here](https://www.figma.com/com
To add `Iconoir-swift` to your Xcode project, follow these steps:
1. In Xcode, open your project and navigate to _File_ > _Swift Packages_ > _Add Package Dependency..._
1. In Xcode, open your project and navigate to *File* > *Swift Packages* > *Add Package Dependency...*
2. Enter the repository URL: `https://github.com/iconoir-icons/iconoir-swift.git`
3. Choose the branch or version you want to add, and click _Next_.
4. Select the target where you want to use the package, then click _Finish_.
3. Choose the branch or version you want to add, and click *Next*.
4. Select the target where you want to use the package, then click *Finish*.
### UIKit
@ -117,4 +123,4 @@ struct ContentView: View {
## License
MIT License.
MIT License

BIN
assets/cover.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

683
bin/build.js Normal file
View file

@ -0,0 +1,683 @@
import execa from 'execa';
import { promises as fs, readFileSync, existsSync } from 'fs';
import { generateTemplateFilesBatch } from 'generate-template-files';
import { Listr } from 'listr2';
import os from 'os';
import path, { basename, dirname } from 'path';
import { fileURLToPath } from 'url';
import { incompatibleNames, flutterIncompatibleNames } from '../constants.js';
import { buildVueIcons } from './buildVue.js';
// Paths
const __dirname = dirname(fileURLToPath(import.meta.url));
const rootDir = path.join(__dirname, '..');
const iconoirIconsDir = path.join(rootDir, 'icons');
const ignoreCleanFilenames = ['IconoirContext.tsx', 'server'];
// Targets for building icons
const targets = {
'meta-data': { path: 'meta-data.json' },
css: { path: 'css/iconoir.css' },
'iconoir-flutter': { flutter: true, path: 'packages/iconoir-flutter' },
'iconoir-react': { react: true, path: 'packages/iconoir-react' },
'iconoir-react-native': {
react: true,
path: 'packages/iconoir-react-native',
},
'iconoir-vue': {
vue: true,
path: 'packages/iconoir-vue',
},
};
// Get targets from command line arguments
// (build all targets if no arguments)
const args = process.argv.slice(2);
const cliTargets = [];
args.forEach((target) => {
if (target in targets) {
cliTargets.push(target);
} else {
console.error(`Target '${target}' doesn't exist!\n\nPossible targets are:`);
for (const [targetName] of Object.entries(targets)) {
console.log(`- ${targetName}`);
}
process.exit(1);
}
});
// Build tasks
const tasks = new Listr(
[
{
title: 'Fetching icons',
task: async (ctx) => {
ctx.iconoirIconsFiles = await fs.readdir(iconoirIconsDir);
},
},
{
title: 'Building targets',
skip: (ctx) => !ctx.iconoirIconsFiles,
task: (_, task) =>
task.newListr(
[
{
title: 'Building meta-data file',
enabled: () =>
cliTargets.length === 0 || cliTargets.includes('meta-data'),
task: async (ctx) => {
await fs.writeFile(
path.join(rootDir, targets['meta-data'].path),
JSON.stringify({ icons: ctx.iconoirIconsFiles })
);
},
},
{
title: 'Building CSS file',
enabled: () =>
cliTargets.length === 0 || cliTargets.includes('css'),
task: async (ctx) => {
const content = [
(
await fs.readFile(
path.join(__dirname, 'header.css'),
'utf8'
)
).replace('[YEAR]', new Date().getFullYear()),
];
ctx.iconoirIconsFiles.forEach((file) => {
const fileContents = readFileSync(
path.join(__dirname, '../icons/', file)
)
.toString()
.replace(/\n/g, '')
.replace(/(width|height)="[0-9]+px"/g, '')
.replace(/[ ]+/g, ' ');
content.push(
`.iconoir-${
path.parse(file).name
}::before{mask-image:url('data:image/svg+xml;charset=utf-8,${fileContents}');-webkit-mask-image:url('data:image/svg+xml;charset=utf-8,${fileContents}');}`
);
});
await fs.writeFile(
path.join(rootDir, targets.css.path),
content
);
},
},
{
title: 'Building React libraries',
enabled: () =>
cliTargets.length === 0 ||
cliTargets.filter((cliTarget) => targets[cliTarget]?.react)
.length > 0,
task: (_, task) =>
task.newListr(
[
{
title: 'Creating temporary directory',
task: async (ctx) => {
try {
ctx.tmpDir = await fs.mkdtemp(
path.join(os.tmpdir(), 'iconoir-')
);
} catch (err) {
ctx.skip = true;
throw new Error(err.message);
}
},
},
{
title:
'Copying icon files to temporary directory, while renaming icons with incompatible names',
skip: (ctx) => ctx.skip,
task: async (ctx) => {
try {
const promises = ctx.iconoirIconsFiles.map((file) => {
const srcFilePath = path.join(
iconoirIconsDir,
file
);
const iconName = file.split('.')[0];
const dstFileName =
iconName in incompatibleNames
? incompatibleNames[iconName]
: iconName;
const dstFilePath = path.join(
ctx.tmpDir,
`${dstFileName}.svg`
);
return fs.copyFile(srcFilePath, dstFilePath);
});
return Promise.all(promises).catch((err) => {
ctx.skip = true;
throw new Error(err.message);
});
} catch (err) {
ctx.skip = true;
throw new Error(err.message);
}
},
},
{
skip: (ctx) => ctx.skip,
task: (_, task) => {
const targetsToBuild =
cliTargets.length > 0
? cliTargets.filter(
(cliTarget) => targets[cliTarget]?.react
)
: Object.keys(targets).filter(
(target) => targets[target].react
);
const tasks = targetsToBuild.map((target) => {
const builtIconsDir = path.join(
rootDir,
targets[target].path,
'src'
);
return {
title: `Building ${target}`,
task: (_, task) =>
task.newListr(
[
{
title: 'Cleaning target directory',
task: async (ctx) => {
try {
const files = await fs.readdir(
builtIconsDir
);
const serverFiles = existsSync(
path.join(builtIconsDir, 'server')
)
? (
await fs.readdir(
path.join(
builtIconsDir,
'server'
)
)
).map((file) => `server/${file}`)
: [];
const promises = [
...files,
...serverFiles,
]
.filter(
(file) =>
!ignoreCleanFilenames.includes(
path.basename(file)
)
)
.map((file) => {
return fs.unlink(
path.join(builtIconsDir, file)
);
});
return Promise.all(promises).catch(
(err) => {
ctx[target] = { skip: true };
throw new Error(err.message);
}
);
} catch (err) {
ctx[target] = { skip: true };
throw new Error(err.message);
}
},
},
{
title: 'Building icon files',
skip: (ctx) => ctx[target]?.skip,
task: async (ctx) => {
try {
await execa(
'svgr',
[
'--config-file',
path.join(
targets[target].path,
'.svgrrc.json'
),
'--out-dir',
builtIconsDir,
'--template',
'bin/templates/icon-template.cjs',
'--index-template',
'bin/templates/index-template.cjs',
ctx.tmpDir,
],
{ preferLocal: true }
);
} catch (err) {
throw new Error(err.message);
}
},
},
...(target === 'iconoir-react'
? [
{
title:
'Building icon files (server components)',
skip: (ctx) => ctx[target]?.skip,
task: async (ctx) => {
try {
await execa(
'svgr',
[
'--config-file',
path.join(
targets[target].path,
'.svgrrc.json'
),
'--out-dir',
path.join(
builtIconsDir,
'server'
),
'--template',
'bin/templates/icon-template-server-component.cjs',
'--index-template',
'bin/templates/index-template.cjs',
ctx.tmpDir,
],
{ preferLocal: true }
);
} catch (err) {
throw new Error(err.message);
}
},
},
]
: []),
],
{ concurrent: false, exitOnError: false }
),
};
});
return task.newListr(tasks, {
concurrent: true,
rendererOptions: { collapse: false },
});
},
},
],
{ concurrent: false }
),
},
{
title: 'Building Vue library',
enabled: () =>
cliTargets.length === 0 ||
cliTargets.filter((cliTarget) => targets[cliTarget]?.vue)
.length > 0,
task: (_, task) =>
task.newListr(
[
{
title: 'Creating temporary directory',
task: async (ctx) => {
try {
ctx.tmpDir = await fs.mkdtemp(
path.join(os.tmpdir(), 'iconoir-')
);
} catch (err) {
ctx.skip = true;
throw new Error(err.message);
}
},
},
{
title:
'Copying icon files to temporary directory, while renaming icons with incompatible names',
skip: (ctx) => ctx.skip,
task: async (ctx) => {
try {
const promises = ctx.iconoirIconsFiles.map((file) => {
const srcFilePath = path.join(
iconoirIconsDir,
file
);
const iconName = file.split('.')[0];
const dstFileName =
iconName in incompatibleNames
? incompatibleNames[iconName]
: iconName;
const dstFilePath = path.join(
ctx.tmpDir,
`${dstFileName}.svg`
);
return fs.copyFile(srcFilePath, dstFilePath);
});
return Promise.all(promises).catch((err) => {
ctx.skip = true;
throw new Error(err.message);
});
} catch (err) {
ctx.skip = true;
throw new Error(err.message);
}
},
},
{
skip: (ctx) => ctx.skip,
task: (_, task) => {
const targetsToBuild =
cliTargets.length > 0
? cliTargets.filter(
(cliTarget) => targets[cliTarget]?.vue
)
: Object.keys(targets).filter(
(target) => targets[target].vue
);
const tasks = targetsToBuild.map((target) => {
const builtIconsDir = path.join(
rootDir,
targets[target].path,
'src'
);
return {
title: `Building ${target}`,
task: (_, task) =>
task.newListr(
[
{
title: 'Cleaning target directory',
task: async (ctx) => {
try {
const files = await fs.readdir(
builtIconsDir
);
files
.filter(
(file) =>
!ignoreCleanFilenames.includes(
path.basename(file)
)
)
.map((file) => {
return fs.unlink(
path.join(builtIconsDir, file)
);
});
return Promise.all(files).catch(
(err) => {
ctx[target] = { skip: true };
throw new Error(err.message);
}
);
} catch (err) {
ctx[target] = { skip: true };
throw new Error(err.message);
}
},
},
{
title: 'Building icon files',
skip: (ctx) => ctx[target]?.skip,
task: async (ctx) => {
try {
await buildVueIcons(ctx.tmpDir, {
outDir: builtIconsDir,
});
} catch (err) {
throw new Error(err.message);
}
},
},
],
{ concurrent: false, exitOnError: false }
),
};
});
return task.newListr(tasks, {
concurrent: true,
rendererOptions: { collapse: false },
});
},
},
],
{ concurrent: false }
),
},
{
title: 'Building Flutter libraries',
enabled: () =>
cliTargets.length === 0 ||
cliTargets.filter((cliTarget) => targets[cliTarget]?.flutter)
.length > 0,
task: (_, task) =>
task.newListr(
[
{
title: 'Creating temporary directory',
task: async (ctx) => {
try {
ctx.flutterTmpDir = await fs.mkdtemp(
path.join(os.tmpdir(), 'iconoir-')
);
} catch (err) {
ctx.skip = true;
throw new Error(err.message);
}
},
},
{
title:
'Copying icon files to temporary directory, while renaming icons with incompatible names',
skip: (ctx) => ctx.skip,
task: async (ctx) => {
try {
const promises = ctx.iconoirIconsFiles.map((file) => {
const srcFilePath = path.join(
iconoirIconsDir,
file
);
const iconName = file.split('.')[0];
const dstFileName =
iconName in flutterIncompatibleNames
? flutterIncompatibleNames[iconName]
: iconName;
const dstFilePath = path.join(
ctx.flutterTmpDir,
`${dstFileName}.svg`
);
ctx.dstFilePaths = [
...(ctx.dstFilePaths ?? []),
dstFilePath,
];
return fs.copyFile(srcFilePath, dstFilePath);
});
return Promise.all(promises).catch((err) => {
ctx.skip = true;
throw new Error(err.message);
});
} catch (err) {
ctx.skip = true;
throw new Error(err.message);
}
},
},
{
skip: (ctx) => ctx.skip,
task: (_, task) => {
const targetsToBuild =
cliTargets.length > 0
? cliTargets.filter(
(cliTarget) => targets[cliTarget]?.flutter
)
: Object.keys(targets).filter(
(target) => targets[target].flutter
);
const tasks = targetsToBuild.map((target) => {
const builtIconsDir = path.join(
rootDir,
targets[target].path,
'lib'
);
return {
title: `Building ${target}`,
task: (_, task) =>
task.newListr(
[
{
title: 'Cleaning target directory',
task: async (ctx) => {
try {
const files = await fs.readdir(
builtIconsDir
);
const promises = files.map((file) => {
return fs.unlink(
path.join(builtIconsDir, file)
);
});
return Promise.all(promises).catch(
(err) => {
ctx[target] = { skip: true };
throw new Error(err.message);
}
);
} catch (err) {
ctx[target] = { skip: true };
throw new Error(err.message);
}
},
},
{
title: 'Create entry file',
task: async () => {
await fs.writeFile(
path.join(
builtIconsDir,
'iconoir_flutter.dart'
),
'library iconoir_flutter;\n\n'
);
},
},
{
title: 'Building icon files',
skip: (ctx) => ctx[target]?.skip,
task: async (ctx) => {
const finalFileNames = [];
try {
await Promise.all(
ctx.dstFilePaths.map(async (file) => {
const svgfilename =
path.parse(file).name;
// Prefix with Svg if icon name starts with a number
const iconname = `${
/^\d/.test(svgfilename)
? 'Svg'
: ''
}${svgfilename}`;
const svgfilecontent = (
await fs.readFile(file)
).toString();
await generateTemplateFilesBatch([
{
option:
'Create Icon Flutter Widget',
entry: {
folderPath:
'./bin/templates/__svgfilename__.dart',
},
dynamicReplacers: [
{
slot: '__icon__',
slotValue: iconname,
},
{
slot: '__svgfilecontent__',
slotValue: svgfilecontent,
},
{
slot: '__svgfilename__',
slotValue: svgfilename,
},
],
output: {
path: './packages/iconoir-flutter/lib/__svgfilename__(snakeCase).dart',
pathAndFileNameDefaultCase:
'(snakeCase)',
},
async onComplete(results) {
finalFileNames.push(
results.output.path
);
},
},
]);
})
);
finalFileNames.sort();
await fs.appendFile(
path.join(
builtIconsDir,
'iconoir_flutter.dart'
),
finalFileNames
.map(
(fileName) =>
`export './${basename(
fileName
)}';`
)
.join('\n')
);
} catch (err) {
throw new Error(err.message);
}
},
},
],
{ concurrent: false, exitOnError: false }
),
};
});
return task.newListr(tasks, {
concurrent: true,
rendererOptions: { collapse: false },
});
},
},
],
{ concurrent: false }
),
},
],
{ concurrent: true }
),
},
{
title: 'Removing React temporary directory',
skip: (ctx) => !ctx.tmpDir,
task: async (ctx) => {
await fs.rm(ctx.tmpDir, { recursive: true });
},
},
{
title: 'Removing Flutter temporary directory',
skip: (ctx) => !ctx.flutterTmpDir,
task: async (ctx) => {
await fs.rm(ctx.flutterTmpDir, { recursive: true });
},
},
],
{
concurrent: false,
exitOnError: false,
rendererOptions: { collapse: false, collapseErrors: false },
}
);
await tasks.run();

View file

@ -1,139 +0,0 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
import { Listr } from 'listr2';
import { pascalCase, snakeCase } from 'scule';
import Tinypool from 'tinypool';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const rootDir = path.join(__dirname, '..', '..');
const iconsDir = path.join(rootDir, 'icons');
const iconsVariants = ['regular', 'solid'];
const defaultVariant = iconsVariants[0];
const targets = {
'css': {
title: 'CSS files',
path: 'css',
},
'flutter': {
title: 'Flutter library',
path: 'packages/iconoir-flutter',
},
'react': {
title: 'React library',
path: 'packages/iconoir-react',
},
'react-native': {
title: 'React Native library',
target: 'react',
native: true,
path: 'packages/iconoir-react-native',
},
'vue': {
title: 'Vue library',
path: 'packages/iconoir-vue',
},
};
const tasks = new Listr(
[
{
title: 'Fetching icons',
task: async (ctx) => {
ctx.tasks = { global: { defaultVariant }, icons: {} };
const iconsVariantsDirs = Object.fromEntries(
iconsVariants.map((variant) => [
variant,
path.join(iconsDir, variant),
]),
);
for (const [variant, dir] of Object.entries(iconsVariantsDirs)) {
const files = await fs.readdir(dir);
const icons = files
.filter((file) => file.endsWith('.svg'))
.map((file) => {
const name = path.parse(file).name;
const nameVariant = `${name}-${variant}`;
return {
name,
nameVariant,
pascalName: pascalCase(name),
pascalNameVariant: pascalCase(nameVariant),
snakeName: snakeCase(name),
snakeNameVariant: snakeCase(nameVariant),
path: path.join(dir, file),
};
});
ctx.tasks.icons[variant] = icons;
}
},
},
{
title: 'Building targets',
task: (ctx, task) =>
task.newListr(
Object.entries(targets).map(([targetName, targetConfig]) => ({
title: targetConfig.title,
enabled: () => ctx.cliTargets.length === 0 || ctx.cliTargets.includes(targetName),
task: (ctx) => {
targetConfig.path = path.join(
rootDir,
...targetConfig.path.split(path.posix.sep),
);
return ctx.pool.run({ targetName, config: ctx.tasks, targetConfig });
},
})),
{ concurrent: true, exitOnError: false },
),
},
],
{
rendererOptions: {
collapseSubtasks: false,
collapseErrors: false,
},
},
);
const cliTargets = [];
// Get targets from command line arguments
// (build all targets if no arguments given)
for (const arg of process.argv.slice(2)) {
if (arg in targets) {
cliTargets.push(arg);
} else {
console.error(
`Target '${arg}' doesn't exist!\n\nPossible targets are:\n${Object.keys(
targets,
)
.map((name) => `- ${name}`)
.join('\n')}`,
);
process.exit(1);
}
}
const pool = new Tinypool({
filename: new URL('./worker.js', import.meta.url).href,
minThreads: 0,
resourceLimits: {
// Vue target (Vite/Rollup) takes up a lot of memory
maxOldGenerationSizeMb: 8192,
},
});
await tasks.run({ cliTargets, pool });
await pool.destroy();

View file

@ -1,20 +0,0 @@
import path from 'node:path';
export function generateImport(name, from) {
if (Array.isArray(name))
name = `{${name.toString()}}`;
return `import ${name} from "${from}";`;
}
export function generateExport(name, from) {
const base = `export {${name.toString()}}`;
return from ? `${base} from "${from}";` : `${base};`;
}
export function toImportPath(input) {
input = input.split(path.sep).join(path.posix.sep);
return input.charAt(0) !== '.' ? `./${input}` : input;
}

View file

@ -1,65 +0,0 @@
import { normalize } from 'node:path';
import ts from 'typescript';
/**
*
* @param {string} path
* @param {string} content
* @param {object} options
*/
export function getDts(path, content, options) {
options = ts.convertCompilerOptionsFromJson(options, '').options;
let output;
const host = ts.createCompilerHost(options);
const _readFile = host.readFile;
host.readFile = (filename) => {
if (normalize(filename) === path)
return content;
return _readFile(filename);
};
const dtsFilename = path.replace(/\.(m|c)?(ts|js)x?$/, '.d.$1ts');
host.writeFile = (filename, contents) => {
if (normalize(filename) === dtsFilename)
output = contents;
};
const program = ts.createProgram([path], options, host);
const emitResult = program.emit();
const allDiagnostics = ts
.getPreEmitDiagnostics(program)
.concat(emitResult.diagnostics);
const results = allDiagnostics.map((diagnostic) => {
if (diagnostic.file) {
const { line, character } = ts.getLineAndCharacterOfPosition(
diagnostic.file,
diagnostic.start,
);
const message = ts.flattenDiagnosticMessageText(
diagnostic.messageText,
'\n',
);
return `${diagnostic.file.fileName} (${line + 1},${
character + 1
}): ${message}`;
} else {
return ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
}
});
if (results.length > 0) {
throw new Error(results);
}
return output;
}

View file

@ -1,21 +0,0 @@
/*!
* Iconoir
* Copyright (c) [YEAR] Luca Burgio - https://iconoir.com
* License - https://github.com/iconoir-icons/iconoir/blob/main/LICENSE (Code: MIT License)
* CSS file created by Till Esser (@Wiwaltill) and automated by Pascal Jufer (@paescuj)
*/
*[class^='iconoir-']::before,
*[class*=' iconoir-']::before {
content: ' ';
display: block;
background: currentColor;
mask-size: cover;
-webkit-mask-size: cover;
width: 1em;
height: 1em;
}
*[class^='iconoir-'],
*[class*=' iconoir-'] {
display: inline-block;
}

View file

@ -1,51 +0,0 @@
import fs from 'node:fs/promises';
import { EOL } from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export default async (ctx, target) => {
const headerFile = await fs.readFile(
path.join(__dirname, 'header.css'),
'utf8',
);
const header = headerFile.replace('[YEAR]', new Date().getFullYear());
const mainCssContent = [header];
for (const [variant, icons] of Object.entries(ctx.icons)) {
const variantCssContent = [header];
const cssTarget = (icon, suffixed) => {
const iconName = suffixed && variant !== ctx.global.defaultVariant
? icon.nameVariant
: icon.name;
return `.iconoir-${iconName}::before`;
};
for (const icon of icons) {
const fileContent = await fs.readFile(icon.path, 'utf8');
const transformedContent = fileContent
.replaceAll(EOL, '')
.replace(/(width|height)="\d+px"/g, '')
.replace(/ +/g, ' ');
const cssContent = `{mask-image:url('data:image/svg+xml;charset=utf-8,${transformedContent}');-webkit-mask-image:url('data:image/svg+xml;charset=utf-8,${transformedContent}');}`;
mainCssContent.push(`${cssTarget(icon, true)}${cssContent}`);
variantCssContent.push(`${cssTarget(icon)}${cssContent}`);
}
await fs.writeFile(
path.join(target.path, `iconoir-${variant}.css`),
variantCssContent,
);
}
await fs.writeFile(path.join(target.path, 'iconoir.css'), mainCssContent);
};

View file

@ -1,47 +0,0 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import iconTemplate from './template.js';
export default async (ctx, target) => {
const promises = [];
const outDir = path.join(target.path, 'lib');
const entryContent = ['library;'];
for (const [variant, icons] of Object.entries(ctx.icons)) {
const variantOutDir = path.join(outDir, variant);
await fs.mkdir(variantOutDir, { recursive: true });
for (const icon of icons) {
const dartFileName = `${icon.snakeName}.dart`;
const dartPath = path.join(variant, dartFileName);
promises.push(
generateIconFile(
icon.path,
path.join(outDir, dartPath),
variant !== ctx.global.defaultVariant
? icon.pascalNameVariant
: icon.pascalName,
),
);
entryContent.push(`export './${dartPath}';`);
}
}
promises.push(
fs.writeFile(path.join(outDir, 'iconoir_flutter.dart'), entryContent),
);
return Promise.all(promises);
};
async function generateIconFile(src, dest, iconName) {
const iconContent = await fs.readFile(src, 'utf8');
const dartContent = iconTemplate(iconName, iconContent);
return fs.writeFile(dest, dartContent);
}

View file

@ -1,26 +0,0 @@
function template(name, svg) {
return `import 'package:flutter/widgets.dart' as widgets;
import 'package:flutter_svg/flutter_svg.dart';
class ${name} extends widgets.StatelessWidget {
final widgets.Color? color;
final double? width;
final double? height;
const ${name}({super.key, this.color, this.width, this.height});
@override
widgets.Widget build(widgets.BuildContext context) => SvgPicture.string(
'''
${svg}''',
colorFilter: color != null
? widgets.ColorFilter.mode(color!, widgets.BlendMode.srcIn)
: null,
width: width,
height: height,
);
}
`;
}
export default template;

View file

@ -1,263 +0,0 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import * as svgr from '@svgr/core';
import * as esbuild from 'esbuild';
import {
generateExport,
generateImport,
toImportPath,
} from '../../lib/import-export.js';
import { getDts } from '../../lib/ts.js';
import iconoirContextTemplate, {
exports as iconoirContextExports,
} from './resources/context-template.js';
import { getTemplate as getIconTemplate } from './resources/icon-template.js';
import { nativeSvgrOptions, svgrOptions } from './resources/svgr-options.js';
const outDir = 'dist';
const jsTargets = [
{
format: 'cjs',
module: 'commonjs',
dir: '.',
ext: 'js',
dtsExt: 'd.ts',
},
{
format: 'esm',
module: 'esnext',
dir: 'esm',
ext: 'mjs',
dtsExt: 'd.mts',
},
];
/** @type {import('esbuild').TransformOptions} */
const defaultEsbuildOptions = { target: 'es6', minify: true };
const defaultTsOptions = {
declaration: true,
emitDeclarationOnly: true,
target: 'es6',
strict: true,
esModuleInterop: true,
skipLibCheck: true,
};
export default async (ctx, target) => {
const localJsTargets = jsTargets.map((jsTarget) => ({
...jsTarget,
}));
const promises = [];
const outPath = path.join(target.path, outDir);
// Preparation
// (needs to run in a separate loop, otherwise leads to uncaught exceptions in case of errors in main loop)
for (const jsTarget of localJsTargets) {
jsTarget.path = path.join(outPath, jsTarget.dir);
await fs.mkdir(jsTarget.path, { recursive: true });
const iconoirContext = iconoirContextTemplate(target.native);
jsTarget.iconoirContextPath = path.join(
jsTarget.path,
`IconoirContext.${jsTarget.ext}`,
);
await generateJs(
jsTarget.iconoirContextPath,
iconoirContext,
jsTarget.format,
);
const iconoirContextTsxPath = path.join(
jsTarget.path,
'IconoirContext.tsx',
);
const iconoirContextDtsPath = path.join(
jsTarget.path,
`IconoirContext.${jsTarget.dtsExt}`,
);
await generateDts(
iconoirContextTsxPath,
iconoirContextDtsPath,
iconoirContext,
jsTarget.module,
target.native,
);
for (const variant of Object.keys(ctx.icons)) {
jsTarget.path = path.join(outPath, jsTarget.dir);
await fs.mkdir(path.join(jsTarget.path, variant), { recursive: true });
}
}
for (const jsTarget of localJsTargets) {
const mainIndex = prepareIndex(jsTarget);
for (const [variant, icons] of Object.entries(ctx.icons)) {
const variantIndex = prepareIndex(jsTarget, variant);
for (const icon of icons) {
const mainIndexComponentName = variant === ctx.global.defaultVariant
? icon.pascalName
: icon.pascalNameVariant;
const jsPath = path.join(
jsTarget.path,
variant,
`${icon.pascalName}.${jsTarget.ext}`,
);
mainIndex.add(mainIndexComponentName, jsPath);
variantIndex.add(icon.pascalName, jsPath);
if (!jsTarget.iconTemplate) {
jsTarget.iconTemplate = getIconTemplate(
target.native,
toImportPath(
path.relative(
path.join(jsTarget.path, variant),
jsTarget.iconoirContextPath,
),
),
);
}
const reactComponent = getReactComponent(
icon.path,
target.native,
jsTarget.iconTemplate,
);
// Only run for first icon, type is same and can be reused for all the others
if (!jsTarget.iconDts) {
jsTarget.iconDts = true;
// Virtual input path
const tsxPath = path.join(jsTarget.path, variant, 'icon.tsx');
const dtsPath = path.join(jsTarget.path, `icon.${jsTarget.dtsExt}`);
const iconDts = generateDts(
tsxPath,
dtsPath,
reactComponent,
jsTarget.module,
target.native,
);
promises.push(iconDts);
}
const iconJs = generateJs(jsPath, reactComponent, jsTarget.format);
promises.push(iconJs);
}
promises.push(variantIndex.generate());
}
promises.push(mainIndex.generate());
}
return Promise.all(promises);
};
async function getReactComponent(iconPath, native, template) {
const iconContent = await fs.readFile(iconPath, 'utf8');
const options = {
...(native ? nativeSvgrOptions : svgrOptions),
template,
};
return svgr.transform(iconContent, options);
}
async function generateDts(inputPath, outputPath, input, module, native) {
const dts = getDts(inputPath, await input, {
...defaultTsOptions,
jsx: native ? 'react-native' : 'react',
module,
...(module === 'esnext' && { moduleResolution: 'bundler' }),
});
return fs.writeFile(outputPath, dts);
}
async function generateJs(outputPath, input, format) {
const { code } = await esbuild.transform(await input, {
...defaultEsbuildOptions,
loader: 'tsx',
format,
});
return fs.writeFile(outputPath, code);
}
function prepareIndex(jsTarget, variant) {
const outputPath = path.join(jsTarget.path, variant ?? '');
const iconoirContextPath = toImportPath(
path.relative(outputPath, jsTarget.iconoirContextPath),
);
const iconoirContext = generateExport(
iconoirContextExports,
iconoirContextPath,
);
const content = [iconoirContext];
const iconJsPath = toImportPath(
path.relative(outputPath, path.join(jsTarget.path, `icon.${jsTarget.ext}`)),
);
const iconDtsImport = generateImport('Icon', iconJsPath);
const dtsContent = [iconoirContext, iconDtsImport, 'type I = typeof Icon;'];
function add(name, iconPath) {
const iconImportPath = toImportPath(path.relative(outputPath, iconPath));
content.push(generateExport(`default as ${name}`, iconImportPath));
dtsContent.push(`export declare const ${name}: I;`);
}
function generate() {
const indexJs = generateIndexJs(
outputPath,
content,
jsTarget.format,
jsTarget.ext,
);
const indexDts = generateIndexDts(outputPath, dtsContent, jsTarget.dtsExt);
return Promise.all([indexJs, indexDts]);
}
return { add, generate };
}
async function generateIndexJs(outputDir, content, format, ext) {
const { code } = await esbuild.transform(content.join(''), {
minify: true,
format,
});
return fs.writeFile(path.join(outputDir, `index.${ext}`), code);
}
async function generateIndexDts(outputDir, content, dtsExt) {
return fs.writeFile(path.join(outputDir, `index.${dtsExt}`), content);
}

View file

@ -1,36 +0,0 @@
function template(native) {
const useClientDirective = native ? '' : '"use client";';
const imports = [
'import React from "react";',
...(native ? ['import type { SvgProps } from "react-native-svg";'] : []),
].join('\n');
return `
${useClientDirective}
${imports}
type IconoirContextValue = Partial<${
native ? 'SvgProps' : 'React.SVGProps<SVGSVGElement>'
}>;
export const IconoirContext = React.createContext<IconoirContextValue>({});
export interface IconoirProviderProps {
iconProps?: Partial<${
native ? `Omit<SvgProps, 'children'>` : 'React.SVGProps<SVGSVGElement>'
}>;
children: React.ReactNode;
}
export function IconoirProvider({ iconProps, children }: IconoirProviderProps) {
return (
<IconoirContext.Provider value={iconProps || {}} children={children} />
);
}
`;
}
export default template;
export const exports = ['IconoirContext', 'IconoirProvider'];

View file

@ -1,34 +0,0 @@
import { generateImport } from '../../../lib/import-export.js';
export function getTemplate(native, iconoirContextPath) {
return (variables, { tpl }) => {
variables.props[0].name = 'passedProps';
// Workaround to fix ref type for React Native
if (native)
variables.props[1].typeAnnotation.typeAnnotation.typeParameters.params[0].typeName.name = 'Svg';
const useClientDirective = native ? [] : '"use client"';
const iconoirContextImport = generateImport(
['IconoirContext'],
iconoirContextPath,
);
return tpl`
${useClientDirective};
${variables.imports};
${iconoirContextImport}
${variables.interfaces};
const ${variables.componentName} = (${variables.props}) => {
const context = React.useContext(IconoirContext);
const props = { ...context, ...passedProps };
return ${variables.jsx};
};
${variables.exports};
`;
};
}

View file

@ -1,46 +0,0 @@
/** @type {import('@svgr/core').Config} */
export const svgrOptions = {
plugins: ['@svgr/plugin-jsx'],
icon: true,
ref: true,
typescript: true,
svgProps: {
width: '1.5em',
height: '1.5em',
color: 'currentColor',
},
jsx: {
babelConfig: {
plugins: [
[
'@svgr/babel-plugin-remove-jsx-attribute',
{
elements: ['path'],
attributes: ['strokeWidth'],
},
'remove-stroke-width',
],
],
},
},
};
/** @type {import('@svgr/core').Config} */
export const nativeSvgrOptions = {
...svgrOptions,
native: true,
jsx: {
babelConfig: {
plugins: [
...svgrOptions.jsx.babelConfig.plugins,
[
'@svgr/babel-plugin-remove-jsx-attribute',
{
elements: ['Svg'],
attributes: ['xmlns'],
},
],
],
},
},
};

View file

@ -1,97 +0,0 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import vue from '@vitejs/plugin-vue';
import { fromHtml } from 'hast-util-from-html';
import { toHtml } from 'hast-util-to-html';
import { build } from 'vite';
import dts from 'vite-plugin-dts';
import { generateExport } from '../../lib/import-export.js';
import iconTemplate from './template.js';
export default async (ctx, target) => {
const promises = [];
const outDir = path.join(target.path, 'src');
const mainIndexContent = [
generateExport(`default as IconoirProvider`, `./IconoirProvider.vue`),
];
for (const [variant, icons] of Object.entries(ctx.icons)) {
const variantOutDir = path.join(outDir, variant);
await fs.mkdir(variantOutDir, { recursive: true });
const variantIndexContent = [
generateExport(`default as IconoirProvider`, `../IconoirProvider.vue`),
];
const generateIconFile = async (src, vueFileName) => {
const iconContent = await fs.readFile(src, 'utf8');
const iconAst = fromHtml(iconContent, { fragment: true });
// Bind iconProps of the provider to the svg root
iconAst.children[0].properties['v-bind'] = 'context';
const transformedIcon = toHtml(iconAst);
const componentContent = iconTemplate(transformedIcon);
const vuePath = path.join(variantOutDir, vueFileName);
return fs.writeFile(vuePath, componentContent);
};
for (const icon of icons) {
const vueFileName = `${icon.pascalName}.vue`;
promises.push(generateIconFile(icon.path, vueFileName));
const mainIndexComponentName = variant === ctx.global.defaultVariant
? icon.pascalName
: icon.pascalNameVariant;
mainIndexContent.push(
generateExport(
`default as ${mainIndexComponentName}`,
`./${variant}/${vueFileName}`,
),
);
variantIndexContent.push(
generateExport(
`default as ${mainIndexComponentName}`,
`./${vueFileName}`,
),
);
}
promises.push(
fs.writeFile(path.join(variantOutDir, 'index.ts'), variantIndexContent),
);
}
promises.push(fs.writeFile(path.join(outDir, 'index.ts'), mainIndexContent));
await Promise.all(promises);
return build({
root: target.path,
logLevel: 'silent',
build: {
outDir: 'dist',
lib: {
entry: path.join('src', 'index.ts'),
fileName: (format, name) =>
format === 'cjs' ? `${name}.js` : `esm/${name}.mjs`,
formats: ['cjs', 'es'],
},
rollupOptions: {
external: ['vue'],
},
},
plugins: [
vue({
isProduction: true,
}),
dts(),
],
});
};

View file

@ -1,21 +0,0 @@
function template(svg) {
return `<script lang="ts">
import type { SVGAttributes } from 'vue';
import { defineComponent, inject } from 'vue';
import providerKey from '../providerKey';
export default defineComponent<SVGAttributes>({
setup() {
const context = inject(providerKey);
return { context };
},
});
</script>
<template>
${svg}
</template>
`;
}
export default template;

View file

@ -1,7 +0,0 @@
export default async ({ targetName, config, targetConfig }) => {
const { default: task } = await import(
`./targets/${targetConfig.target || targetName}/index.js`
);
return task(config, targetConfig);
};

48
bin/buildVue.js Normal file
View file

@ -0,0 +1,48 @@
import fs from 'fs/promises';
import { parse } from 'svg-parser';
import componentTemplate from './templates/vue/icon-template.cjs';
import indexTemplate from './templates/vue/index-template.cjs';
import providerTemplate from './templates/vue/provider-template.cjs';
import providerKeyTemplate from './templates/vue/provider-key-template.cjs';
import { toHtml } from 'hast-util-to-html';
import path from 'path';
export async function buildVueIcons(srcDir, { outDir = './out/' }) {
const files = await fs.readdir(srcDir, 'utf8');
const providerKeyFileName = 'providerKey';
const providerKey = providerKeyTemplate();
await fs.writeFile(path.join(outDir, providerKeyFileName + '.ts'), providerKey, 'utf8');
const fileNames = [];
for (const file of files) {
const svgRaw = await fs.readFile(path.join(srcDir, file), 'utf8');
const svgAst = parse(svgRaw);
// Bind iconProps of the provider to the svg root
svgAst.children[0].properties['v-bind'] = 'context';
const svgString = toHtml(svgAst);
const component = componentTemplate(svgString, providerKeyFileName);
const pascalCaseFileName = file
.replaceAll(/[\s-]([\w\d])/g, (_, cg1) => cg1.toUpperCase())
.replace(/^\w/, (m) => m.toUpperCase())
.replace('.svg', '.vue');
await fs.writeFile(
path.join(outDir, pascalCaseFileName),
component,
'utf8'
);
fileNames.push(pascalCaseFileName);
}
const providerFileName = 'IconoirProvider.vue';
const provider = providerTemplate(providerKeyFileName);
await fs.writeFile(path.join(outDir, providerFileName), provider, 'utf8');
fileNames.push(providerFileName);
const index = indexTemplate(fileNames);
await fs.writeFile(path.join(outDir, 'index.ts'), index, 'utf8');
}

21
bin/header.css Normal file
View file

@ -0,0 +1,21 @@
/*!
* Iconoir
* Copyright (c) [YEAR] Luca Burgio - https://iconoir.com
* License - https://github.com/lucaburgio/iconoir/blob/main/LICENSE (Code: MIT License)
* CSS file created by Till Esser (@Wiwaltill) and automated by Pascal Jufer (@paescuj)
*/
*[class^="iconoir-"]::before,
*[class*=" iconoir-"]::before {
content: " ";
display: block;
background: currentColor;
mask-size: cover;
-webkit-mask-size: cover;
width: 1em;
height: 1em;
}
*[class^="iconoir-"],
*[class*=" iconoir-"] {
display: inline-block;
}

View file

@ -1,34 +1,28 @@
/* eslint-disable no-console */
import fs from 'node:fs';
import path from 'node:path';
import process from 'node:process';
import { updateYamlKey } from '@atomist/yaml-updater';
import fs from 'fs';
import path from 'path';
import semver from 'semver';
const PACKAGE_BASE = '';
const newVersion = semver.valid(semver.coerce(process.env.TAG_NAME));
console.info('New version is %s', newVersion);
if (!newVersion) {
throw new Error(`Tag name ${process.env.TAG_NAME} is not valid.`);
}
publishNpmPackage('iconoir');
publishNpmPackage('iconoir-react');
publishNpmPackage('iconoir-react-native');
publishNpmPackage('iconoir-vue');
publishPubPackage('iconoir-flutter');
function publishNpmPackage(name) {
console.info('Publishing %s', name);
const packageJsonPath = name === 'iconoir'
? 'package.json'
: path.join('packages', name, 'package.json');
const packageJsonPath =
name === 'iconoir'
? 'package.json'
: path.join('packages', name, 'package.json');
const contents = JSON.parse(fs.readFileSync(packageJsonPath).toString());
contents.version = newVersion;
if (PACKAGE_BASE) {
contents.name = `${PACKAGE_BASE}/${name}`;
}
fs.writeFileSync(packageJsonPath, JSON.stringify(contents, undefined, 2));
console.info('package.json updated');
}
@ -39,8 +33,14 @@ function publishPubPackage(name) {
fs.writeFileSync(
pubspecFilepath,
updateYamlKey('version', newVersion, pubspecContents),
updateYamlKey('version', newVersion, pubspecContents)
);
console.info('pubspec.yaml updated');
}
publishNpmPackage('iconoir');
publishNpmPackage('iconoir-react');
publishNpmPackage('iconoir-react-native');
publishNpmPackage('iconoir-vue');
publishPubPackage('iconoir-flutter');

View file

@ -0,0 +1,21 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_svg/flutter_svg.dart';
class __icon__(pascalCase) extends StatelessWidget {
final Color? color;
final double? width;
final double? height;
const __icon__(pascalCase)({Key? key, this.color, this.width, this.height})
: super(key: key);
@override
Widget build(BuildContext context) => SvgPicture.string(
'''
__svgfilecontent__''',
colorFilter:
color != null ? ColorFilter.mode(color!, BlendMode.srcIn) : null,
width: width,
height: height,
);
}

View file

@ -0,0 +1,22 @@
const template = (
{ template },
opts,
{ imports, interfaces, componentName, props, jsx, exports }
) => {
const plugins = ['jsx'];
if (opts.typescript) {
plugins.push('typescript');
}
const typeScriptTpl = template.smart({ plugins });
return typeScriptTpl.ast`${imports}
${interfaces}
function ${componentName}(${props}) {
return ${jsx};
}
${exports}
`;
};
module.exports = template;

View file

@ -0,0 +1,26 @@
const template = (
{ template },
opts,
{ imports, interfaces, componentName, props, jsx, exports }
) => {
const plugins = ['jsx'];
if (opts.typescript) {
plugins.push('typescript');
}
const typeScriptTpl = template.smart({ plugins });
props[0].name = 'passedProps';
return typeScriptTpl.ast`${imports}
import { IconoirContext } from './IconoirContext'
${interfaces}
function ${componentName}(${props}) {
const context = React.useContext(IconoirContext);
const props = { ...context, ...passedProps };
return ${jsx};
}
${exports}
`;
};
module.exports = template;

View file

@ -0,0 +1,15 @@
const path = require('path');
function template(filePaths) {
const exportEntries = filePaths.map((filePath) => {
const basename = path.basename(filePath, path.extname(filePath));
const exportName = /^\d/.test(basename) ? `Svg${basename}` : basename;
return `export { default as ${exportName} } from './${basename}'`;
});
exportEntries.push(
"export { IconoirProvider, IconoirContext, IconoirContextValue } from './IconoirContext'"
);
return exportEntries.join('\n');
}
module.exports = template;

View file

@ -0,0 +1,14 @@
const template = (svg, keyFileName) => `<script lang="ts">
import { defineComponent, inject } from "vue-demi";
import type { SVGAttributes } from "vue-demi";
import providerKey from "./${keyFileName}";
export default defineComponent<SVGAttributes>(() => {
const context = inject(providerKey);
return { context };
});
</script>
<template>
${svg}
</template>`;
module.exports = template;

View file

@ -0,0 +1,13 @@
const path = require('path');
function template(filePaths) {
const exportEntries = filePaths.map((filePath) => {
const basename = path.basename(filePath, path.extname(filePath));
const exportName = /^\d/.test(basename) ? `Svg${basename}` : basename;
return `export { default as ${exportName} } from './${filePath}'`;
});
return exportEntries.join('\n');
}
module.exports = template;

View file

@ -0,0 +1,6 @@
const template = () => `import type { InjectionKey, SVGAttributes } from "vue-demi";
const providerKey = Symbol() as InjectionKey<SVGAttributes>;
export default providerKey;
`;
module.exports = template;

View file

@ -0,0 +1,15 @@
const template = (keyFileName) => `<script setup lang="ts">
import { provide } from "vue-demi";
import type { SVGAttributes } from "vue-demi";
import providerKey from "./${keyFileName}";
interface Props {
iconProps: SVGAttributes
}
const props = defineProps<Props>();
provide(providerKey, props.iconProps);
</script>
<template>
<slot />
</template>`;
module.exports = template;

17
constants.js Normal file
View file

@ -0,0 +1,17 @@
export const incompatibleNames = {
'1st-medal': 'medal-1st',
'4k-display': 'display-4k',
'2x2-cell': 'cell-2x2',
'360-view': 'view360',
github: 'gitHub',
'github-outline': 'gitHubOutline',
'gitlab-full': 'gitLabFull',
linkedin: 'linkedIn',
tiktok: 'tikTok',
youtube: 'youTube',
};
export const flutterIncompatibleNames = {
...incompatibleNames,
'color-filter': 'color-filter-icon'
};

View file

@ -1,9 +1,9 @@
# Iconoir - CSS
# Iconoir CSS
Import the CSS file:
Import the CSS File:
```html
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/iconoir-icons/iconoir@main/css/iconoir.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/iconoir-icons/iconoir@main/css/iconoir.css">
```
Here is an example in HTML:
@ -11,8 +11,9 @@ Here is an example in HTML:
```html
<i class="iconoir-hand-brake"></i>
```
The class must always be "iconoir-" and then the name of the icon. You can find the names of the icons [here](https://iconoir.com).
The icons are `display: inline-block` and default to the current font size. You can control this
by adjusting the `::before` styles of the element (which is where the icons are added as a mask).
<SuggestLibrary />

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,7 +1,7 @@
# Iconoir - Framer
# Iconoir Framer
Iconoir is happily part of [Framer](https://framer.com).
Iconoir is happily part of [Framer](https://framer.com) now. To start using the icons: On the top
menu, `Insert` > `Graphics` > `Iconoir`. You can switch between icons from the right sidebar in the
editor.
To start using the icons: On the top menu, `Insert` > `Graphics` > `Iconoir`.
You can switch between icons from the right sidebar in the editor.
<SuggestLibrary />

View file

@ -1,115 +0,0 @@
// @ts-check
import antfu from '@antfu/eslint-config';
import nextPlugin from '@next/eslint-plugin-next';
import jsxA11yPlugin from 'eslint-plugin-jsx-a11y';
import reactPlugin from 'eslint-plugin-react';
import hooksPlugin from 'eslint-plugin-react-hooks';
export default antfu({
typescript: true,
formatters: true,
stylistic: {
semi: true,
overrides: {
'style/arrow-parens': 'error',
'style/brace-style': ['error', '1tbs', { allowSingleLine: true }],
},
},
javascript: {
overrides: {
'antfu/no-top-level-await': 'off',
},
},
ignores: [
'css/*.css',
'iconoir.com/out/',
'**/.expo/',
'packages/iconoir-flutter/.dart_tool/',
'packages/iconoir-flutter/build/',
'packages/iconoir-flutter/example/',
],
rules: {
'style/padding-line-between-statements': [
'error',
{
blankLine: 'always',
prev: [
'block',
'block-like',
'cjs-export',
'class',
'multiline-block-like',
'multiline-const',
'multiline-expression',
'multiline-let',
'multiline-var',
],
next: '*',
},
{
blankLine: 'always',
prev: ['const', 'let'],
next: [
'block',
'block-like',
'cjs-export',
'class',
],
},
{
blankLine: 'always',
prev: '*',
next: [
'multiline-block-like',
'multiline-const',
'multiline-expression',
'multiline-let',
'multiline-var',
],
},
],
},
}, {
files: ['iconoir.com/**'],
plugins: {
'@next/next': nextPlugin,
'react': reactPlugin,
'react-hooks': hooksPlugin,
'jsx-a11y': jsxA11yPlugin,
},
settings: {
next: {
rootDir: 'iconoir.com/',
},
react: {
version: 'detect',
},
},
// @ts-ignore
rules: {
...nextPlugin.configs.recommended.rules,
...nextPlugin.configs['core-web-vitals'].rules,
...reactPlugin.configs.recommended.rules,
...hooksPlugin.configs.recommended.rules,
// rules from "eslint-config-next"
'react/no-unknown-property': 'off',
'react/react-in-jsx-scope': 'off',
'react/prop-types': 'off',
'jsx-a11y/alt-text': [
'warn',
{
elements: ['img'],
img: ['Image'],
},
],
'jsx-a11y/aria-props': 'warn',
'jsx-a11y/aria-proptypes': 'warn',
'jsx-a11y/aria-unsupported-elements': 'warn',
'jsx-a11y/role-has-required-aria-props': 'warn',
'jsx-a11y/role-supports-aria-props': 'warn',
'react/jsx-no-target-blank': 'off',
},
});

View file

@ -1,41 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View file

@ -1,17 +0,0 @@
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Iconoir',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}

View file

@ -1,32 +0,0 @@
import {
Check,
Iconoir,
IconoirProvider,
Medal1st,
Medal1stSolid,
} from 'iconoir-react';
import { AdobeAfterEffects as AdobeAfterEffectsRegular } from 'iconoir-react/regular';
import { AdobeAfterEffects as AdobeAfterEffectsSolid } from 'iconoir-react/solid';
export default function Home() {
return (
<>
<Iconoir />
<Medal1st color="red" height={36} width={36} />
<Medal1stSolid />
<AdobeAfterEffectsRegular color="red" />
<AdobeAfterEffectsSolid color="green" />
<IconoirProvider
iconProps={{
color: '#1E441E',
strokeWidth: 1,
width: '2em',
height: '2em',
}}
>
<Check />
</IconoirProvider>
</>
);
}

View file

@ -1,9 +0,0 @@
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
experimental: {
optimizePackageImports: ['iconoir-react'],
},
};
export default nextConfig;

View file

@ -1,22 +0,0 @@
{
"name": "example-next",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "15.4.4",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@types/node": "^22.10.2",
"@types/react": "^19.0.1",
"@types/react-dom": "^19.0.2",
"iconoir-react": "workspace:*",
"typescript": "^5.7.2"
}
}

View file

@ -1,27 +0,0 @@
{
"compilerOptions": {
"incremental": true,
"target": "ES2017",
"jsx": "preserve",
"lib": ["dom", "dom.iterable", "esnext"],
"module": "esnext",
"moduleResolution": "bundler",
"paths": {
"@/*": ["./*"]
},
"resolveJsonModule": true,
"allowJs": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"isolatedModules": true,
"skipLibCheck": true,
"plugins": [
{
"name": "next"
}
]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

View file

@ -1,36 +0,0 @@
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
# dependencies
node_modules/
# Expo
.expo/
dist/
web-build/
expo-env.d.ts
# Native
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
# Metro
.metro-health-check*
# debug
npm-debug.*
yarn-debug.*
yarn-error.*
# macOS
.DS_Store
*.pem
# local env files
.env*.local
# typescript
*.tsbuildinfo

View file

@ -1,21 +0,0 @@
import { Check, Iconoir, IconoirProvider } from 'iconoir-react-native';
import { View } from 'react-native';
export default function App() {
return (
<View>
<Iconoir />
<IconoirProvider
iconProps={{
color: '#1E441E',
strokeWidth: 1,
width: '2em',
height: '2em',
}}
>
<Check />
</IconoirProvider>
</View>
);
}

View file

@ -1,21 +0,0 @@
{
"expo": {
"name": "Iconoir",
"slug": "example-react-native",
"version": "1.0.0",
"orientation": "portrait",
"userInterfaceStyle": "light",
"newArchEnabled": true,
"splash": {
"backgroundColor": "#ffffff"
},
"ios": {
"supportsTablet": true
},
"android": {
"adaptiveIcon": {
"backgroundColor": "#ffffff"
}
}
}
}

View file

@ -1,8 +0,0 @@
import { registerRootComponent } from 'expo';
import App from './App';
// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
// It also ensures that whether you load the app in Expo Go or in a native build,
// the environment is set up appropriately
registerRootComponent(App);

View file

@ -1,31 +0,0 @@
/*
* Workaround to be able to import iconoir lib from workspace.
* See also: https://github.com/pnpm/pnpm/issues/4286
*/
const { makeMetroConfig } = require('@rnx-kit/metro-config');
const MetroSymlinksResolver = require('@rnx-kit/metro-resolver-symlinks');
const { getDefaultConfig } = require('expo/metro-config');
const symlinksResolver = MetroSymlinksResolver({
remapModule: (_context, moduleName) => {
if (moduleName === 'iconoir-react-native') {
return require.resolve(moduleName);
}
return moduleName;
},
},
);
/** @type {import('expo/metro-config').MetroConfig} */
const expoConfig = getDefaultConfig(__dirname);
/** @type {import('expo/metro-config').MetroConfig} */
module.exports = makeMetroConfig({
...expoConfig,
resolver: {
...expoConfig.resolver,
resolveRequest: symlinksResolver,
},
});

View file

@ -1,30 +0,0 @@
{
"name": "example-react-native",
"version": "1.0.0",
"private": true,
"main": "index.ts",
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web"
},
"dependencies": {
"expo": "^53.0.20",
"expo-status-bar": "^2.0.0",
"iconoir-react-native": "workspace:*",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-native": "^0.79.5",
"react-native-svg": "^15.11.2",
"react-native-web": "^0.20.0"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@react-native/metro-config": "^0.79.5",
"@rnx-kit/metro-config": "^2.0.1",
"@rnx-kit/metro-resolver-symlinks": "^0.2.1",
"@types/react": "^19.0.14",
"typescript": "~5.8.3"
}
}

View file

@ -1,6 +0,0 @@
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true
}
}

View file

@ -1,30 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo

View file

@ -1 +0,0 @@
/// <reference types="vite/client" />

View file

@ -1,12 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Iconoir</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View file

@ -1,27 +0,0 @@
{
"name": "example-vue",
"type": "module",
"private": true,
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build"
},
"dependencies": {
"vue": "^3.5.18"
},
"devDependencies": {
"@iconoir/vue": "workspace:*",
"@tsconfig/node22": "^22.0.2",
"@types/node": "^22.16.5",
"@vitejs/plugin-vue": "^6.0.0",
"@vue/tsconfig": "^0.7.0",
"npm-run-all2": "^8.0.4",
"typescript": "~5.8.3",
"vite": "^7.0.6",
"vite-plugin-vue-devtools": "^7.7.7",
"vue-tsc": "^3.0.4"
}
}

View file

@ -1,26 +0,0 @@
<script setup lang="ts">
import {
Check,
Iconoir,
IconoirProvider,
Medal1st,
Medal1stSolid,
} from '@iconoir/vue';
</script>
<template>
<Iconoir />
<Medal1st color="red" height="36" width="36" />
<Medal1stSolid />
<IconoirProvider
:icon-props="{
'color': '#1E441E',
'stroke-width': 1,
'width': '2em',
'height': '2em',
}"
>
<Check />
</IconoirProvider>
</template>

View file

@ -1,4 +0,0 @@
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');

View file

@ -1,13 +0,0 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"]
}

View file

@ -1,11 +0,0 @@
{
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
],
"files": []
}

View file

@ -1,19 +0,0 @@
{
"extends": "@tsconfig/node22/tsconfig.json",
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"],
"noEmit": true
},
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*"
]
}

View file

@ -1,18 +0,0 @@
import { fileURLToPath, URL } from 'node:url';
import vue from '@vitejs/plugin-vue';
import { defineConfig } from 'vite';
import vueDevTools from 'vite-plugin-vue-devtools';
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
});

View file

@ -0,0 +1,16 @@
{
"root": true,
"extends": ["next/core-web-vitals"],
"rules": {
"react/no-unescaped-entities": ["off"]
},
"overrides": [
{
"files": ["*.js"],
"parser": "espree",
"parserOptions": {
"ecmaVersion": 2020
}
}
]
}

34
iconoir.com/README.md Normal file
View file

@ -0,0 +1,34 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

View file

@ -1,53 +1,51 @@
import React from 'react';
import styled from 'styled-components';
const AdContainer = styled.div`
#carbonads {
margin: 24px 0 0 0;
a {
text-decoration: none;
}
.carbon-wrap {
display: flex;
align-items: flex-start;
> :first-child {
margin-right: 12px;
}
& > a > img {
width: 100px;
height: 74px;
filter: grayscale(100%);
}
}
.carbon-text {
color: var(--black-80);
font-size: 14px;
}
.carbon-poweredby {
text-align: center;
font-size: 12px;
color: var(--black-40);
}
}
`;
export function Ad() {
const containerRef = React.useRef<HTMLDivElement>(null);
const addedScript = React.useRef(false);
React.useEffect(() => {
const container = containerRef.current;
if (container && !addedScript.current) {
addedScript.current = true;
const script = document.createElement('script');
script.async = true;
script.type = 'text/javascript';
script.src = '//cdn.carbonads.com/carbon.js?serve=CESDK5QJ&placement=iconoircom';
script.src =
'//cdn.carbonads.com/carbon.js?serve=CESDK5QJ&placement=iconoircom';
script.id = '_carbonads_js';
container.appendChild(script);
}
}, []);
return <AdContainer ref={containerRef} />;
}
const AdContainer = styled.div`
#carbonads {
margin: 24px 0 0 0;
a {
text-decoration: none !important;
}
.carbon-wrap {
display: flex;
align-items: flex-start;
> :first-child {
margin-right: 12px;
}
& > a > img {
width:100px;
height:74px;
filter: grayscale(100%);
}
}
.carbon-text {
color: var(--black-80);
font-size: 14px;
}
.carbon-poweredby {
text-align: center;
font-size: 12px;
color: var(--black-40);
}
}
`;

View file

@ -1,6 +1,7 @@
import anime from 'animejs';
import React from 'react';
// eslint-disable-next-line no-unused-vars
type SetInstances = (instances: anime.AnimeInstance[]) => void;
function playWithLines1(setInstances: SetInstances): anime.AnimeInstance[] {
@ -10,7 +11,7 @@ function playWithLines1(setInstances: SetInstances): anime.AnimeInstance[] {
strokeDashoffset: [anime.setDashoffset, 0],
easing: 'easeInOutSine',
duration: 1500,
delay(_el, i) {
delay: function (el, i) {
return i * 250;
},
direction: 'alternate',
@ -18,7 +19,6 @@ function playWithLines1(setInstances: SetInstances): anime.AnimeInstance[] {
}),
];
}
function playWithLines2(setInstances: SetInstances): anime.AnimeInstance[] {
return [
anime({
@ -26,7 +26,7 @@ function playWithLines2(setInstances: SetInstances): anime.AnimeInstance[] {
strokeDashoffset: [anime.setDashoffset, 0],
easing: 'easeInOutSine',
duration: 1500,
delay(_el, i) {
delay: function (el, i) {
return i * 250;
},
direction: 'alternate',
@ -34,7 +34,6 @@ function playWithLines2(setInstances: SetInstances): anime.AnimeInstance[] {
}),
];
}
function playWithLines3(setInstances: SetInstances): anime.AnimeInstance[] {
return [
anime({
@ -42,7 +41,7 @@ function playWithLines3(setInstances: SetInstances): anime.AnimeInstance[] {
strokeDashoffset: [anime.setDashoffset, 0],
easing: 'easeInOutSine',
duration: 1500,
delay(_el, i) {
delay: function (el, i) {
return i * 250;
},
direction: 'alternate',
@ -59,7 +58,6 @@ function playWithLines3(setInstances: SetInstances): anime.AnimeInstance[] {
}),
];
}
function playWithLines4(setInstances: SetInstances): anime.AnimeInstance[] {
return [
anime({
@ -67,7 +65,7 @@ function playWithLines4(setInstances: SetInstances): anime.AnimeInstance[] {
strokeDashoffset: [anime.setDashoffset, 0],
easing: 'easeInOutSine',
duration: 1500,
delay(_el, i) {
delay: function (el, i) {
return i * 250;
},
direction: 'alternate',
@ -78,19 +76,16 @@ function playWithLines4(setInstances: SetInstances): anime.AnimeInstance[] {
export function AnimatedSvg() {
const instancesRef = React.useRef<anime.AnimeInstance[] | null>(null);
React.useEffect(() => {
instancesRef.current = playWithLines1((instances) => {
instancesRef.current = instances;
});
return () => {
for (const instance of instancesRef.current || []) {
instance.pause();
}
};
}, []);
return (
<svg
className="playWithLines2"

View file

@ -1,4 +1,4 @@
import styled, { css, keyframes } from 'styled-components';
import styled, { keyframes } from 'styled-components';
import useResizeObserver from 'use-resize-observer';
import {
FEEDBACK_LINK,
@ -8,32 +8,86 @@ import {
import { media } from '../lib/responsive';
import { Text14 } from './Typography';
export function AvailableFor() {
const { ref, width } = useResizeObserver();
return (
<>
<MobileHeader>Available For</MobileHeader>
<AvailableForOuter>
<AvailableForContainer contentWidth={width || 0} ref={ref}>
<DesktopHeader>Available for</DesktopHeader>
<a href={LIBRARY_LINKS.React} target={'_blank'} rel={'noreferrer'}>
<AvailableForImage
src={'/logo-react.svg'}
alt={'React Logo'}
title={'React'}
/>
</a>
<a href={LIBRARY_LINKS.Flutter} target={'_blank'} rel={'noreferrer'}>
<AvailableForImage
src={'/logo-flutter.svg'}
alt={'Flutter Logo'}
title={'Flutter'}
/>
</a>
<a href={LIBRARY_LINKS.Figma} target={'_blank'} rel={'noreferrer'}>
<AvailableForImage
src={'/logo-figma.svg'}
alt={'Figma Logo'}
title={'Figma'}
/>
</a>
<a
href={LIBRARY_LINKS.ReactNative}
target={'_blank'}
rel={'noreferrer'}
>
<AvailableForImage
src={'/logo-react-native.svg'}
alt={'React Native Logo'}
title={'React Native'}
/>
</a>
<a href={LIBRARY_LINKS.Framer} target={'_blank'} rel={'noreferrer'}>
<AvailableForImage
src={'/logo-framer.svg'}
alt={'Framer Logo'}
title={'Framer'}
/>
</a>
<AreYouUsing>
<a href={SUGGEST_LIBRARY_LINK} target={'_blank'} rel={'noreferrer'}>
<Text14>More?</Text14>
</a>
<a href={FEEDBACK_LINK} target={'_blank'} rel={'noreferrer'}>
<Text14>Are you using the library?</Text14>
</a>
</AreYouUsing>
</AvailableForContainer>
</AvailableForOuter>
</>
);
}
const AreYouUsing = styled.div`
* {
white-space: nowrap;
}
`;
const MobileHeader = styled(Text14)`
&&& {
display: block;
margin-top: 5px;
text-align: center;
${media.lg} {
display: none;
}
}
`;
const DesktopHeader = styled(Text14)`
&&& {
display: block;
margin-top: 5px;
text-align: center;
${media.lg} {
display: none;
${media.lg} {
display: block;
}
}
`;
const DesktopHeader = styled(Text14)`
display: none;
${media.lg} {
display: block;
}
`;
const AvailableForAnimation = keyframes`
5% {
transform: translateX(0);
@ -48,7 +102,6 @@ const AvailableForAnimation = keyframes`
transform: translateX(0);
}
`;
const AvailableForOuter = styled.div`
max-width: 100vw;
margin: 16px -30px 70px -30px;
@ -59,19 +112,16 @@ const AvailableForOuter = styled.div`
padding: 0;
}
`;
const AvailableForContainer = styled.div<{ $contentWidth: number }>`
const AvailableForContainer = styled.div<{ contentWidth: number }>`
display: flex;
align-items: center;
justify-content: flex-start;
width: max-content;
--content-width: ${(props) => props.$contentWidth}px;
${(props) =>
props.$contentWidth
&& css`
animation: ${AvailableForAnimation} 40s cubic-bezier(0.37, 0, 0.63, 1)
infinite;
`}
--content-width: ${(props) => props.contentWidth}px;
${(props) => (props.contentWidth ? '&' : '&.noop')} {
animation: ${AvailableForAnimation} 40s cubic-bezier(0.37, 0, 0.63, 1)
infinite;
}
> :not(:last-child) {
margin-right: 30px;
}
@ -82,11 +132,10 @@ const AvailableForContainer = styled.div<{ $contentWidth: number }>`
}
${media.md} {
> :not(:last-child) {
margin-right: 40px;
margin-right: 60px;
}
}
`;
const AvailableForImage = styled.img`
height: 40px;
display: block;
@ -99,107 +148,3 @@ const AvailableForImage = styled.img`
height: 50px;
}
`;
export function AvailableFor() {
const { ref, width } = useResizeObserver();
return (
<>
<MobileHeader>Available For</MobileHeader>
<AvailableForOuter>
<AvailableForContainer $contentWidth={width || 0} ref={ref}>
<DesktopHeader>Available for</DesktopHeader>
<a href={LIBRARY_LINKS.React} target="_blank" rel="noreferrer">
<AvailableForImage
src="/logo-react.svg"
alt="React Logo"
title="React"
/>
</a>
<a
href="https://github.com/iconoir-icons/iconoir#swift-package"
target="_blank"
rel="nofollow noreferrer"
>
<AvailableForImage
src="/logo-swift.svg"
alt="Swift Logo"
title="Swift"
/>
</a>
<a
href={LIBRARY_LINKS.Flutter}
target="_blank"
rel="nofollow noreferrer"
>
<AvailableForImage
src="/logo-flutter.svg"
alt="Flutter Logo"
title="Flutter"
/>
</a>
<a
href={LIBRARY_LINKS.Figma}
target="_blank"
rel="nofollow noreferrer"
>
<AvailableForImage
src="/logo-figma.svg"
alt="Figma Logo"
title="Figma"
/>
</a>
<a
href={LIBRARY_LINKS.ReactNative}
target="_blank"
rel="nofollow noreferrer"
>
<AvailableForImage
src="/logo-react-native.svg"
alt="React Native Logo"
title="React Native"
/>
</a>
<a
href={LIBRARY_LINKS.Vue}
target="_blank"
rel="nofollow noreferrer"
>
<AvailableForImage
src="/logo-vue.svg"
alt="Vue Logo"
title="Vue"
/>
</a>
<a
href={LIBRARY_LINKS.Framer}
target="_blank"
rel="nofollow noreferrer"
>
<AvailableForImage
src="/logo-framer.svg"
alt="Framer Logo"
title="Framer"
/>
</a>
<AreYouUsing>
<a
href={SUGGEST_LIBRARY_LINK}
target="_blank"
rel="nofollow noreferrer"
>
<Text14>More?</Text14>
</a>
<a
href={FEEDBACK_LINK}
target="_blank"
rel="nofollow noreferrer"
>
<Text14>Are you using the library?</Text14>
</a>
</AreYouUsing>
</AvailableForContainer>
</AvailableForOuter>
</>
);
}

View file

@ -12,74 +12,68 @@ export const ResetButton = styled.button`
`;
export const LargeButton = styled(ResetButton)`
&&& {
background: var(--white);
height: 75px;
border-radius: 10px 50px 50px 50px;
border: solid 2px var(--g0);
background: var(--white);
height: 75px;
border-radius: 10px 50px 50px 50px;
border: solid 2px var(--g0);
box-shadow: 0px 8px 0px 0px var(--g0);
box-shadow: 0px 8px 0px 0px var(--g0);
display: inline-flex;
align-items: center;
text-decoration: none;
color: var(--g0);
padding: 0 70px;
font-size: 20px;
line-height: 26px;
display: inline-flex;
align-items: center;
text-decoration: none;
color: var(--g0);
padding: 0 70px;
font-size: 20px;
line-height: 26px;
font-weight: 700;
position: relative;
cursor: pointer;
z-index: 12;
> :not(:last-child) {
margin-right: 15px;
}
* {
font-weight: 700;
position: relative;
cursor: pointer;
z-index: 12;
> :not(:last-child) {
margin-right: 15px;
}
* {
font-weight: 700;
}
transition: 0.2s;
&:hover {
box-shadow: 0px 3px 0px 0px var(--g0);
transform: translateY(5px);
}
&:focus::after,
&:hover::after {
inset: -7px;
opacity: 1;
}
}
transition: 0.2s;
&:hover {
box-shadow: 0px 3px 0px 0px var(--g0);
transform: translateY(5px);
}
&:focus::after,
&:hover::after {
inset: -7px;
opacity: 1;
}
`;
const Button = styled(LargeButton)`
&&&& {
height: 40px;
font-size: 13px;
line-height: 21px;
padding: 0 18px;
&::after {
border-width: 2px;
}
&:focus::after,
&:hover::after {
inset: -4px;
}
&:active {
background: var(--darker-gray);
}
export const Button = styled(LargeButton)`
height: 40px;
font-size: 13px;
line-height: 21px;
padding: 0 18px;
&::after {
border-width: 2px;
}
&:focus::after,
&:hover::after {
inset: -4px;
}
&:active {
background: var(--darker-gray);
}
`;
export const CopyButton = styled(Button)`
&&&&& {
text-transform: uppercase;
background: var(--white);
height: 30px;
padding: 0 12px;
font-size: 11px;
letter-spacing: 0.12em;
line-height: 17.6px;
font-weight: 700;
color: var(--black);
font-family: var(--font-family);
}
`;
text-transform: uppercase;
background: var(--white);
height: 30px;
padding: 0 12px;
font-size: 11px;
letter-spacing: 0.12em;
line-height: 17.6px;
font-weight: 700;
color: var(--black);
font-family: var(--font-family) !important;
`;

View file

@ -2,6 +2,25 @@ import styled from 'styled-components';
import { media } from '../lib/responsive';
import { Text15 } from './Typography';
export interface CategoryRowProps {
category: string;
numIcons: number;
style?: any;
}
export function CategoryRow({ category, numIcons, style }: CategoryRowProps) {
return (
<Container style={style}>
<InnerContainer>
<Title>{category}</Title>
<Text15>
{numIcons} Icon{numIcons === 1 ? '' : 's'}
</Text15>
<Separator />
</InnerContainer>
</Container>
);
}
const InnerContainer = styled.div`
display: flex;
align-items: center;
@ -13,7 +32,6 @@ const InnerContainer = styled.div`
margin-right: 10px;
}
`;
const Container = styled.div`
display: flex;
align-items: flex-end;
@ -23,42 +41,15 @@ const Container = styled.div`
padding-bottom: 40px;
}
`;
const Title = styled(Text15)`
&&& {
font-weight: 700;
color: var(--g0);
background-color: var(--g6);
border-radius: 8px;
padding: 6px 10px;
}
font-weight: 700;
color: var(--g0);
background-color: var(--g6);
border-radius: 8px;
padding: 6px 10px;
`;
const Separator = styled.div`
height: 1px;
flex: 1;
background: var(--g6);
`;
export interface CategoryRowProps {
category: string;
numIcons: number;
style?: any;
}
export function CategoryRow({ category, numIcons, style }: CategoryRowProps) {
return (
<Container style={style}>
<InnerContainer>
<Title>{category}</Title>
<Text15>
{numIcons}
{' '}
Icon
{numIcons === 1 ? '' : 's'}
</Text15>
<Separator />
</InnerContainer>
</Container>
);
}

View file

@ -1,97 +1,21 @@
import type { MDXRemoteSerializeResult } from 'next-mdx-remote';
import { BoxIso } from 'iconoir-react';
import moment from 'moment';
import { MDXRemoteSerializeResult } from 'next-mdx-remote';
import React from 'react';
import styled from 'styled-components';
import { media } from '../lib/responsive';
import { CopyButton } from './Button';
import { MDXRemote } from './MDXRemote';
import { media } from '../lib/responsive';
import { Code, Text15, Text18 } from './Typography';
import { CopyButton } from './Button';
const EXPAND_HEIGHT = 400;
const Container = styled.div`
margin: 40px 0;
display: flex;
align-items: stretch;
flex-direction: column;
width: 100%;
${media.lg} {
flex-direction: row;
align-items: flex-start;
margin: 24px 0;
}
`;
const ContainerLeft = styled.div`
display: flex;
align-items: flex-start;
margin-bottom: 12px;
${media.lg} {
margin-bottom: 0;
margin-right: 30px;
}
`;
const ContainerIcon = styled.div`
font-size: 18px;
color: var(--black);
margin-right: 18px;
`;
const TitleContainer = styled.div`
width: 100px;
`;
const EntryTitle = styled(Text18)`
&&& {
color: var(--black);
font-weight: 700;
}
`;
const ExpandContainer = styled.div`
position: absolute;
bottom: 16px;
right: 23px;
`;
const EntryBody = styled(Code)<{ $expanded?: boolean }>`
&&& {
flex: 1;
margin: 0;
max-height: ${(props) => (props.$expanded ? 'none' : `${EXPAND_HEIGHT}px`)};
position: relative;
overflow: hidden;
* {
font-family: var(--code-family);
}
ul {
list-style: none none;
margin: 0;
padding: 0;
li {
margin-bottom: 8px;
}
}
code {
display: inline-block;
background: var(--g5);
color: var(--black);
font-family: var(--font-family) !important;
padding: 0 4px;
font-size: 18px;
}
}
`;
export interface ChangelogEntryProps {
name: string;
url: string;
created_at: string;
body?: MDXRemoteSerializeResult;
}
export function ChangelogEntry({
name,
url,
@ -101,16 +25,14 @@ export function ChangelogEntry({
const [expanded, setExpanded] = React.useState(false);
const [shouldExpand, setShouldExpand] = React.useState(false);
const containerRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
if (
containerRef.current
&& containerRef.current.clientHeight > EXPAND_HEIGHT
containerRef.current &&
containerRef.current.clientHeight > EXPAND_HEIGHT
) {
setShouldExpand(true);
}
}, []);
return (
<Container ref={containerRef}>
<ContainerLeft>
@ -120,8 +42,8 @@ export function ChangelogEntry({
<TitleContainer>
<a
href={url}
target="_blank"
rel="noreferrer"
target={'_blank'}
rel={'noreferrer'}
style={{ textDecoration: 'none' }}
>
<EntryTitle>{name}</EntryTitle>
@ -129,18 +51,81 @@ export function ChangelogEntry({
<Text15>{moment(created_at).format('MMM DD, YYYY')}</Text15>
</TitleContainer>
</ContainerLeft>
<EntryBody $expanded={expanded}>
<EntryBody expanded={expanded}>
{body ? <MDXRemote {...body} /> : 'No changelog'}
{shouldExpand
? (
<ExpandContainer>
<CopyButton onClick={() => setExpanded((e) => !e)}>
{expanded ? 'Collapse' : 'Expand'}
</CopyButton>
</ExpandContainer>
)
: null}
{shouldExpand ? (
<ExpandContainer>
<CopyButton onClick={() => setExpanded((e) => !e)}>
{expanded ? 'Collapse' : 'Expand'}
</CopyButton>
</ExpandContainer>
) : null}
</EntryBody>
</Container>
);
}
const Container = styled.div`
margin: 40px 0 !important;
display: flex;
align-items: stretch;
flex-direction: column;
width: 100%;
${media.lg} {
flex-direction: row;
align-items: flex-start;
margin: 24px 0 !important;
}
`;
const ContainerLeft = styled.div`
display: flex;
align-items: flex-start;
margin-bottom: 12px;
${media.lg} {
margin-bottom: 0;
margin-right: 30px;
}
`;
const ContainerIcon = styled.div`
font-size: 18px;
color: var(--black);
margin-right: 18px;
`;
const TitleContainer = styled.div`
width: 100px;
`;
const EntryTitle = styled(Text18)`
color: var(--black);
font-weight: 700;
`;
const ExpandContainer = styled.div`
position: absolute;
bottom: 16px;
right: 23px;
`;
const EntryBody = styled(Code)<{ expanded?: boolean }>`
flex: 1;
margin: 0;
max-height: ${(props) => (props.expanded ? 'none' : `${EXPAND_HEIGHT}px`)};
position: relative;
overflow: hidden;
* {
font-family: var(--code-family);
}
ul {
list-style: none none;
margin: 0;
padding: 0;
li {
margin-bottom: 8px;
}
}
code {
display: inline-block;
background: var(--g5) !important;
color: var(--black);
font-family: var(--font-family) !important;
padding: 0 4px;
font-size: 18px !important;
}
`;

View file

@ -2,8 +2,20 @@ import Link from 'next/link';
import styled from 'styled-components';
import { Text13 } from './Typography';
export interface CurrentVersionProps {
version: string;
}
export function CurrentVersion({ version }: CurrentVersionProps) {
return (
<Link href={'/docs/changelog'} passHref legacyBehavior>
<Container as={'a'}>
{version}
</Container>
</Link>
);
}
const Container = styled(Text13)`
&&& {
color: var(--g1);
font-weight: 700;
background: var(--g5);
@ -11,25 +23,10 @@ const Container = styled(Text13)`
padding: 7px 16px;
border-radius: 200px;
display: block;
text-decoration: none;
transition:
color 0.1s linear,
background 0.1s linear;
text-decoration: none !important;
transition: color 0.1s linear, background 0.1s linear;
&:hover {
background: var(--black);
background: var(--black) !important;
color: var(--white);
}
}
`;
export interface CurrentVersionProps {
version: string;
}
export function CurrentVersion({ version }: CurrentVersionProps) {
return (
<Link href="/docs/changelog" passHref legacyBehavior>
<Container as="a">{version}</Container>
</Link>
);
}

View file

@ -1,62 +1,17 @@
import type { IconListCustomizations } from './IconList';
import React from 'react';
import styled from 'styled-components';
import { media } from '../lib/responsive';
import { DEFAULT_CUSTOMIZATIONS } from './IconList';
import { Button } from './Button';
import { DEFAULT_CUSTOMIZATIONS, IconListCustomizations } from './IconList';
import { ColorButton, ColorInput } from './Input';
import { Slider } from './Slider';
import { Text13, Text15 } from './Typography';
const CustomizationBox = styled.div`
background-color: var(--g7);
width: 84%;
padding: 8%;
border-radius: 10px;
margin: 24px 0;
display: none;
${media.md} {
display: block;
}
`;
const Header = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 30px;
border-bottom: solid 1px var(--g6);
padding-bottom: 10px;
`;
const Field = styled.div`
margin-bottom: 18px;
`;
const HorizontalField = styled(Field)`
display: flex;
align-items: center;
justify-content: space-between;
`;
const ResetButton = styled(Field)`
&&& {
margin: initial;
text-decoration: underline;
color: var(--dark-gray);
font-size: 13px;
&:hover {
color: var(--black);
cursor: pointer;
}
}
`;
import { media } from '../lib/responsive';
export interface CustomizationEditorProps {
customizations: IconListCustomizations;
// eslint-disable-next-line no-unused-vars
onChange: (customizations: IconListCustomizations) => void;
}
export function CustomizationEditor({
customizations,
onChange,
@ -64,11 +19,9 @@ export function CustomizationEditor({
const [, startTransition] = (React as any).useTransition();
const [color, setColor] = React.useState(customizations.hexColor);
const [size, setSize] = React.useState(customizations.size);
const [strokeWidth, setStrokeWidth] = React.useState(
customizations.strokeWidth,
customizations.strokeWidth
);
React.useEffect(() => {
setColor(customizations.hexColor);
setSize(customizations.size);
@ -86,55 +39,93 @@ export function CustomizationEditor({
return (
<>
<CustomizationBox>
<Header>
<Text15 style={{ fontWeight: 700, color: 'var(--black)' }}>
Customize
</Text15>
<ResetButton onClick={() => onChange(DEFAULT_CUSTOMIZATIONS)}>
Reset
</ResetButton>
</Header>
<Field>
<Slider
label="Optical Size"
minValue={16}
maxValue={64}
value={[size]}
formatOptions={{ maximumFractionDigits: 0 }}
onChange={(values) => {
setSize(values[0]);
updateCustomizations({ size: values[0] });
}}
/>
</Field>
<Field>
<Slider
label="Stroke Weight"
minValue={0.5}
maxValue={3}
value={[strokeWidth]}
step={0.1}
formatOptions={{ maximumFractionDigits: 1 }}
onChange={(values) => {
setStrokeWidth(values[0]);
updateCustomizations({ strokeWidth: values[0] });
}}
/>
</Field>
<HorizontalField>
<Text13>Color</Text13>
<ColorInput
type="color"
value={color}
onChange={(e) => {
setColor(e.target.value);
updateCustomizations({ hexColor: e.target.value });
}}
/>
<ColorButton />
</HorizontalField>
</CustomizationBox>
<CustomizationBox>
<Header>
<Text15 style={{ fontWeight: 700, color: 'var(--black)' }}>
Customize
</Text15>
<ResetButton onClick={() => onChange(DEFAULT_CUSTOMIZATIONS)}>Reset</ResetButton>
</Header>
<Field>
<Slider
label={'Optical Size'}
minValue={16}
maxValue={64}
value={[size]}
formatOptions={{ maximumFractionDigits: 0 }}
onChange={(values) => {
setSize(values[0]);
updateCustomizations({ size: values[0] });
}}
/>
</Field>
<Field>
<Slider
label={'Stroke Weight'}
minValue={0.5}
maxValue={3}
value={[strokeWidth]}
step={0.1}
formatOptions={{ maximumFractionDigits: 1 }}
onChange={(values) => {
setStrokeWidth(values[0]);
updateCustomizations({ strokeWidth: values[0] });
}}
/>
</Field>
<HorizontalField>
<Text13>Color</Text13>
<ColorInput
type={'color'}
value={color}
onChange={(e) => {
setColor(e.target.value);
updateCustomizations({ hexColor: e.target.value });
}}
/>
<ColorButton />
</HorizontalField>
</CustomizationBox>
</>
);
}
const CustomizationBox = styled.div`
background-color:var(--g7);
width: 84%;
padding: 8%;
border-radius: 10px;
margin: 24px 0;
display: none;
${media.md} {
display: block;
}
`;
const Header = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 30px;
border-bottom: solid 1px var(--g6);
padding-bottom: 10px;
`;
const Field = styled.div`
margin-bottom: 24px;
`;
const HorizontalField = styled(Field)`
display: flex;
align-items: center;
justify-content: space-between;
`;
const ResetButton = styled(Field)`
margin: initial;
text-decoration: underline;
color: var(--dark-gray);
font-size: 13px;
&:hover{
color: var(--black);
cursor: pointer;
}
`;

View file

@ -1,103 +1,15 @@
import type { DocumentationItem } from '../pages/docs/[...slug]';
import { NavArrowUp } from 'iconoir-react';
import Link from 'next/link';
import { useRouter } from 'next/router';
import React from 'react';
import styled from 'styled-components';
import { DocumentationItem } from '../pages/docs/[...slug]';
import { media } from '../lib/responsive';
const HeaderItemIcon = styled.div<{ $active?: boolean }>`
font-size: 13px;
transition: transform 0.25s linear;
transform: rotate(${(props) => (props.$active ? 180 : 0)}deg);
margin-right: 7px;
position: relative;
top: 6px;
svg {
display: block;
}
${media.lg} {
display: none;
}
`;
const ChildrenContainer = styled.div<{ $expanded?: boolean }>`
display: ${(props) => (props.$expanded ? 'block' : 'none')};
${media.lg} {
display: block;
}
`;
const HeaderItem = styled.div`
padding: 10px 30px;
font-size: 15px;
line-height: 19px;
color: var(--g0);
font-weight: 700;
display: flex;
align-items: baseline;
cursor: pointer;
${media.lg} {
padding: 22px 45px;
cursor: default;
pointer-events: none;
&:not(:first-child) {
margin-top: 10px;
}
}
`;
const NavigationItem = styled.div<{ $active?: boolean }>`
padding: 12px 12px 12px 75px;
transition:
background 0.1s linear,
color 0.1s linear;
font-size: 16px;
line-height: 14.5px;
letter-spacing: -0.02em;
color: var(--g1);
display: flex;
align-items: center;
text-decoration: none;
span {
font-weight: 400;
}
> :not(:last-child) {
margin-right: 14px;
}
&:hover,
${(props) => (props.$active ? '&' : '&.noop')} {
color: var(--g0);
text-decoration: underline;
}
${(props) => (props.$active ? 'span' : '&.noop')} {
font-weight: 500;
}
${media.lg} {
padding: 12px 12px 12px 65px;
}
`;
const NavigationItemLabel = styled.span`
display: flex;
align-items: center;
justify-content: center;
padding: 2px 6px;
border-radius: 4px;
font-size: 11px;
line-height: 17.6px;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--g1);
background: var(--g5);
`;
export interface DocumentationNavigationProps {
documentationItems: DocumentationItem[];
pathPrefix?: string[];
}
export function DocumentationNavigation({
documentationItems,
pathPrefix,
@ -105,53 +17,46 @@ export function DocumentationNavigation({
const router = useRouter();
const activePath = router.asPath.replace('/docs/', '');
const [expandedTitles, setExpandedTitles] = React.useState<string[]>([]);
React.useEffect(() => {
const expandedItems = documentationItems.filter((item) => {
const normalized = activePath.replace((pathPrefix || []).join('/'), '');
return (
normalized === item.path
|| item.children?.some((child) => {
normalized === item.path ||
item.children?.some((child) => {
return activePath.startsWith(
[item.path, child.path].filter(Boolean).join('/'),
[item.path, child.path].filter(Boolean).join('/')
);
})
);
});
setExpandedTitles(expandedItems.map((item) => item.title));
}, [activePath, pathPrefix, documentationItems]);
return (
<>
{documentationItems.map((documentationItem) => {
const path = [...(pathPrefix || []), documentationItem.path]
.filter(Boolean)
.join('/');
if (documentationItem.children?.length) {
const active = expandedTitles.includes(documentationItem.title);
return (
<React.Fragment key={documentationItem.title}>
<HeaderItem
onClick={() => {
setExpandedTitles((et) => {
const includes = et.includes(documentationItem.title);
return includes
? et.filter((i) => i !== documentationItem.title)
: [...et, documentationItem.title];
});
}}
>
<HeaderItemIcon $active={active}>
<HeaderItemIcon active={active}>
<NavArrowUp />
</HeaderItemIcon>
{documentationItem.title}
</HeaderItem>
<ChildrenContainer $expanded={active}>
<ChildrenContainer expanded={active}>
<DocumentationNavigation
documentationItems={documentationItem.children}
pathPrefix={[
@ -170,15 +75,13 @@ export function DocumentationNavigation({
legacyBehavior
key={documentationItem.path}
>
<NavigationItem as="a" $active={activePath === path}>
<NavigationItem as={'a'} active={activePath === path}>
<span>{documentationItem.title}</span>
{documentationItem.label
? (
<NavigationItemLabel>
{documentationItem.label}
</NavigationItemLabel>
)
: null}
{documentationItem.label ? (
<NavigationItemLabel>
{documentationItem.label}
</NavigationItemLabel>
) : null}
</NavigationItem>
</Link>
);
@ -187,3 +90,85 @@ export function DocumentationNavigation({
</>
);
}
const HeaderItemIcon = styled.div<{ active?: boolean }>`
font-size: 13px;
transition: transform 0.25s linear;
transform: rotate(${(props) => (props.active ? 180 : 0)}deg);
margin-right: 7px;
position: relative;
top: 6px;
svg {
display: block;
}
${media.lg} {
display: none;
}
`;
const ChildrenContainer = styled.div<{ expanded?: boolean }>`
display: ${(props) => (props.expanded ? 'block' : 'none')};
${media.lg} {
display: block;
}
`;
const HeaderItem = styled.div`
padding: 10px 30px;
font-size: 15px;
line-height: 19px;
color: var(--g0);
font-weight: 700;
display: flex;
align-items: baseline;
cursor: pointer;
${media.lg} {
padding: 22px 45px;
cursor: default;
pointer-events: none;
&:not(:first-child) {
margin-top: 10px;
}
}
`;
const NavigationItem = styled.div<{ active?: boolean }>`
padding: 12px 45px 12px 75px;
transition: background 0.1s linear, color 0.1s linear;
font-weight: 500;
font-size: 16px;
line-height: 14.5px;
letter-spacing: -0.02em;
color: var(--g1);
display: flex;
align-items: center;
text-decoration: none;
span {
font-weight: 500;
}
> :not(:last-child) {
margin-right: 14px;
}
&:hover,
${(props) => (props.active ? '&' : '&.noop')} {
color: var(--g0);
text-decoration: underline;
}
${(props) => (props.active ? 'span' : '&.noop')} {
font-weight: 700;
}
${media.lg} {
padding: 12px 45px 12px 65px;
}
`;
const NavigationItemLabel = styled.span`
display: flex;
align-items: center;
justify-content: center;
padding: 2px 6px;
border-radius: 4px;
font-size: 11px;
line-height: 17.6px;
font-weight: 700 !important;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--g1);
background: var(--g5);
`;

View file

@ -1,96 +0,0 @@
import { Sparks } from 'iconoir-react';
import React, { useEffect, useRef, useState } from 'react';
import styled from 'styled-components';
import { media } from '../lib/responsive';
const Text = styled.span`
color: #ffffffba;
`;
const PopupContent = styled.div`
display: none;
${media.lg} {
display: flex;
align-items: center;
justify-content: center;
padding: 14px 0;
background: #8330c6;
color: var(--white);
font-weight: 500;
font-size: 15px;
position: fixed;
top: 16px;
left: 16px;
right: 16px;
border-radius: 8px;
z-index: 9999;
margin: auto;
}
> * {
margin: 0 4px;
}
> a {
color: var(--white);
opacity: 1;
text-decoration: underline;
}
`;
const CloseButton = styled.span`
color: var(--white);
float: right;
font-size: 28px;
font-weight: bold;
&:hover,
&:focus {
color: black;
text-decoration: none;
cursor: pointer;
}
`;
export function DonationPopup() {
const [isVisible, setIsVisible] = useState(false);
const isInitialMount = useRef(true);
useEffect(() => {
if (isInitialMount.current) {
isInitialMount.current = false;
const isReturningUser = localStorage.getItem('returningUser');
const isReturningUserAge = localStorage.getItem('returningUserAge');
if (isReturningUser === 'true' && isReturningUserAge === 'false') {
setTimeout(() => setIsVisible(true), 15000);
localStorage.setItem('returningUserAge', 'true');
} else {
localStorage.setItem('returningUser', 'true');
localStorage.setItem('returningUserAge', 'false');
}
}
}, []);
const handleClose = () => {
setIsVisible(false);
};
return (
<>
{isVisible && (
<PopupContent>
<Sparks></Sparks>
<Text>
Your one-time or recurring contribution does a lot to keep Iconoir
going.
</Text>
<a
href="https://opencollective.com/iconoir/donate?interval=month&amount=10"
target="_blank"
>
Support the project!
</a>
<CloseButton onClick={handleClose}>&times;</CloseButton>
</PopupContent>
)}
</>
);
}

View file

@ -1,72 +1,24 @@
import type { Icon, IconListFilters } from './IconList';
import { IconoirProvider } from 'iconoir-react';
import React from 'react';
import styled from 'styled-components';
import { media } from '../lib/responsive';
import { Ad } from './Ad';
import { Sponsor } from './Sponsor';
import { CustomizationEditor } from './CustomizationEditor';
import { FiltersEditor } from './FiltersEditor';
import { IconList } from './IconList';
import { Streamline } from './Streamline';
import { Icon, IconList, IconListFilters } from './IconList';
import { media } from '../lib/responsive';
import { useCustomizationPersistence } from './useCustomizationPersistence';
const Container = styled.div`
display: flex;
flex-direction: column-reverse;
${media.md} {
align-items: flex-start;
flex-direction: row;
}
`;
const Left = styled.div`
flex: 1;
min-height: calc(100vh - 100px);
background: white;
${media.md} {
background: none;
}
`;
const Right = styled.div`
position: sticky;
top: 20px;
width: 275px;
display: block;
z-index: -1;
margin: 110px auto;
${media.md} {
margin-left: 68px;
z-index: 1;
}
`;
const FilterContainer = styled.div<{ $isMobile?: boolean }>`
display: ${(props) => (props.$isMobile ? 'block' : 'none')};
margin-bottom: 40px;
position: sticky;
top: 20px;
z-index: 100;
width: 100%;
${media.md} {
position: relative;
top: 0;
display: ${(props) => (props.$isMobile ? 'none' : 'block')};
margin-bottom: 10px;
}
`;
export interface ExploreProps {
allIcons: Icon[];
}
export function Explore({ allIcons }: ExploreProps) {
const [filters, setFilters] = React.useState<IconListFilters>({});
const [customizations, setCustomizations] = useCustomizationPersistence();
return (
<Container>
<Left>
<FilterContainer $isMobile>
<FilterContainer isMobile>
<FiltersEditor filters={filters} onChange={setFilters} />
</FilterContainer>
<IconoirProvider
@ -86,7 +38,8 @@ export function Explore({ allIcons }: ExploreProps) {
<FilterContainer>
<FiltersEditor filters={filters} onChange={setFilters} />
</FilterContainer>
<Streamline />
<Sponsor />
<Ad />
<CustomizationEditor
customizations={customizations}
onChange={setCustomizations}
@ -95,3 +48,45 @@ export function Explore({ allIcons }: ExploreProps) {
</Container>
);
}
const Container = styled.div`
display: flex;
flex-direction: column-reverse;
${media.md} {
align-items: flex-start;
flex-direction: row;
}
`;
const Left = styled.div`
flex: 1;
min-height: calc(100vh - 100px);
background: white;
${media.md} {
background: none;
}
`;
const Right = styled.div`
position: sticky;
top: 50px;
width: 275px;
margin-left: 68px;
display: block;
z-index: -1;
${media.md} {
z-index: 1;
}
`;
const FilterContainer = styled.div<{ isMobile?: boolean }>`
display: ${(props) => (props.isMobile ? 'block' : 'none')};
margin-bottom: 40px;
position: sticky;
top: 20px;
z-index: 100;
width: 100%;
${media.md} {
position: relative;
top: 0;
display: ${(props) => (props.isMobile ? 'none' : 'block')};
margin-bottom: 10px;
}
`;

View file

@ -1,12 +1,12 @@
import type { IconListFilters } from './IconList';
import React from 'react';
import { IconListFilters } from './IconList';
import { LargeInput } from './Input';
export interface FiltersEditorProps {
filters: IconListFilters;
// eslint-disable-next-line no-unused-vars
onChange: (filters: IconListFilters) => void;
}
export function FiltersEditor({ filters, onChange }: FiltersEditorProps) {
const [, startTransition] = (React as any).useTransition();
const [search, setSearch] = React.useState(filters.search);
@ -14,15 +14,12 @@ export function FiltersEditor({ filters, onChange }: FiltersEditorProps) {
// Keep track if the user hits tab before scrolling, so we can scroll the search
// field to the top of the page automatically.
const didScrollRef = React.useRef(false);
React.useEffect(() => {
const scrollEvent = () => {
didScrollRef.current = true;
window.removeEventListener('scroll', scrollEvent);
};
window.addEventListener('scroll', scrollEvent);
return () => {
window.removeEventListener('scroll', scrollEvent);
};
@ -43,10 +40,10 @@ export function FiltersEditor({ filters, onChange }: FiltersEditorProps) {
return (
<LargeInput
placeholder="Search..."
placeholder={'Search...'}
value={search}
type="search"
autoCapitalize="none"
type={'search'}
autoCapitalize={'none'}
tabIndex={1}
onFocus={(e) => {
if (!didScrollRef.current) {

View file

@ -1,20 +1,69 @@
import { PeaceHand } from 'iconoir-react';
import styled from 'styled-components';
import {
GITHUB_LINK,
ISSUE_LINK,
LICENSE_LINK,
PRIVACY_LINK,
SUPPORT_LINK,
} from '../lib/constants';
import { LICENSE_LINK } from '../lib/constants';
import { Logo, LogoContainer, LogoIcon } from './Header';
import { NavigationItemContainer } from './NavigationItem';
import { Text13, Text17 } from './Typography';
export interface FooterCategoryProps {
category: string;
links: { name: string, url: string }[];
}
function FooterCategory({ category, links }: FooterCategoryProps) {
return (
<FooterCategoryContainer>
<FooterCategoryTitle>{category}</FooterCategoryTitle>
<FooterCategoryLinks>
{links.map((link) => (
<FooterCategoryLink key={link.url} href={link.url}>{link.name}</FooterCategoryLink>
))}
</FooterCategoryLinks>
</FooterCategoryContainer>
);
}
export function Footer() {
return (
<Container>
<LogoContainer>
<LogoIcon>
<PeaceHand />
</LogoIcon>
<Logo src={'/iconoir-logo.svg'} alt={'Iconoir Logo'} />
</LogoContainer>
<FooterCategories>
<FooterCategory category={'Project'} links={[
{ name: 'Our Mission', url:'/support' },
{ name: 'Contribute', url:'/docs/contributing' },
{ name: 'Donate', url:'https://opencollective.com/iconoir/donate' }
]} />
<FooterCategory category={'Support'} links={[
{ name: 'License', url:'https://github.com/iconoir-icons/iconoir/blob/main/LICENSE' },
{ name: 'GitHub Repository', url:'https://github.com/iconoir-icons/iconoir' },
{ name: 'File a Request', url:'https://github.com/iconoir-icons/iconoir/issues/new/choose' }
]} />
<FooterCategory category={'Developers'} links={[
{ name: 'Changelog',url:'/docs/changelog' },
{ name: 'React and React Native', url:'/docs/packages/iconoir-react' },
{ name: 'Flutter', url:'/docs/packages/iconoir-flutter'} ,
{ name: 'Framer and Figma', url:'/docs/packages/framer' },
{ name: 'CSS', url:'/docs/packages/css' }
]} />
</FooterCategories>
<FooterEnd>
<Text13 style={{ fontWeight: 400 }}>Parts of this content are &copy;2020-2023 by individual Iconoir contributors. Content available under a <a href={'https://github.com/iconoir-icons/iconoir/blob/main/LICENSE'} target={'_blank'} rel={'noreferrer'}>MIT License</a>.</Text13>
<Text13 style={{ fontWeight: 400 }}><a href={'https://www.freeprivacypolicy.com/live/ba00d743-a0cd-44f8-8cb5-6f58911db0fb'} target={'_blank'} rel={'noreferrer'}>Privacy</a></Text13>
</FooterEnd>
</Container>
);
}
const Container = styled.div`
display: block;
margin-top: 110px;
padding-top: 30px;
margin-top: 100px;
margin: 100px -50px -50px -50px; /* not ideal. To fix removing padding from body.*/
padding: 84px 12%;
background-color: var(--g7);
align-items: center;
@ -22,7 +71,6 @@ const Container = styled.div`
margin-right: 50px;
}
`;
const FooterEnd = styled.div`
border-top: 1px solid var(--g5);
padding-top: 20px;
@ -31,29 +79,24 @@ const FooterEnd = styled.div`
display: flex;
justify-content: space-between;
`;
const FooterCategories = styled.div`
width: 100%;
margin-top: 54px;
display: flex;
`;
const FooterCategoryContainer = styled.div`
width: 28%;
margin-right: 20px;
`;
const FooterCategoryTitle = styled(Text17)`
&&& {
margin-bottom: 24px;
font-weight: 600;
display: block;
color: var(--g0);
}
margin-bottom: 24px;
font-weight: 600;
display: block;
color: var(--g0);
`;
const FooterCategoryLinks = styled.div`
const FooterCategoryLinks = styled.div``;
`;
const FooterCategoryLink = styled.a`
display: block;
font-size: 17px;
@ -61,105 +104,8 @@ const FooterCategoryLink = styled.a`
width: fit-content;
margin-bottom: 12px;
text-decoration: none;
&:hover {
&:hover{
text-decoration: underline;
color: var(--g0);
}
`;
interface FooterCategoryProps {
category: string;
links: { name: string; url: string }[];
}
function FooterCategory({ category, links }: FooterCategoryProps) {
return (
<FooterCategoryContainer>
<FooterCategoryTitle>{category}</FooterCategoryTitle>
<FooterCategoryLinks>
{links.map((link) => (
<FooterCategoryLink key={link.url} href={link.url}>
{link.name}
</FooterCategoryLink>
))}
</FooterCategoryLinks>
</FooterCategoryContainer>
);
}
export function Footer() {
const year = new Date().getFullYear();
return (
<Container>
<LogoContainer>
<LogoIcon>
<PeaceHand />
</LogoIcon>
<Logo src="/iconoir-logo.svg" alt="Iconoir Logo" />
</LogoContainer>
<FooterCategories>
<FooterCategory
category="Project"
links={[
{ name: 'Our Mission', url: '/support' },
{ name: 'Contribute', url: '/docs/contributing' },
{
name: 'Donate',
url: SUPPORT_LINK,
},
]}
/>
<FooterCategory
category="Support"
links={[
{
name: 'License',
url: LICENSE_LINK,
},
{
name: 'GitHub Repository',
url: GITHUB_LINK,
},
{
name: 'File a Request',
url: ISSUE_LINK,
},
]}
/>
<FooterCategory
category="Developers"
links={[
{ name: 'Changelog', url: '/docs/changelog' },
{
name: 'React and React Native',
url: '/docs/packages/iconoir-react',
},
{ name: 'Flutter', url: '/docs/packages/iconoir-flutter' },
{ name: 'Framer and Figma', url: '/docs/packages/framer' },
{ name: 'CSS', url: '/docs/packages/css' },
]}
/>
</FooterCategories>
<FooterEnd>
<Text13 style={{ fontWeight: 400 }}>
Parts of this content are &copy;2020-
{year}
{' '}
by individual Iconoir
contributors. Content available under a
{' '}
<a href={LICENSE_LINK} target="_blank" rel="nofollow noreferrer">
MIT License
</a>
.
</Text13>
<Text13 style={{ fontWeight: 400 }}>
<a href={PRIVACY_LINK} target="_blank" rel="nofollow noreferrer">
Privacy
</a>
</Text13>
</FooterEnd>
</Container>
);
}

View file

@ -5,9 +5,9 @@ export function GA() {
<>
<Script
src="https://www.googletagmanager.com/gtag/js?id=UA-33344001-9"
strategy="afterInteractive"
strategy={'afterInteractive'}
/>
<Script id="google-analytics" strategy="afterInteractive">
<Script id={'google-analytics'} strategy={'afterInteractive'}>
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}

View file

@ -1,28 +1,62 @@
import { Discord, Menu, Sparks, Xmark } from 'iconoir-react';
import { Heart } from 'iconoir-react/solid';
import { Cancel, Heart, Menu } from 'iconoir-react';
import Link from 'next/link';
import React from 'react';
import styled, { css } from 'styled-components';
import { DISCORD_LINK, SHARE_LINK } from '../lib/constants';
import { media } from '../lib/responsive';
import styled from 'styled-components';
import { AnimatedSvg } from './AnimatedSvg';
import { ResetButton } from './Button';
import { SHARE_LINK } from '../lib/constants';
import { CurrentVersion } from './CurrentVersion';
import { NavigationItem } from './NavigationItem';
import { NavigationItem, NavigationItemContainer } from './NavigationItem';
import { media } from '../lib/responsive';
import { Text15 } from './Typography';
const StyledDiscord = styled(Discord)<{ $isMobile?: boolean }>`
display: none;
${media.lg} {
display: flex;
margin: 0 0 0 16px;
&:hover {
scale: 1.1;
transition: 0.2s;
color: #7289da;
}
}
`;
export interface HeaderProps {
currentVersion: string;
}
export function Header({ currentVersion }: HeaderProps) {
const [menuVisible, setMenuVisible] = React.useState(false);
return (
<Container>
<HeaderLeft>
<Link href={'/'}>
<LogoContainer>
<LogoIcon>
<AnimatedSvg />
</LogoIcon>
<Logo src={'/iconoir-logo.svg'} alt={'Iconoir Logo'} />
</LogoContainer>
</Link>
<CurrentVersion version={currentVersion} />
</HeaderLeft>
<HeaderCenter>
<MobileMenuContainer visible={menuVisible}>
<NavigationItem href={'/'}>Icons</NavigationItem>
<NavigationItem href={'/docs'}>Documentation</NavigationItem>
<NavigationItem href={'/support'} style={{ marginRight: 0 }}>
Donate &mdash; Our Mission
</NavigationItem>
<BuiltWith isMobile>
Share with <Heart width={'1em'} height={'1em'} /> on{' '}
<a href={SHARE_LINK} target={'_blank'} rel={'noreferrer'}>
Twitter
</a>{' '}
</BuiltWith>
</MobileMenuContainer>
</HeaderCenter>
<HeaderRight>
<BuiltWith>
Share with <Heart width={'1em'} height={'1em'} /> on{' '}
<a href={SHARE_LINK} target={'_blank'} rel={'noreferrer'}>
Twitter
</a>{' '}
</BuiltWith>
<MobileMenuButton onClick={() => setMenuVisible((v) => !v)}>
{menuVisible ? <Cancel /> : <Menu />}
</MobileMenuButton>
</HeaderRight>
</Container>
);
}
export const LogoContainer = styled.div`
position: relative;
@ -30,51 +64,27 @@ export const LogoContainer = styled.div`
display: inline-flex;
align-items: center;
transition: 0.1s;
&:hover {
&:hover{
scale: 1.1;
transition: 0.2s;
}
`;
const Banner = styled(Text15)`
display: none;
${media.lg} {
display: flex;
align-items: center;
justify-content: center;
padding: 16px 0;
background: var(--g5);
color: var(--g0);
font-weight: 500;
position: absolute;
width: 100%;
text-decoration: underline;
top: 0;
}
> * {
margin: 0 4px;
}
`;
const MobileMenuButton = styled(ResetButton)`
&&& {
z-index: 101;
color: var(--black);
background: transparent;
display: inline-block;
margin-left: auto;
cursor: pointer;
svg {
width: 24px;
height: 24px;
}
${media.lg} {
display: none;
}
z-index: 101;
color: var(--black);
background: transparent;
display: inline-block;
margin-left: auto !important;
cursor: pointer;
svg {
width: 24px;
height: 24px;
}
${media.lg} {
display: none;
}
`;
const MobileMenuContainer = styled.div<{ $visible?: boolean }>`
const MobileMenuContainer = styled.div<{ visible?: boolean }>`
position: absolute;
top: 0;
left: 0;
@ -82,9 +92,7 @@ const MobileMenuContainer = styled.div<{ $visible?: boolean }>`
z-index: 100;
background: white;
padding-top: 100px;
transition:
transform 0.5s cubic-bezier(0.16, 1, 0.3, 1),
opacity 0.25s linear;
transition: transform 0.5s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.25s linear;
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.1);
transform: translateY(-100%);
pointer-events: none;
@ -92,13 +100,11 @@ const MobileMenuContainer = styled.div<{ $visible?: boolean }>`
display: flex;
flex-direction: column;
align-items: stretch;
${(props) =>
props.$visible
&& css`
pointer-events: all;
transform: translateY(0);
opacity: 1;
`}
${(props) => (props.visible ? '&' : '&.noop')} {
pointer-events: all;
transform: translateY(0);
opacity: 1;
}
${media.lg} {
background: none;
padding-top: 0;
@ -115,16 +121,11 @@ const MobileMenuContainer = styled.div<{ $visible?: boolean }>`
}
}
`;
const Container = styled.div`
display: flex;
align-items: center;
justify-content: center;
${media.lg} {
margin-top: 40px;
}
`;
const HeaderItem = styled.div`
flex: 1;
width: 33%;
@ -133,144 +134,60 @@ const HeaderItem = styled.div`
justify-content: center;
align-items: center;
`;
const HeaderCenter = styled(HeaderItem)`
padding: 0 16px;
> :not(:last-child) {
margin-right: 16px;
}
`;
const HeaderLeft = styled(HeaderItem)`
&&& {
justify-content: flex-start;
}
justify-content: flex-start;
`;
const HeaderRight = styled(HeaderItem)`
&&& {
justify-content: flex-end;
}
justify-content: flex-end;
`;
export const Logo = styled.img`
height: 24px;
margin-top: -4px;
color: var(--black);
margin-right: 16px;
margin-right: 16px !important;
z-index: 101;
`;
export const LogoIcon = styled.div`
color: var(--black);
margin-right: 4px;
margin-right: 4px !important;
svg {
width: 36px;
height: 36px;
}
`;
const Share = styled(Text15)<{ $isMobile?: boolean }>`
&&& {
display: none;
${(props) =>
props.$isMobile
&& css`
display: flex;
justify-content: center;
padding: 12px 0;
`}
${media.lg} {
display: ${(props) => (props.$isMobile ? 'none' : 'block')};
}
color: var(--black-60);
white-space: pre-wrap;
const BuiltWith = styled(Text15)<{ isMobile?: boolean }>`
display: ${(props) => (props.isMobile ? 'flex' : 'none')};
${media.lg} {
display: ${(props) => (props.isMobile ? 'none' : 'flex')};
}
align-items: center;
justify-content: center;
color: var(--black-60);
border-bottom: none !important;
svg {
fill: var(--black);
margin: 0 0.22em;
}
> * {
margin: 0 0.22em;
}
a {
color: var(--black);
font-weight: 700;
}
> :last-child {
margin-right: 0;
}
${media.lg} {
justify-content: flex-start;
a {
display: inline-flex;
align-items: center;
color: unset;
text-decoration: unset;
}
svg,
span {
color: var(--black);
}
a:hover {
text-decoration: underline;
font-weight: normal;
}
}
`;
export interface HeaderProps {
currentVersion: string;
}
export function Header({ currentVersion }: HeaderProps) {
const [menuVisible, setMenuVisible] = React.useState(false);
return (
<Container>
<Banner>
<Sparks></Sparks>
<a
href="https://opencollective.com/iconoir/donate?interval=month&amount=10"
target="_blank"
>
Your one-time or recurring contribution does a lot to keep Iconoir
going.
</a>
</Banner>
<HeaderLeft>
<Link href="/">
<LogoContainer>
<LogoIcon>
<AnimatedSvg />
</LogoIcon>
<Logo src="/iconoir-logo.svg" alt="Iconoir Logo" />
</LogoContainer>
</Link>
<CurrentVersion version={currentVersion} />
</HeaderLeft>
<HeaderCenter>
<MobileMenuContainer $visible={menuVisible}>
<NavigationItem href="/">Icons</NavigationItem>
<NavigationItem href="/docs/introduction" activeMatch="/docs">
Documentation
</NavigationItem>
<NavigationItem href="/support" style={{ marginRight: 0 }}>
Donate &mdash; Our Mission
</NavigationItem>
<Share $isMobile>
<a href={SHARE_LINK} target="_blank" rel="noreferrer nofollow">
Share with
{' '}
<Heart width="1em" height="1em" />
{' '}
on
{' '}
<span>X (Twitter)</span>
</a>
</Share>
</MobileMenuContainer>
</HeaderCenter>
<HeaderRight>
<Share>
<a href={SHARE_LINK} target="_blank" rel="noreferrer nofollow">
Share with
{' '}
<Heart width="1em" height="1em" />
{' '}
on
{' '}
<span>X (Twitter)</span>
</a>
</Share>
<a href={DISCORD_LINK} rel="nofollow noreferrer">
<StyledDiscord $isMobile />
</a>
<MobileMenuButton onClick={() => setMenuVisible((v) => !v)}>
{menuVisible ? <Xmark /> : <Menu />}
</MobileMenuButton>
</HeaderRight>
</Container>
);
}

View file

@ -1,13 +1,53 @@
import React, { useEffect, useRef } from 'react';
import React from 'react';
import styled from 'styled-components';
import { media } from '../lib/responsive';
import {useRef, useEffect} from 'react';
export interface HeaderBackgroundProps {
children: React.ReactElement;
}
export function HeaderBackground({ children }: HeaderBackgroundProps) {
const parallaxRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!parallaxRef.current) return;
const parallaxElements = parallaxRef.current.querySelectorAll('[data-parallax-factor]');
const handleMouseMove = (event: MouseEvent) => {
const x = event.clientX / window.innerWidth;
const y = event.clientY / window.innerHeight;
parallaxElements.forEach((el) => {
const factor = parseFloat(el.getAttribute('data-parallax-factor') || '1');
(el as HTMLElement).style.transform = `translate3d(${x * factor * 40}px, ${y * factor * 80}px, 0)`;
});
};
document.addEventListener('mousemove', handleMouseMove);
return () => {
document.removeEventListener('mousemove', handleMouseMove);
};
}, []);
return (
<HeaderContainer ref={parallaxRef}>
<FloatingIconCellar data-parallax-factor="0.75"/>
<FloatingIconPay data-parallax-factor="1.5"/>
<FloatingFaceID data-parallax-factor="0.5"/>
<FloatingCommand data-parallax-factor="1.25"/>
<FloatingFill data-parallax-factor="2"/>
{children}
</HeaderContainer>
);
}
const HeaderContainer = styled.div`
position: relative;
width: fit-content;
margin: auto;
`;
const FloatingIcon = styled.div`
position: absolute;
display: none;
@ -20,117 +60,63 @@ const FloatingIcon = styled.div`
display: flex;
}
`;
const FloatingIconCellar = styled(FloatingIcon)`
-webkit-transform: rotate(6deg);
-moz-transform: rotate(6deg);
-moz-transform: rotate(6deg);
top: -120px;
right: 0px;
width: 200px;
height: 200px;
width:200px;
height:200px;
background-image: url(/cellar.gif);
background-size: 70%;
background-size:70%;
${media.lg} {
}
`;
const FloatingIconPay = styled(FloatingIcon)`
-webkit-transform: rotate(18deg);
-moz-transform: rotate(18deg);
-moz-transform: rotate(18deg);
top: -50px;
right: -100px;
width: 130px;
height: 130px;
width:130px;
height:130px;
background-image: url(/pay-bitcoin.gif);
background-size: 70%;
background-size:70%;
${media.lg} {
}
`;
const FloatingFaceID = styled(FloatingIcon)`
-webkit-transform: rotate(6deg);
-moz-transform: rotate(6deg);
-moz-transform: rotate(6deg);
top: -130px;
right: 380px;
width: 110px;
height: 110px;
width:110px;
height:110px;
background-image: url(/face-id.gif);
background-size: 70%;
background-size:70%;
${media.lg} {
}
`;
const FloatingCommand = styled(FloatingIcon)`
-webkit-transform: rotate(-7deg);
-moz-transform: rotate(-7deg);
-moz-transform: rotate(-7deg);
top: -94px;
left: 150px;
width: 110px;
height: 110px;
width:110px;
height:110px;
background-image: url(/command.gif);
background-size: 70%;
background-size:70%;
${media.lg} {
}
`;
const FloatingFill = styled(FloatingIcon)`
-webkit-transform: rotate(-14deg);
-moz-transform: rotate(-14deg);
-moz-transform: rotate(-14deg);
top: -64px;
left: -75px;
width: 110px;
height: 110px;
width:110px;
height:110px;
background-image: url(/fill.gif);
background-size: 70%;
background-size:70%;
${media.lg} {
}
`;
export interface HeaderBackgroundProps {
children: React.ReactElement;
}
export function HeaderBackground({ children }: HeaderBackgroundProps) {
const parallaxRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!parallaxRef.current)
return;
const parallaxElements = parallaxRef.current.querySelectorAll(
'[data-parallax-factor]',
);
const handleMouseMove = (event: MouseEvent) => {
const x = event.clientX / window.innerWidth;
const y = event.clientY / window.innerHeight;
parallaxElements.forEach((el) => {
const factor = Number.parseFloat(
el.getAttribute('data-parallax-factor') || '1',
);
(el as HTMLElement).style.transform = `translate3d(${
x * factor * 40
}px, ${y * factor * 80}px, 0)`;
});
};
document.addEventListener('mousemove', handleMouseMove);
return () => {
document.removeEventListener('mousemove', handleMouseMove);
};
}, []);
return (
<HeaderContainer ref={parallaxRef}>
<FloatingIconCellar data-parallax-factor="0.75" />
<FloatingIconPay data-parallax-factor="1.5" />
<FloatingFaceID data-parallax-factor="0.5" />
<FloatingCommand data-parallax-factor="1.25" />
<FloatingFill data-parallax-factor="2" />
{children}
</HeaderContainer>
);
}

View file

@ -1,16 +1,22 @@
import React from 'react';
import styled from 'styled-components';
import { media } from '../lib/responsive';
import {useRef, useEffect} from 'react';
export interface HeaderSecondaryProps {
children: React.ReactElement;
}
export function HeaderSecondary({ children }: HeaderSecondaryProps) {
return (
<HeaderContainer>
{children}
</HeaderContainer>
);
}
const HeaderContainer = styled.div`
position: relative;
width: fit-content;
margin: auto;
`;
export interface HeaderSecondaryProps {
children: React.ReactElement;
}
export function HeaderSecondary({ children }: HeaderSecondaryProps) {
return <HeaderContainer>{children}</HeaderContainer>;
}
`;

View file

@ -1,63 +0,0 @@
import React from 'react';
import styled from 'styled-components';
const PromoContainer = styled.div`
border-radius: 12px;
border: 1px solid var(--g6);
text-align: center;
margin-top: 24px;
text-decoration: none;
&:hover {
background-color: var(--g7);
}
`;
const SponsorLabel = styled.div`
font-size: 12px;
font-weight: 400;
color: var(--g4);
margin: 10px 0;
`;
const PromoContent = styled.div`
// Your content styles here, similar to SponsorText
`;
const PromoImage = styled.img`
width: 70%;
`;
const PromoInfo = styled.div`
// Styles for the text container, similar to SponsorRight
`;
const PromoTitle = styled.h2`
font-size: 16px;
font-weight: 600;
margin: 16px auto 0 auto;
`;
const PromoSub = styled.h2`
font-size: 14px;
margin: 0 auto 10px auto;
`;
export function Hono() {
return (
<a
href="https://wearehono.com/?utm_source=iconoir&utm_medium=sidebar"
style={{ textDecoration: 'none' }}
>
<PromoContainer>
<PromoContent>
<PromoInfo>
<PromoTitle>Buy high-quality logos</PromoTitle>
<PromoSub>with Hono.</PromoSub>
<PromoImage src="./hono-ad.png" />
<SponsorLabel>Our sponsor</SponsorLabel>
</PromoInfo>
</PromoContent>
</PromoContainer>
</a>
);
}

View file

@ -1,29 +1,120 @@
import type { Icon as IconType } from './IconList';
import * as AllIcons from 'iconoir-react';
import React from 'react';
import styled from 'styled-components';
import { showNotification } from '../lib/showNotification';
import { ResetButton } from './Button';
import { DEFAULT_CUSTOMIZATIONS } from './IconList';
import { DEFAULT_CUSTOMIZATIONS, Icon as IconType } from './IconList';
const HEADER = '<?xml version="1.0" encoding="UTF-8"?>';
function bakeSvg(
svgString: string,
color: string,
strokeWidth: string | number,
strokeWidth: string | number
) {
return (
HEADER
+ svgString
HEADER +
svgString
.replace(
/stroke="currentColor"/g,
`stroke="currentColor" stroke-width="${strokeWidth}"`,
`stroke="currentColor" stroke-width="${strokeWidth}"`
)
.replace(/currentColor/g, color)
);
}
export interface IconProps {
iconWidth: number;
icon: IconType;
}
export function Icon({ iconWidth, icon }: IconProps) {
const IconComponent = (AllIcons as any)[icon.iconComponentName];
const iconContainerRef = React.useRef<HTMLDivElement>(null);
const downloadRef = React.useRef<HTMLAnchorElement>(null);
const htmlContentsRef = React.useRef<string>('');
const iconContext = React.useContext(AllIcons.IconoirContext);
const [supportsClipboard, setSupportsClipboard] = React.useState(false);
React.useEffect(() => {
setSupportsClipboard(
typeof window !== 'undefined' &&
typeof window?.navigator?.clipboard?.writeText !== 'undefined'
);
}, []);
React.useEffect(() => {
if (iconContainerRef.current) {
htmlContentsRef.current = bakeSvg(
iconContainerRef.current.innerHTML,
iconContext.color || DEFAULT_CUSTOMIZATIONS.hexColor,
iconContext.strokeWidth || DEFAULT_CUSTOMIZATIONS.strokeWidth
);
}
}, [iconContext, supportsClipboard]);
React.useEffect(() => {
const element =
downloadRef.current ||
(iconContainerRef.current as unknown as HTMLAnchorElement);
if (element) {
element.href = `data:image/svg+xml;base64,${btoa(
htmlContentsRef.current
)}`;
}
}, [iconContext, supportsClipboard]);
return (
<div className={'icon-container'}>
<BorderContainer iconWidth={iconWidth}>
<IconContainer
ref={iconContainerRef}
{...((supportsClipboard
? {}
: {
as: 'a',
href: '#',
rel: 'noreferrer',
download: `${icon.filename}.svg`,
}) as any)}
>
<IconComponent />
</IconContainer>
{supportsClipboard ? (
<HoverContainer>
<CornerBR/>
<CornerTR/>
<CornerBL/>
<CornerTL/>
<HoverButton
onClick={() => {
if (htmlContentsRef.current) {
navigator.clipboard
.writeText(htmlContentsRef.current)
.then(() => {
showNotification('SVG code copied!');
})
.catch((err) => {
console.error(err);
});
}
}}
>
Copy SVG
</HoverButton>
<HoverButton
as={'a'}
ref={downloadRef}
href={'#'}
rel={'noreferrer'}
download={`${icon.filename}.svg`}
>
Download
</HoverButton>
</HoverContainer>
) : null}
</BorderContainer>
<Subtitle iconWidth={iconWidth} title={icon.filename}>
{icon.filename}
</Subtitle>
</div>
);
}
const Overlay = styled.div`
position: absolute;
border-radius: 50%;
@ -32,68 +123,60 @@ const Overlay = styled.div`
width: 8px;
height: 8px;
`;
const CornerBR = styled(Overlay)`
bottom: -6px;
right: -6px;
z-index: 999;
bottom: -6px;
right: -6px;
z-index: 999;
`;
const CornerTR = styled(Overlay)`
top: -6px;
right: -6px;
top: -6px;
right: -6px;
`;
const CornerBL = styled(Overlay)`
bottom: -6px;
left: -6px;
bottom: -6px;
left: -6px;
`;
const CornerTL = styled(Overlay)`
top: -6px;
left: -6px;
top: -6px;
left: -6px;
`;
const HoverContainer = styled.div`
const HoverContainer = styled.div<{ supportsCopy?: boolean }>`
position: absolute;
display: ${(props) => (props.supportsCopy ? 'block' : 'none')};
inset: 0;
display: flex;
align-items: stretch;
justify-content: stretch;
flex-direction: column;
transform: translateZ(0px); // Safari Fix
transition: opacity 0.1s linear;
opacity: 0;
pointer-events: none;
`;
const HoverButton = styled(ResetButton)`
&&& {
display: flex;
align-items: center;
justify-content: center;
background: var(--white);
border-radius: 0px;
transition: background 0.1s linear;
color: var(--g0);
font-size: 14px;
line-height: 23px;
font-weight: 700;
text-align: center;
flex: 1;
cursor: pointer;
text-decoration: none;
&:hover,
&:active {
background: var(--g0);
color: var(--white);
}
display: flex;
align-items: center;
justify-content: center;
background: var(--white);
border-radius: 0px !important;
transition: background 0.1s linear;
color: var(--g0);
font-size: 14px;
line-height: 23px;
font-weight: 700;
text-align: center;
flex: 1;
cursor: pointer;
text-decoration: none;
&:hover,
&:active {
background: var(--g0);
color: var(--white);
}
`;
const BorderContainer = styled.div<{ $iconWidth: number }>`
width: ${(props) => props.$iconWidth}px;
const BorderContainer = styled.div<{ iconWidth: number }>`
width: ${(props) => props.iconWidth}px;
box-sizing: border-box;
padding-bottom: 100%;
position: relative;
@ -108,7 +191,6 @@ const BorderContainer = styled.div<{ $iconWidth: number }>`
}
}
`;
const IconContainer = styled.div`
position: absolute;
inset: 0;
@ -116,129 +198,14 @@ const IconContainer = styled.div`
align-items: center;
justify-content: center;
`;
const IconTag = styled.div`
background-color: var(--g6);
position: absolute;
top: 0px;
right: 0px;
border-radius: 5px 10px;
padding: 3px 6px;
letter-spacing: 0.3px;
font-weight: 600;
font-size: 11px;
color: var(--g0);
`;
const Subtitle = styled.div<{ $iconWidth: number }>`
const Subtitle = styled.div<{ iconWidth: number }>`
font-size: 11px;
font-weight: 500;
line-height: 14.74px;
color: var(--black-60);
color: var(--black-40);
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: ${(props) => props.$iconWidth}px;
width: ${(props) => props.iconWidth}px;
`;
export interface IconProps {
iconWidth: number;
icon: IconType;
}
export function Icon({ iconWidth, icon }: IconProps) {
const IconComponent = (AllIcons as any)[icon.iconComponentName];
const iconContainerRef = React.useRef<HTMLDivElement>(null);
const downloadRef = React.useRef<HTMLAnchorElement>(null);
const htmlContentsRef = React.useRef<string>('');
const iconContext = React.useContext(AllIcons.IconoirContext);
const [supportsClipboard, setSupportsClipboard] = React.useState(false);
React.useEffect(() => {
setSupportsClipboard(
typeof window !== 'undefined'
&& typeof window?.navigator?.clipboard?.writeText !== 'undefined',
);
}, []);
React.useEffect(() => {
if (iconContainerRef.current) {
htmlContentsRef.current = bakeSvg(
(iconContainerRef.current.firstChild as SVGElement).outerHTML,
iconContext.color || DEFAULT_CUSTOMIZATIONS.hexColor,
iconContext.strokeWidth || DEFAULT_CUSTOMIZATIONS.strokeWidth,
);
}
}, [iconContext, supportsClipboard]);
React.useEffect(() => {
const element = downloadRef.current || (iconContainerRef.current as unknown as HTMLAnchorElement);
if (element) {
element.href = `data:image/svg+xml;base64,${btoa(
htmlContentsRef.current,
)}`;
}
}, [iconContext, supportsClipboard]);
return (
<div className="icon-container">
<BorderContainer $iconWidth={iconWidth}>
<IconContainer
ref={iconContainerRef}
{...((supportsClipboard
? {}
: {
as: 'a',
href: '#',
rel: 'noreferrer',
download: `${icon.filename}.svg`,
}) as any)}
>
<IconComponent />
{icon.filename.includes('-solid') ? <IconTag>SOLID</IconTag> : ''}
</IconContainer>
{supportsClipboard
? (
<HoverContainer>
<CornerBR />
<CornerTR />
<CornerBL />
<CornerTL />
<HoverButton
onClick={() => {
if (htmlContentsRef.current) {
navigator.clipboard
.writeText(htmlContentsRef.current)
.then(() => {
showNotification('SVG code copied!');
})
.catch((err) => {
console.error(err);
});
}
}}
>
Copy SVG
</HoverButton>
<HoverButton
as="a"
ref={downloadRef}
href="#"
rel="noreferrer"
download={`${icon.filename}.svg`}
>
Download
</HoverButton>
</HoverContainer>
)
: null}
</BorderContainer>
<Subtitle $iconWidth={iconWidth} title={icon.filename}>
{icon.filename}
</Subtitle>
</div>
);
}

View file

@ -1,16 +1,14 @@
import type {
ListChildComponentProps,
} from 'react-window';
import { chunk } from 'lodash';
import React from 'react';
import {
areEqual,
ListChildComponentProps,
VariableSizeList as List,
} from 'react-window';
import styled from 'styled-components';
import useResizeObserver from 'use-resize-observer';
import { ICON_SPACE, ICON_WIDTH } from '../lib/constants';
import { CategoryRow } from './CategoryRow';
import { ICON_SPACE, ICON_WIDTH } from '../lib/constants';
import { IconListEmpty } from './IconListEmpty';
import { IconsRow } from './IconsRow';
import { ReactWindowScroller } from './ReactWindowScroller';
@ -43,22 +41,18 @@ function normalizeString(s: string) {
function filterIcons(allIcons: Icon[], filters: IconListFilters): Icon[] {
if (filters.search) {
const normalSearch = normalizeString(filters.search!);
let result = allIcons;
let result = allIcons
for (const term of normalSearch.split(' ')) {
result = result.filter((icon) => {
return (
normalizeString(icon.filename).includes(term)
|| normalizeString(icon.category).includes(term)
|| icon.tags.some((tag) => normalizeString(tag).includes(term))
normalizeString(icon.filename).includes(term) ||
normalizeString(icon.category).includes(term) ||
icon.tags.some((tag) => normalizeString(tag).includes(term))
);
});
})
}
return result;
} else {
return allIcons;
}
} else return allIcons;
}
interface IconCategoryRow {
@ -69,34 +63,28 @@ interface IconIconsRow {
icons: Icon[];
}
type IconRow = IconCategoryRow | IconIconsRow;
function isCategoryRow(iconRow: IconRow): iconRow is IconCategoryRow {
return !!(iconRow as IconCategoryRow).category;
}
function getRowsFromIcons(
filteredIcons: Icon[],
iconsPerRow: number,
iconsPerRow: number
): IconRow[] {
const categoryGroups: Record<string, Icon[]> = {};
for (const icon of filteredIcons) {
if (!categoryGroups[icon.category])
categoryGroups[icon.category] = [];
if (!categoryGroups[icon.category]) categoryGroups[icon.category] = [];
categoryGroups[icon.category].push(icon);
}
const result: IconRow[] = [];
const sortedCategories = Object.keys(categoryGroups).sort();
for (const sortedCategory of sortedCategories) {
result.push({
category: sortedCategory,
numIcons: categoryGroups[sortedCategory].length,
});
const iconRows = chunk(categoryGroups[sortedCategory], iconsPerRow);
for (const iconRow of iconRows) {
result.push({ icons: iconRow });
}
@ -109,7 +97,6 @@ const ICON_BOTTOM_PADDING = 65;
const HEADER_HEIGHT = 150;
const HEADER_INNER_HEIGHT = 15 + 40;
const HEADER_TOP_PADDING = HEADER_HEIGHT - HEADER_INNER_HEIGHT;
function getItemSize(row: IconRow, iconWidth: number): number {
if (isCategoryRow(row)) {
return HEADER_HEIGHT;
@ -122,67 +109,29 @@ interface IconListContextValue {
iconWidth: number;
iconsPerRow: number;
}
const IconListContext = React.createContext<IconListContextValue | undefined>(undefined);
const Container = styled.div`
width: 100%;
margin-top: -${HEADER_TOP_PADDING}px;
> :first-child {
overflow: visible !important;
> :first-child {
-webkit-overflow-scrolling: touch;
}
}
`;
const Row = React.memo(
({ data, index, style }: ListChildComponentProps<IconRow[]>) => {
const { iconWidth } = React.useContext(IconListContext)!;
const row = data[index];
if (isCategoryRow(row)) {
return (
<CategoryRow
category={row.category}
numIcons={row.numIcons}
style={style}
/>
);
} else {
return <IconsRow icons={row.icons} style={style} iconWidth={iconWidth} />;
}
},
areEqual,
);
Row.displayName = 'Row';
export const IconListContext = React.createContext<
IconListContextValue | undefined
>(undefined);
export interface IconListProps {
filters: IconListFilters;
allIcons: Icon[];
}
export function IconList({ filters, allIcons }: IconListProps) {
const filteredIcons = filterIcons(allIcons, filters);
const { ref, width = 400 } = useResizeObserver();
const iconsPerRow = width
? Math.floor((width + ICON_SPACE) / (ICON_WIDTH + ICON_SPACE))
: null;
let children = null;
const listRef = React.useRef<List<IconRow[]> | null>(null);
const listRef = React.useRef<List<IconRow[]> | null>();
const [height, setHeight] = React.useState(400);
const iconWidth = iconsPerRow
? Math.floor((width + ICON_SPACE) / iconsPerRow) - ICON_SPACE
: null;
React.useEffect(() => {
setHeight(window.innerHeight);
}, []);
React.useEffect(() => {
if (listRef.current) {
listRef.current.resetAfterIndex(0, true);
@ -191,17 +140,14 @@ export function IconList({ filters, allIcons }: IconListProps) {
if (filteredIcons.length && iconsPerRow && width && iconWidth) {
const iconRows = getRowsFromIcons(filteredIcons, iconsPerRow);
children = (
<IconListContext.Provider value={{ iconsPerRow, iconWidth }}>
<ReactWindowScroller>
{({ ref, outerRef, style, onScroll }: any) => (
<List<IconRow[]>
ref={(c) => {
if (typeof ref === 'function')
ref(c);
else
ref.current = c;
if (typeof ref === 'function') ref(c);
else ref.current = c;
listRef.current = c;
}}
itemData={iconRows}
@ -225,3 +171,34 @@ export function IconList({ filters, allIcons }: IconListProps) {
return <Container ref={ref}>{children}</Container>;
}
const Container = styled.div`
width: 100%;
margin-top: -${HEADER_TOP_PADDING}px;
> :first-child {
overflow: visible !important;
> :first-child {
-webkit-overflow-scrolling: touch;
}
}
`;
const Row = React.memo(
({ data, index, style }: ListChildComponentProps<IconRow[]>) => {
const { iconWidth } = React.useContext(IconListContext)!;
const row = data[index];
if (isCategoryRow(row)) {
return (
<CategoryRow
category={row.category}
numIcons={row.numIcons}
style={style}
/>
);
} else {
return <IconsRow icons={row.icons} style={style} iconWidth={iconWidth} />;
}
},
areEqual
);
Row.displayName = 'Row';

View file

@ -3,6 +3,29 @@ import styled from 'styled-components';
import { SUGGEST_ICON_LINK } from '../lib/constants';
import { Text18 } from './Typography';
export interface IconListEmptyProps {
searchTerm: string;
}
export function IconListEmpty({ searchTerm }: IconListEmptyProps) {
return (
<Container>
<IconContainer>
<SpockHandGesture />
</IconContainer>
<Title>
Unfortunately there are no icons for &apos;{searchTerm}&apos;
</Title>
<Text18 style={{ color: 'var(--black-60)' }}>
{"If you can't find the icon, you can make a"}
<br />
<a href={SUGGEST_ICON_LINK} target={'_blank'} rel={'noreferrer'}>
suggestion on GitHub.
</a>
</Text18>
</Container>
);
}
const Container = styled.div`
margin-top: 90px;
display: flex;
@ -10,7 +33,6 @@ const Container = styled.div`
flex-direction: column;
text-align: center;
`;
const IconContainer = styled.div`
svg {
width: 60px;
@ -19,37 +41,8 @@ const IconContainer = styled.div`
margin-bottom: 65px;
color: var(--black);
`;
const Title = styled(Text18)`
&&& {
font-weight: 700;
margin-bottom: 30px;
color: var(--black);
}
font-weight: 700;
margin-bottom: 30px;
color: var(--black);
`;
export interface IconListEmptyProps {
searchTerm: string;
}
export function IconListEmpty({ searchTerm }: IconListEmptyProps) {
return (
<Container>
<IconContainer>
<SpockHandGesture />
</IconContainer>
<Title>
Unfortunately there are no icons for &apos;
{searchTerm}
&apos;
</Title>
<Text18 style={{ color: 'var(--black-60)' }}>
If you can&apos;t find the icon, you can make a
<br />
<a href={SUGGEST_ICON_LINK} target="_blank" rel="noreferrer">
suggestion on GitHub.
</a>
</Text18>
</Container>
);
}

View file

@ -1,22 +1,13 @@
import type { Icon } from './IconList';
import styled from 'styled-components';
import { ICON_SPACE } from '../lib/constants';
import { Icon as IconC } from './Icon';
const RowContainer = styled.div`
display: flex;
align-items: center;
> :not(:last-child) {
margin-right: ${ICON_SPACE}px;
}
`;
import { Icon } from './IconList';
export interface IconsRowProps {
icons: Icon[];
style?: any;
iconWidth: number;
}
export function IconsRow({ icons, style, iconWidth }: IconsRowProps) {
return (
<RowContainer style={style}>
@ -26,3 +17,11 @@ export function IconsRow({ icons, style, iconWidth }: IconsRowProps) {
</RowContainer>
);
}
const RowContainer = styled.div`
display: flex;
align-items: center;
> :not(:last-child) {
margin-right: ${ICON_SPACE}px;
}
`;

View file

@ -19,44 +19,40 @@ const ResetInput = styled.input`
}
`;
const Input = styled(ResetInput)`
&&& {
min-height: 35px;
background: var(--white);
border-radius: 10px;
overflow: hidden;
padding: 6px;
text-align: center;
font-size: 13px;
line-height: 21px;
font-weight: 500;
color: var(--black);
border: solid 1px var(--g6);
&:hover {
border: solid 2px var(--g0);
}
&:focus {
border: solid 2px var(--g0);
}
export const Input = styled(ResetInput)`
min-height: 35px;
background: var(--white);
border-radius: 10px;
overflow: hidden;
padding: 6px;
text-align: center;
font-size: 13px;
line-height: 21px;
font-weight: 500;
color: var(--black);
border: solid 1px var(--g6);
&:hover{
border: solid 2px var(--g0);
}
&:focus{
border: solid 2px var(--g0);
}
`;
export const LargeInput = styled(Input)`
&&& {
height: 60px;
font-size: 16px;
line-height: 26px;
border-radius: 12px;
padding: 0 23px;
text-align: left;
width: 100%;
box-sizing: border-box;
outline: none;
transition: 0.2s;
&:hover {
transform: scale(1.02);
}
height: 60px;
font-size: 16px;
line-height: 26px;
border-radius: 12px;
padding: 0 23px;
text-align: left;
width: 100%;
box-sizing: border-box;
outline: none;
transition: 0.2s;
&:hover{
transform: scale(1.02);
}
`;
@ -69,7 +65,7 @@ export const ColorButton = styled.div`
right: 20px;
position: absolute;
pointer-events: none;
&:hover {
&:hover{
transition: 0.2s;
scale: 1.2;
}
@ -86,10 +82,10 @@ export const ColorInput = styled.input`
border: none;
}
background-color: var(--gray-200);
opacity: 0;
opacity:0;
transition: 0.2s;
&:hover + ${ColorButton} {
transition: 0.2s;
scale: 1.3;
}
`;
`;

Some files were not shown because too many files have changed in this diff Show more