Compare commits

..

No commits in common. "main" and "v5.3.1" have entirely different histories.

5656 changed files with 131093 additions and 28891 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,22 @@ 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:
* Bits:
## 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

@ -9,19 +9,23 @@ jobs:
build:
name: Build
runs-on: ubuntu-latest
# Skip job on forks
if: github.repository_owner == 'iconoir-icons'
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup
uses: ./.github/actions/setup
- name: Build CSS
run: pnpm run build css
- name: Commit changes
uses: stefanzweifel/git-auto-commit-action@v6
with:
commit_message: Update build artifacts
- uses: actions/checkout@v2
- uses: actions/cache@v2
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-
- uses: actions/setup-node@v2
with:
node-version: '16'
registry-url: 'https://registry.npmjs.org'
- uses: pnpm/action-setup@v2.1.0
with:
version: 7.8.0
run_install: true
- run: pnpm run build
- 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,56 +9,48 @@ 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@v2
with:
# We have to checkout main or PNPM fails. Tag should be on main anyway.
ref: main
- name: Setup
uses: ./.github/actions/setup
ref: main # We have to checkout main or PNPM fails. Tag should be on main anyway.
- uses: actions/cache@v2
with:
node-registry: https://registry.npmjs.org
- name: Generate changelog file
uses: rhysd/changelog-from-release/action@v3
path: ~/.pnpm-store
key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-
- uses: actions/setup-node@v2
with:
node-version: '16'
registry-url: 'https://registry.npmjs.org'
- uses: pnpm/action-setup@v2.1.0
with:
version: 7.8.0
run_install: true
- uses: rhysd/changelog-from-release/action@v2
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
file: packages/iconoir-flutter/CHANGELOG.md
github_token: ${{ secrets.GITHUB_TOKEN }}
commit: false
args: -d=false
- name: Build
run: pnpm run build
- name: Prepare packages
run: pnpm run prepublish-all
- run: pnpm run build
- run: pnpm run prepublish-all
env:
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
- name: Update tag
run: |
git -c user.email="actions@github.com" -c user.name="GitHub Actions" tag -fa ${{ github.ref_name }} -m "${{ github.ref_name }}"
git push -f origin ${{ github.ref_name }}
- name: Publish packages
run: pnpm -r publish --access public
- run: git -c user.email="actions@github.com" -c user.name="GitHub Actions" tag -fa ${{ github.ref_name }} -m "${{ github.ref_name }}"
- run: git push -f origin ${{ github.ref_name }}
- run: npm publish --access public
continue-on-error: true
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_CONFIG_PROVENANCE: true
- name: Publish Flutter
uses: k-paxian/dart-package-publisher@v1.6
with:
credentialJson: ${{ secrets.PUB_CREDENTIAL_JSON }}
relativePath: ./packages/iconoir-flutter
- run: pnpm -r publish --filter './packages/**' --access public
continue-on-error: true
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
# - uses: sakebook/actions-flutter-pub-publisher@v1.4.0
# with:
# credential: ${{ secrets.PUB_CREDENTIAL_JSON }}
# package_directory: ./packages/iconoir-flutter

View file

@ -1,59 +1,62 @@
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: {}
push:
tags:
- 'v*'
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup
uses: ./.github/actions/setup
- name: Build
run: pnpm run build react
- name: Build website
run: pnpm run build
working-directory: iconoir.com
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Setup GitHub Pages
uses: actions/configure-pages@v5
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
- uses: actions/checkout@v2
with:
path: ./iconoir.com/out
ref: main # We have to checkout main or PNPM fails. Tag should be on main anyway.
- uses: actions/cache@v2
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-
- uses: actions/setup-node@v2
with:
node-version: '16'
registry-url: 'https://registry.npmjs.org'
- uses: pnpm/action-setup@v2.1.0
with:
version: 7.8.0
run_install: true
- name: Build Packages
run: pnpm run dist
- name: Build
run: ./node_modules/.bin/next build
working-directory: iconoir.com
- name: Export
run: ./node_modules/.bin/next export
working-directory: iconoir.com
- name: Setup Pages
uses: actions/configure-pages@v1
- name: Upload artifact
uses: actions/upload-pages-artifact@v1
with:
path: './iconoir.com/out'
deploy:
name: Deploy
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
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.

112
README.md
View file

@ -1,70 +1,69 @@
# Iconoir
<div align="center">
<img src="assets/cover.png" alt="Iconoir" />
</div>
[![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 align="center">
Iconoir is an open-source library with 1000+ unique SVG icons, designed on a 24x24 pixels grid. No premium icons, no email sign-up, no newsletters.
</div>
## What is Iconoir?
<div align="center">
<a href="https://iconoir.com"><strong>Browse at iconoir.com &rarr;</strong></a>
</div>
Iconoir is an open-source library with 1600+ unique SVG icons, designed on a 24x24 pixels grid.
<a href="https://iconoir.com"><strong>Browse at iconoir.com &rarr;</strong></a>
<div align="center">
<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>
</div>
## 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).
## Vue
A Vue library is available under the name `@iconoir/vue`.
For more details, see the package [README](./packages/iconoir-vue).
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).
## 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/lucaburgio/iconoir@main/css/iconoir.css">
```
Here is an example in HTML:
@ -72,49 +71,12 @@ 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).
## Figma
The library is available in the Figma community [here](https://www.figma.com/community/file/983248991460488027/Iconoir-Pack).
## Swift Package
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..._
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_.
### UIKit
```swift
import UIKit
import Iconoir
let imageView = UIImageView(image: Iconoir.bell.asUIImage)
```
### SwiftUI
```swift
import SwiftUI
import Iconoir
struct ContentView: View {
var body: some View {
Iconoir.bell.asImage
.foregroundColor(.blue)
.font(.system(size: 24))
}
}
```
## License
MIT License.
MIT License

BIN
assets/cover.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

482
bin/build.js Normal file
View file

@ -0,0 +1,482 @@
import execa from 'execa';
import { promises as fs, readFileSync } 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 } from '../constants.js';
// Paths
const __dirname = dirname(fileURLToPath(import.meta.url));
const rootDir = path.join(__dirname, '..');
const iconoirIconsDir = path.join(rootDir, 'icons');
const ignoreCleanFilenames = ['IconoirContext.tsx'];
// 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',
},
};
// 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, '');
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 promises = files
.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);
}
},
},
],
{ 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.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`
);
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 temporary directory',
skip: (ctx) => !ctx.tmpDir,
task: async (ctx) => {
await fs.rm(ctx.tmpDir, { 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);
};

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: 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,13 @@ 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');
publishPubPackage('iconoir-flutter');

View file

@ -0,0 +1,20 @@
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__''',
color: color,
width: width,
height: height,
);
}

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;

12
constants.js Normal file
View file

@ -0,0 +1,12 @@
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',
};

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,7 @@ 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 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)),
},
},
});

6
iconoir.com/.eslintrc.json Executable file
View file

@ -0,0 +1,6 @@
{
"extends": "next/core-web-vitals",
"rules": {
"react/no-unescaped-entities": ["off"]
}
}

0
iconoir.com/.gitignore vendored Normal file → Executable file
View file

34
iconoir.com/README.md Executable 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,11 +1,29 @@
import React from 'react';
import styled from 'styled-components';
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.id = '_carbonads_js';
container.appendChild(script);
}
}, []);
return <AdContainer ref={containerRef} />;
}
const AdContainer = styled.div`
#carbonads {
margin: 24px 0 0 0;
a {
text-decoration: none;
text-decoration: none !important;
}
.carbon-wrap {
display: flex;
@ -13,11 +31,6 @@ const AdContainer = styled.div`
> :first-child {
margin-right: 12px;
}
& > a > img {
width: 100px;
height: 74px;
filter: grayscale(100%);
}
}
.carbon-text {
color: var(--black-80);
@ -30,24 +43,3 @@ const AdContainer = styled.div`
}
}
`;
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.id = '_carbonads_js';
container.appendChild(script);
}
}, []);
return <AdContainer ref={containerRef} />;
}

View file

@ -1,6 +1,7 @@
import anime from 'animejs';
import React from 'react';
import anime from 'animejs';
// 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,39 +1,90 @@
import styled, { css, keyframes } from 'styled-components';
import React from 'react';
import styled, { keyframes } from 'styled-components';
import useResizeObserver from 'use-resize-observer';
import {
FEEDBACK_LINK,
LIBRARY_LINKS,
SUGGEST_LIBRARY_LINK,
} from '../lib/constants';
import { media } from '../lib/responsive';
import { FEEDBACK_LINK, LIBRARY_LINKS, SUGGEST_LIBRARY } from './constants';
import { media } from './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} 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,30 +99,26 @@ const AvailableForAnimation = keyframes`
transform: translateX(0);
}
`;
const AvailableForOuter = styled.div`
max-width: 100vw;
margin: 16px -30px 70px -30px;
padding: 0 30px;
overflow: hidden;
${media.lg} {
margin: 80px auto;
margin: 120px auto;
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,124 +129,14 @@ 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;
transition: 0.2s;
&:hover {
scale: 1.2;
transition: 0.2s;
}
${media.lg} {
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,55 @@ 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);
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;
background: var(--black);
height: 75px;
display: inline-flex;
align-items: center;
text-decoration: none;
color: var(--white);
padding: 0 43px;
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;
}
}
&::after {
content: ' ';
display: block;
position: absolute;
inset: 0;
border: solid 3px var(--black);
opacity: 0;
transition: inset 0.5s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.15s linear;
z-index: 10;
}
&: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 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);
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);
}
`;

View file

@ -1,7 +1,27 @@
import React from 'react';
import styled from 'styled-components';
import { media } from '../lib/responsive';
import { media } from './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 +33,6 @@ const InnerContainer = styled.div`
margin-right: 10px;
}
`;
const Container = styled.div`
display: flex;
align-items: flex-end;
@ -23,42 +42,12 @@ 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(--black);
`;
const Separator = styled.div`
height: 1px;
flex: 1;
background: var(--g6);
background: var(--light-gray);
`;
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,17 +1,70 @@
import type { MDXRemoteSerializeResult } from 'next-mdx-remote';
import { BoxIso } from 'iconoir-react';
import moment from 'moment';
import React from 'react';
import styled from 'styled-components';
import { media } from '../lib/responsive';
import { CopyButton } from './Button';
import { Code, CopyButton, Text15, Text18 } from './Typography';
import moment from 'moment';
import { MDXRemoteSerializeResult } from 'next-mdx-remote';
import { MDXRemote } from './MDXRemote';
import { Code, Text15, Text18 } from './Typography';
import { FILE_PREFIX } from './constants';
import { media } from './responsive';
const EXPAND_HEIGHT = 400;
export interface ChangelogEntryProps {
name: string;
body: MDXRemoteSerializeResult;
created_at: string;
}
export function ChangelogEntry({
name,
body,
created_at,
}: ChangelogEntryProps) {
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
) {
setShouldExpand(true);
}
}, []);
return (
<Container ref={containerRef}>
<ContainerLeft>
<ContainerIcon>
<BoxIso />
</ContainerIcon>
<TitleContainer>
<a
href={`${FILE_PREFIX}/../../releases/tag/${name}`}
target={'_blank'}
rel={'noreferrer'}
style={{ textDecoration: 'none' }}
>
<EntryTitle>{name}</EntryTitle>
</a>
<Text15>{moment(created_at).format('MMM DD, YYYY')}</Text15>
</TitleContainer>
</ContainerLeft>
<EntryBody expanded={expanded}>
<MDXRemote {...body} />
{shouldExpand ? (
<ExpandContainer>
<CopyButton onClick={() => setExpanded((e) => !e)}>
{expanded ? 'Collapse' : 'Expand'}
</CopyButton>
</ExpandContainer>
) : null}
</EntryBody>
</Container>
);
}
const Container = styled.div`
margin: 40px 0;
margin: 40px 0 !important;
display: flex;
align-items: stretch;
flex-direction: column;
@ -19,10 +72,9 @@ const Container = styled.div`
${media.lg} {
flex-direction: row;
align-items: flex-start;
margin: 24px 0;
margin: 24px 0 !important;
}
`;
const ContainerLeft = styled.div`
display: flex;
align-items: flex-start;
@ -32,115 +84,46 @@ const ContainerLeft = styled.div`
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;
}
color: var(--black);
font-weight: 700;
`;
const ExpandContainer = styled.div`
position: absolute;
bottom: 16px;
right: 23px;
`;
const EntryBody = styled(Code)<{ $expanded?: boolean }>`
&&& {
flex: 1;
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;
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;
padding: 0;
li {
margin-bottom: 8px;
}
}
code {
display: inline-block;
background: var(--gray) !important;
color: var(--black);
font-family: var(--font-family) !important;
padding: 0 4px;
font-size: 18px !important;
}
`;
export interface ChangelogEntryProps {
name: string;
url: string;
created_at: string;
body?: MDXRemoteSerializeResult;
}
export function ChangelogEntry({
name,
url,
body,
created_at,
}: ChangelogEntryProps) {
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
) {
setShouldExpand(true);
}
}, []);
return (
<Container ref={containerRef}>
<ContainerLeft>
<ContainerIcon>
<BoxIso />
</ContainerIcon>
<TitleContainer>
<a
href={url}
target="_blank"
rel="noreferrer"
style={{ textDecoration: 'none' }}
>
<EntryTitle>{name}</EntryTitle>
</a>
<Text15>{moment(created_at).format('MMM DD, YYYY')}</Text15>
</TitleContainer>
</ContainerLeft>
<EntryBody $expanded={expanded}>
{body ? <MDXRemote {...body} /> : 'No changelog'}
{shouldExpand
? (
<ExpandContainer>
<CopyButton onClick={() => setExpanded((e) => !e)}>
{expanded ? 'Collapse' : 'Expand'}
</CopyButton>
</ExpandContainer>
)
: null}
</EntryBody>
</Container>
);
}

View file

@ -1,35 +1,29 @@
import Link from 'next/link';
import React from 'react';
import styled from 'styled-components';
import { Text13 } from './Typography';
export interface CurrentVersionProps {
version: string;
color?: string;
}
export function CurrentVersion({ version, color }: CurrentVersionProps) {
return (
<Link href={'/docs/changelog'} passHref>
<Container as={'a'} style={color ? { background: color } : undefined}>
{version}
</Container>
</Link>
);
}
const Container = styled(Text13)`
&&& {
color: var(--g1);
color: var(--black);
font-weight: 700;
background: var(--g5);
background: var(--pink);
line-height: 1;
padding: 7px 16px;
border-radius: 200px;
display: block;
text-decoration: none;
transition:
color 0.1s linear,
background 0.1s linear;
&:hover {
background: var(--black);
color: var(--white);
}
}
text-decoration: none !important;
`;
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,16 @@
import type { IconListCustomizations } from './IconList';
import React from 'react';
import { DEFAULT_CUSTOMIZATIONS, IconListCustomizations } from './IconList';
import styled from 'styled-components';
import { media } from '../lib/responsive';
import { DEFAULT_CUSTOMIZATIONS } from './IconList';
import { ColorButton, ColorInput } from './Input';
import { Text13, Text18 } from './Typography';
import { Button } from './Button';
import { 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;
}
}
`;
export interface CustomizationEditorProps {
customizations: IconListCustomizations;
// eslint-disable-next-line no-unused-vars
onChange: (customizations: IconListCustomizations) => void;
}
export function CustomizationEditor({
customizations,
onChange,
@ -64,11 +18,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 +38,65 @@ 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>
<Header>
<Text18 style={{ fontWeight: 700, color: 'var(--black)' }}>
Customize
</Text18>
<Button onClick={() => onChange(DEFAULT_CUSTOMIZATIONS)}>Reset</Button>
</Header>
<Field>
<Slider
label={'Size'}
minValue={12}
maxValue={128}
value={[size]}
formatOptions={{ maximumFractionDigits: 0 }}
onChange={(values) => {
setSize(values[0]);
updateCustomizations({ size: values[0] });
}}
/>
</Field>
<Field>
<Slider
label={'Stroke Width'}
minValue={0.5}
maxValue={3}
value={[strokeWidth]}
step={0.01}
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 });
}}
/>
</HorizontalField>
</>
);
}
const Header = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 45px;
`;
const Field = styled.div`
margin-bottom: 35px;
`;
const HorizontalField = styled(Field)`
display: flex;
align-items: center;
justify-content: space-between;
`;

View file

@ -1,15 +1,95 @@
import type { DocumentationItem } from '../pages/docs/[...slug]';
import { NavArrowUp } from 'iconoir-react';
import React from 'react';
import { DocumentationItem } from '../pages/docs/[...slug]';
import styled from 'styled-components';
import Link from 'next/link';
import { useRouter } from 'next/router';
import React from 'react';
import styled from 'styled-components';
import { media } from '../lib/responsive';
import { media } from './responsive';
import { NavArrowUp } from 'iconoir-react';
const HeaderItemIcon = styled.div<{ $active?: boolean }>`
export interface DocumentationNavigationProps {
documentationItems: DocumentationItem[];
pathPrefix?: string[];
}
export function DocumentationNavigation({
documentationItems,
pathPrefix,
}: DocumentationNavigationProps) {
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) => {
return activePath.startsWith(
[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}>
<NavArrowUp />
</HeaderItemIcon>
{documentationItem.title}
</HeaderItem>
<ChildrenContainer expanded={active}>
<DocumentationNavigation
documentationItems={documentationItem.children}
pathPrefix={[
...(pathPrefix || []),
documentationItem.path,
].filter(Boolean)}
/>
</ChildrenContainer>
</React.Fragment>
);
} else {
return (
<Link href={`/docs/${path}`} passHref key={documentationItem.path}>
<NavigationItem as={'a'} active={activePath === path}>
<span>{documentationItem.title}</span>
{documentationItem.label ? (
<NavigationItemLabel>
{documentationItem.label}
</NavigationItemLabel>
) : null}
</NavigationItem>
</Link>
);
}
})}
</>
);
}
const HeaderItemIcon = styled.div<{ active?: boolean }>`
font-size: 13px;
transition: transform 0.25s linear;
transform: rotate(${(props) => (props.$active ? 180 : 0)}deg);
transform: rotate(${(props) => (props.active ? 180 : 0)}deg);
margin-right: 7px;
position: relative;
top: 6px;
@ -20,19 +100,19 @@ const HeaderItemIcon = styled.div<{ $active?: boolean }>`
display: none;
}
`;
const ChildrenContainer = styled.div<{ $expanded?: boolean }>`
display: ${(props) => (props.$expanded ? 'block' : '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;
text-transform: uppercase;
font-size: 12px;
line-height: 19px;
color: var(--g0);
color: var(--black);
letter-spacing: 0.12em;
font-weight: 700;
display: flex;
align-items: baseline;
@ -46,144 +126,45 @@ const HeaderItem = styled.div`
}
}
`;
const NavigationItem = styled.div<{ $active?: boolean }>`
padding: 12px 12px 12px 75px;
transition:
background 0.1s linear,
color 0.1s linear;
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);
color: var(--black-60);
display: flex;
align-items: center;
text-decoration: none;
span {
font-weight: 400;
font-weight: 500;
}
> :not(:last-child) {
margin-right: 14px;
}
&:hover,
${(props) => (props.$active ? '&' : '&.noop')} {
color: var(--g0);
text-decoration: underline;
${(props) => (props.active ? '&' : '&.noop')} {
background: var(--light-gray);
color: var(--black);
}
${(props) => (props.$active ? 'span' : '&.noop')} {
font-weight: 500;
${(props) => (props.active ? 'span' : '&.noop')} {
font-weight: 700;
}
${media.lg} {
padding: 12px 12px 12px 65px;
padding: 12px 45px 12px 65px;
}
`;
const NavigationItemLabel = styled.span`
display: flex;
align-items: center;
justify-content: center;
padding: 2px 6px;
border-radius: 4px;
padding: 0 4px;
font-size: 11px;
line-height: 17.6px;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--g1);
background: var(--g5);
color: var(--white);
background: var(--black);
`;
export interface DocumentationNavigationProps {
documentationItems: DocumentationItem[];
pathPrefix?: string[];
}
export function DocumentationNavigation({
documentationItems,
pathPrefix,
}: DocumentationNavigationProps) {
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) => {
return activePath.startsWith(
[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}>
<NavArrowUp />
</HeaderItemIcon>
{documentationItem.title}
</HeaderItem>
<ChildrenContainer $expanded={active}>
<DocumentationNavigation
documentationItems={documentationItem.children}
pathPrefix={[
...(pathPrefix || []),
documentationItem.path,
].filter(Boolean)}
/>
</ChildrenContainer>
</React.Fragment>
);
} else {
return (
<Link
href={`/docs/${path}`}
passHref
legacyBehavior
key={documentationItem.path}
>
<NavigationItem as="a" $active={activePath === path}>
<span>{documentationItem.title}</span>
{documentationItem.label
? (
<NavigationItemLabel>
{documentationItem.label}
</NavigationItemLabel>
)
: null}
</NavigationItem>
</Link>
);
}
})}
</>
);
}

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,23 @@
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 { CustomizationEditor } from './CustomizationEditor';
import { FiltersEditor } from './FiltersEditor';
import { IconList } from './IconList';
import { Streamline } from './Streamline';
import { Icon, IconList, IconListFilters } from './IconList';
import { media } from './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,12 +37,45 @@ export function Explore({ allIcons }: ExploreProps) {
<FilterContainer>
<FiltersEditor filters={filters} onChange={setFilters} />
</FilterContainer>
<Streamline />
<CustomizationEditor
customizations={customizations}
onChange={setCustomizations}
/>
<Ad />
</Right>
</Container>
);
}
const Container = styled.div`
display: flex;
align-items: flex-start;
flex-direction: row;
`;
const Left = styled.div`
flex: 1;
min-height: calc(100vh - 100px);
`;
const Right = styled.div`
position: sticky;
top: 50px;
width: 275px;
margin-left: 68px;
display: none;
${media.md} {
display: block;
}
`;
const FilterContainer = styled.div<{ isMobile?: boolean }>`
display: ${(props) => (props.isMobile ? 'block' : 'none')};
margin-bottom: 40px;
position: sticky;
top: 20px;
z-index: 100;
${media.md} {
position: relative;
top: 0;
display: ${(props) => (props.isMobile ? 'none' : 'block')};
margin-bottom: 50px;
}
`;

View file

@ -1,33 +1,15 @@
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);
// 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);
};
}, []);
React.useEffect(() => {
setSearch(filters.search);
}, [filters]);
@ -43,19 +25,10 @@ export function FiltersEditor({ filters, onChange }: FiltersEditorProps) {
return (
<LargeInput
placeholder="Search..."
placeholder={'Search...'}
value={search}
type="search"
autoCapitalize="none"
tabIndex={1}
onFocus={(e) => {
if (!didScrollRef.current) {
e.target.scrollIntoView({
block: 'start',
behavior: 'auto',
});
}
}}
type={'search'}
autoCapitalize={'none'}
onChange={(e) => {
const value = e.target.value;
setSearch(value);

View file

@ -1,165 +1,42 @@
import { PeaceHand } from 'iconoir-react';
import React from 'react';
import styled from 'styled-components';
import {
GITHUB_LINK,
ISSUE_LINK,
LICENSE_LINK,
PRIVACY_LINK,
SUPPORT_LINK,
} from '../lib/constants';
import { LICENSE_LINK } from './constants';
import { Logo, LogoContainer, LogoIcon } from './Header';
import { Text13, Text17 } from './Typography';
const Container = styled.div`
display: block;
margin-top: 110px;
padding-top: 30px;
margin-top: 100px;
padding: 84px 12%;
background-color: var(--g7);
align-items: center;
> :not(:last-child) {
margin-right: 50px;
}
`;
const FooterEnd = styled.div`
border-top: 1px solid var(--g5);
padding-top: 20px;
margin-top: 74px;
width: 100%;
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);
}
`;
const FooterCategoryLinks = styled.div``;
const FooterCategoryLink = styled.a`
display: block;
font-size: 17px;
color: var(--g1);
width: fit-content;
margin-bottom: 12px;
text-decoration: none;
&: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>
);
}
import { NavigationItemContainer } from './NavigationItem';
export function Footer() {
const year = new Date().getFullYear();
return (
<Container>
<LogoContainer>
<LogoIcon>
<PeaceHand />
</LogoIcon>
<Logo src="/iconoir-logo.svg" alt="Iconoir Logo" />
<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>
<FooterNavigationItem
as={'a'}
href={LICENSE_LINK}
target={'_blank'}
rel={'noreferrer'}
>
License
</FooterNavigationItem>
</Container>
);
}
const Container = styled.div`
margin-top: 100px;
padding-top: 30px;
border-top: solid 2px var(--light-gray);
display: flex;
align-items: center;
> :not(:last-child) {
margin-right: 50px;
}
`;
const FooterNavigationItem = styled(NavigationItemContainer)`
color: var(--black);
`;

View file

@ -1,3 +1,4 @@
import React from 'react';
import Script from 'next/script';
export function GA() {
@ -5,9 +6,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,80 +1,95 @@
import { Discord, Menu, Sparks, Xmark } from 'iconoir-react';
import { Heart } from 'iconoir-react/solid';
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 { AnimatedSvg } from './AnimatedSvg';
import { ResetButton } from './Button';
import styled from 'styled-components';
import { AUTHOR_LINKS } from './constants';
import { Cancel, Heart, Menu } from 'iconoir-react';
import { CurrentVersion } from './CurrentVersion';
import { NavigationItem } from './NavigationItem';
import { Text15 } from './Typography';
import { media } from './responsive';
import { ResetButton } from './Button';
import { AnimatedSvg } from './AnimatedSvg';
import { NavigationItem, NavigationItemContainer } from './NavigationItem';
import Link from 'next/link';
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;
currentVersionColor?: string;
}
export function Header({ currentVersion, currentVersionColor }: HeaderProps) {
const [menuVisible, setMenuVisible] = React.useState(false);
return (
<Container>
<HeaderLeft>
<Link href={'/'}>
<a>
<LogoContainer>
<LogoIcon>
<AnimatedSvg />
</LogoIcon>
<Logo src={'/iconoir-logo.svg'} alt={'Iconoir Logo'} />
</LogoContainer>
</a>
</Link>
<CurrentVersion version={currentVersion} color={currentVersionColor} />
</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>
Made with <Heart width={'1em'} height={'1em'} /> by{' '}
<a href={AUTHOR_LINKS.Luca} target={'_blank'} rel={'noreferrer'}>
Luca
</a>{' '}
&amp;{' '}
<a href={AUTHOR_LINKS.Sam} target={'_blank'} rel={'noreferrer'}>
Sam
</a>
</BuiltWith>
</MobileMenuContainer>
</HeaderCenter>
<HeaderRight>
<BuiltWith>
Designed and built with <Heart width={'1em'} height={'1em'} /> by{' '}
<a href={AUTHOR_LINKS.Luca} target={'_blank'} rel={'noreferrer'}>
Luca
</a>{' '}
&amp;{' '}
<a href={AUTHOR_LINKS.Sam} target={'_blank'} rel={'noreferrer'}>
Sam
</a>
</BuiltWith>
<MobileMenuButton onClick={() => setMenuVisible((v) => !v)}>
{menuVisible ? <Cancel /> : <Menu />}
</MobileMenuButton>
</HeaderRight>
</Container>
);
}
export const LogoContainer = styled.div`
position: relative;
z-index: 101;
display: inline-flex;
align-items: center;
transition: 0.1s;
&: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 +97,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,14 +105,13 @@ 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} {
margin-left: auto;
background: none;
padding-top: 0;
box-shadow: none;
@ -115,16 +127,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 +140,59 @@ 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(NavigationItemContainer)<{ 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,136 +1,46 @@
import React, { useEffect, useRef } from 'react';
import React from 'react';
import styled from 'styled-components';
import { media } from '../lib/responsive';
const HeaderContainer = styled.div`
position: relative;
width: fit-content;
margin: auto;
`;
const FloatingIcon = styled.div`
position: absolute;
display: none;
background-repeat: no-repeat;
z-index: -1;
pointer-events: none;
align-items: center;
justify-content: center;
${media.md} {
display: flex;
}
`;
const FloatingIconCellar = styled(FloatingIcon)`
-webkit-transform: rotate(6deg);
-moz-transform: rotate(6deg);
top: -120px;
right: 0px;
width: 200px;
height: 200px;
background-image: url(/cellar.gif);
background-size: 70%;
${media.lg} {
}
`;
const FloatingIconPay = styled(FloatingIcon)`
-webkit-transform: rotate(18deg);
-moz-transform: rotate(18deg);
top: -50px;
right: -100px;
width: 130px;
height: 130px;
background-image: url(/pay-bitcoin.gif);
background-size: 70%;
${media.lg} {
}
`;
const FloatingFaceID = styled(FloatingIcon)`
-webkit-transform: rotate(6deg);
-moz-transform: rotate(6deg);
top: -130px;
right: 380px;
width: 110px;
height: 110px;
background-image: url(/face-id.gif);
background-size: 70%;
${media.lg} {
}
`;
const FloatingCommand = styled(FloatingIcon)`
-webkit-transform: rotate(-7deg);
-moz-transform: rotate(-7deg);
top: -94px;
left: 150px;
width: 110px;
height: 110px;
background-image: url(/command.gif);
background-size: 70%;
${media.lg} {
}
`;
const FloatingFill = styled(FloatingIcon)`
-webkit-transform: rotate(-14deg);
-moz-transform: rotate(-14deg);
top: -64px;
left: -75px;
width: 110px;
height: 110px;
background-image: url(/fill.gif);
background-size: 70%;
${media.lg} {
}
`;
import { media } from './responsive';
export interface HeaderBackgroundProps {
children: React.ReactElement;
src: string;
}
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);
};
}, []);
export function HeaderBackground({ children, src }: HeaderBackgroundProps) {
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" />
<HeaderContainer>
<ImageContainer>
<BackgroundImage src={src} />
</ImageContainer>
{children}
</HeaderContainer>
);
}
const HeaderContainer = styled.div`
position: relative;
`;
const ImageContainer = styled.div`
position: absolute;
top: -100px;
bottom: -100px;
left: -30px;
right: -30px;
${media.lg} {
left: -100px;
right: -100px;
}
z-index: -1;
pointer-events: none;
display: flex;
align-items: center;
justify-content: center;
`;
const BackgroundImage = styled.img`
width: 90%;
max-width: calc(min(1100px, 90vw));
max-height: 60%;
${media.md} {
max-height: 100%;
}
`;

View file

@ -1,16 +0,0 @@
import React from 'react';
import styled from 'styled-components';
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,152 +1,31 @@
import type { Icon as IconType } from './IconList';
import * as AllIcons from 'iconoir-react';
import React from 'react';
import { DEFAULT_CUSTOMIZATIONS, Icon as IconType } from './IconList';
import styled from 'styled-components';
import { showNotification } from '../lib/showNotification';
import * as AllIcons from 'iconoir-react';
import { ResetButton } from './Button';
import { DEFAULT_CUSTOMIZATIONS } from './IconList';
import { showNotification } from '../helpers/showNotification';
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)
);
}
const Overlay = styled.div`
position: absolute;
border-radius: 50%;
border: solid 2px var(--g0);
background: var(--white);
width: 8px;
height: 8px;
`;
const CornerBR = styled(Overlay)`
bottom: -6px;
right: -6px;
z-index: 999;
`;
const CornerTR = styled(Overlay)`
top: -6px;
right: -6px;
`;
const CornerBL = styled(Overlay)`
bottom: -6px;
left: -6px;
`;
const CornerTL = styled(Overlay)`
top: -6px;
left: -6px;
`;
const HoverContainer = styled.div`
position: absolute;
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);
}
}
`;
const BorderContainer = styled.div<{ $iconWidth: number }>`
width: ${(props) => props.$iconWidth}px;
box-sizing: border-box;
padding-bottom: 100%;
position: relative;
border: solid 1px var(--g6);
border-radius: 12px;
margin-bottom: 10px;
@media (hover: hover) {
&:hover ${HoverContainer} {
opacity: 1;
pointer-events: all;
border: solid 2px var(--g0);
}
}
`;
const IconContainer = styled.div`
position: absolute;
inset: 0;
display: flex;
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 }>`
font-size: 11px;
font-weight: 500;
line-height: 14.74px;
color: var(--black-60);
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
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);
@ -154,37 +33,34 @@ export function Icon({ iconWidth, icon }: IconProps) {
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',
typeof window !== 'undefined' &&
typeof window?.navigator?.clipboard?.writeText !== 'undefined'
);
}, []);
React.useEffect(() => {
if (iconContainerRef.current) {
htmlContentsRef.current = bakeSvg(
(iconContainerRef.current.firstChild as SVGElement).outerHTML,
iconContainerRef.current.innerHTML,
iconContext.color || DEFAULT_CUSTOMIZATIONS.hexColor,
iconContext.strokeWidth || DEFAULT_CUSTOMIZATIONS.strokeWidth,
iconContext.strokeWidth || DEFAULT_CUSTOMIZATIONS.strokeWidth
);
}
}, [iconContext, supportsClipboard]);
React.useEffect(() => {
const element = downloadRef.current || (iconContainerRef.current as unknown as HTMLAnchorElement);
const element =
downloadRef.current ||
(iconContainerRef.current as unknown as HTMLAnchorElement);
if (element) {
element.href = `data:image/svg+xml;base64,${btoa(
htmlContentsRef.current,
htmlContentsRef.current
)}`;
}
}, [iconContext, supportsClipboard]);
return (
<div className="icon-container">
<BorderContainer $iconWidth={iconWidth}>
<div className={'icon-container'}>
<BorderContainer iconWidth={iconWidth}>
<IconContainer
ref={iconContainerRef}
{...((supportsClipboard
@ -197,48 +73,103 @@ export function Icon({ iconWidth, icon }: IconProps) {
}) 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}
{supportsClipboard ? (
<HoverContainer>
<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>
<Subtitle>{icon.filename}</Subtitle>
</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;
border-radius: 12px;
overflow: hidden;
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(--light-gray);
border-radius: 0 !important;
transition: background 0.1s linear;
color: var(--black);
font-size: 14px;
line-height: 23px;
font-weight: 700;
text-align: center;
flex: 1;
cursor: pointer;
text-decoration: none;
&:hover,
&:active {
background: var(--gray);
}
`;
const BorderContainer = styled.div<{ iconWidth: number }>`
width: ${(props) => props.iconWidth}px;
box-sizing: border-box;
padding-bottom: 100%;
position: relative;
border: solid 1px var(--light-gray);
border-radius: 12px;
margin-bottom: 10px;
@media (hover: hover) {
&:hover ${HoverContainer} {
opacity: 1;
pointer-events: all;
}
}
`;
const IconContainer = styled.div`
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
`;
const Subtitle = styled.div`
font-size: 11px;
font-weight: 500;
line-height: 14.74px;
color: var(--black-40);
text-align: center;
`;

View file

@ -1,19 +1,17 @@
import type {
ListChildComponentProps,
} from 'react-window';
import { chunk } from 'lodash';
import React from 'react';
import {
areEqual,
VariableSizeList as List,
ListChildComponentProps,
areEqual,
} from 'react-window';
import styled from 'styled-components';
import { chunk } from 'lodash';
import useResizeObserver from 'use-resize-observer';
import { ICON_SPACE, ICON_WIDTH } from '../lib/constants';
import { ICON_SPACE, ICON_WIDTH } from './constants';
import { CategoryRow } from './CategoryRow';
import { IconListEmpty } from './IconListEmpty';
import { IconsRow } from './IconsRow';
import { ReactWindowScroller } from './ReactWindowScroller';
import styled from 'styled-components';
import { IconListEmpty } from './IconListEmpty';
export interface IconListFilters {
search?: string;
@ -42,23 +40,15 @@ function normalizeString(s: string) {
function filterIcons(allIcons: Icon[], filters: IconListFilters): Icon[] {
if (filters.search) {
const normalSearch = normalizeString(filters.search!);
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))
);
});
}
return result;
} else {
return allIcons;
}
return allIcons.filter((icon) => {
const normalSearch = normalizeString(filters.search!);
return (
normalizeString(icon.filename).includes(normalSearch) ||
normalizeString(icon.category).includes(normalSearch) ||
icon.tags.some((tag) => normalizeString(tag).includes(normalSearch))
);
});
} else return allIcons;
}
interface IconCategoryRow {
@ -69,34 +59,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 +93,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 +105,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 +136,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 +167,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;
> :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

@ -1,8 +1,32 @@
import { SpockHandGesture } from 'iconoir-react';
import React from 'react';
import styled from 'styled-components';
import { SUGGEST_ICON_LINK } from '../lib/constants';
import { SUGGEST_ICON } from './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} target={'_blank'} rel={'noreferrer'}>
suggestion on GitHub.
</a>
</Text18>
</Container>
);
}
const Container = styled.div`
margin-top: 90px;
display: flex;
@ -10,7 +34,6 @@ const Container = styled.div`
flex-direction: column;
text-align: center;
`;
const IconContainer = styled.div`
svg {
width: 60px;
@ -19,37 +42,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,14 @@
import type { Icon } from './IconList';
import React from 'react';
import { ICON_SPACE } from './constants';
import { 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;
}
`;
export interface IconsRowProps {
icons: Icon[];
style?: any;
iconWidth: number;
}
export function IconsRow({ icons, style, iconWidth }: IconsRowProps) {
return (
<RowContainer style={style}>
@ -26,3 +18,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,77 +19,38 @@ 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(--super-light-gray);
border: 1px solid var(--gray);
border-radius: 10px;
overflow: hidden;
padding: 6px;
text-align: center;
font-size: 13px;
line-height: 21px;
font-weight: 500;
color: var(--black);
`;
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: 75px;
font-size: 16px;
line-height: 26px;
border-radius: 12px;
padding: 0 23px;
text-align: left;
width: 100%;
box-sizing: border-box;
outline: none;
`;
export const ColorButton = styled.div`
width: 24px;
height: 24px;
background-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='51px' height='51px' stroke-width='2.3' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg' color='%23000000'%3E%3Cpath d='M7 13.161l5.464-5.464a1 1 0 011.415 0l2.12 2.12a1 1 0 010 1.415l-1.928 1.929m-7.071 0l-2.172 2.172a.999.999 0 00-.218.327l-1.028 2.496c-.508 1.233.725 2.466 1.958 1.959l2.497-1.028c.122-.05.233-.125.326-.218l5.708-5.708m-7.071 0h7.071M13.878 3.454l2.121 2.121m4.243 4.243l-2.121-2.121m-2.122-2.122l1.414-1.414a1 1 0 011.415 0l.707.707a1 1 0 010 1.414L18.12 7.697m-2.122-2.122l2.122 2.122' stroke='%23000000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3C/path%3E%3C/svg%3E");
background-repeat: no-repeat;
background-size: 100%;
right: 20px;
position: absolute;
pointer-events: none;
&:hover {
transition: 0.2s;
scale: 1.2;
}
`;
export const ColorInput = styled.input`
export const ColorInput = styled(Input)`
padding: 0px;
border: none;
cursor: pointer;
width: 30px;
height: 30px;
&::-webkit-color-swatch,
&::-moz-color-swatch {
border: none;
}
background-color: var(--gray-200);
opacity: 0;
transition: 0.2s;
&:hover + ${ColorButton} {
transition: 0.2s;
scale: 1.3;
}
`;

View file

@ -1,20 +1,12 @@
import React from 'react';
import styled from 'styled-components';
import { media } from '../lib/responsive';
import { GA } from './GA';
const Container = styled.div`
padding: 50px 30px;
${media.lg} {
padding: 30px 50px 50px 50px;
}
`;
export function Layout({ children }: React.PropsWithChildren) {
export interface LayoutProps {}
export function Layout({ children }: React.PropsWithChildren<LayoutProps>) {
return (
<Container>
<div>
<GA />
{children}
</Container>
</div>
);
}

View file

@ -1,7 +1,8 @@
import type { MDXRemoteProps } from 'next-mdx-remote';
import { MDXRemote as CoreMDXRemote } from 'next-mdx-remote';
import React from 'react';
import { MDXRemote as CoreMDXRemote, MDXRemoteProps } from 'next-mdx-remote';
import { SuggestLibrary } from './SuggestLibrary';
import { Pre, Body, H1, H2, H3, Code } from './Typography';
import { Table } from './Table';
import { Body, Code, H1, H2, H3, InlineCode, Li, Pre } from './Typography';
export function MDXRemote(props: MDXRemoteProps) {
return (
@ -16,8 +17,7 @@ export function MDXRemote(props: MDXRemoteProps) {
h2: H2,
h3: H3,
table: Table,
code: InlineCode,
li: Li,
SuggestLibrary,
}}
/>
);

View file

@ -2,111 +2,70 @@ import Link from 'next/link';
import { useRouter } from 'next/router';
import React from 'react';
import styled from 'styled-components';
import { media } from '../lib/responsive';
import { media } from './responsive';
import { Text15 } from './Typography';
const NavigationItemContainer = styled(Text15)<{
$text: string;
$isActive?: boolean;
}>`
&&& {
font-weight: ${(props) => (props.$isActive ? '700' : '500')};
font-size: 18px;
line-height: 28px;
text-decoration: none;
white-space: nowrap;
padding: 24px;
color: var(--black);
text-align: center;
width: 100%;
box-sizing: border-box;
position: relative;
z-index: 2;
transition: background 0.1s linear;
&:not(:last-child) {
border-bottom: solid 1px var(--light-gray);
}
${media.lg} {
font-size: 15px;
line-height: 20px;
font-weight: ${(props) => (props.$isActive ? '600' : '500')};
padding: 0;
color: var(--g0);
width: auto;
border-bottom: none !important;
transition: 0.2s;
/* Prevent layout shift */
display: inline-flex;
flex-direction: column;
&::after {
content: '${(props) => props.$text}';
height: 0;
visibility: hidden;
overflow: hidden;
user-select: none;
pointer-events: none;
font-weight: 600;
}
&::before {
position: absolute;
z-index: -1;
content: '';
display: block;
top: -12px;
bottom: -12px;
left: -16px;
right: -16px;
border-radius: 10px;
transition: background 0.1s linear;
}
&:hover::before {
background: var(--g6);
transition: 0.2s;
top: -16px;
bottom: -16px;
left: -20px;
right: -20px;
}
}
}
`;
export interface NavigationItemProps {
href: string;
activeMatch?: string;
children: React.ReactElement | string;
style?: any;
}
export function NavigationItem({
href,
activeMatch,
children,
style,
}: NavigationItemProps) {
export function NavigationItem({ href, children, style }: NavigationItemProps) {
const router = useRouter();
return (
<Link href={href} passHref legacyBehavior>
<Link href={href} passHref>
<NavigationItemContainer
as="a"
style={style}
$text={children.toString()}
$isActive={
activeMatch
? router.asPath.startsWith(activeMatch)
: href.slice(1)
? router.asPath.slice(1).startsWith(href.slice(1))
: router.asPath === href
as={'a'}
isActive={
href.slice(1)
? router.asPath.slice(1).startsWith(href.slice(1))
: router.asPath === href
}
style={style}
>
{children}
</NavigationItemContainer>
</Link>
);
}
export const NavigationItemContainer = styled(Text15)<{ isActive?: boolean }>`
font-weight: 700;
font-size: 18px;
line-height: 28px;
text-decoration: none;
white-space: nowrap;
padding: 25px;
color: var(--black);
text-align: center;
width: 100%;
box-sizing: border-box;
position: relative;
z-index: 2;
&:not(:last-child) {
border-bottom: solid 1px var(--light-gray);
}
${media.lg} {
font-size: 15px;
line-height: 20px;
font-weight: 500;
padding: 0;
color: var(--black-60);
width: auto;
border-bottom: none !important;
${(props) => (props.isActive ? '&' : '&.noop')} {
color: var(--white);
&::before {
position: absolute;
z-index: -1;
content: '';
display: block;
top: -18px;
bottom: -18px;
left: -24px;
right: -24px;
background: var(--black);
}
}
}
`;

View file

@ -1,9 +1,102 @@
import React from 'react';
import styled from 'styled-components';
import { media } from '../lib/responsive';
import { PraiseItem } from './PraiseItem';
import { media } from './responsive';
const NUM_PRAISE_ITEMS = 3;
export function Praise() {
const containerRef = React.useRef<HTMLDivElement>(null);
const indicatorContainerRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
if (containerRef.current) {
const handle = () => {
if (indicatorContainerRef.current && containerRef.current) {
const currentScrollLeft = containerRef.current.scrollLeft;
const totalScroll = containerRef.current.scrollWidth;
const interval = totalScroll / NUM_PRAISE_ITEMS;
const currentIndex =
currentScrollLeft >=
containerRef.current.scrollWidth - window.innerWidth - 100
? indicatorContainerRef.current.children.length - 1
: Math.round(currentScrollLeft / interval);
for (
let i = 0;
i < indicatorContainerRef.current.children.length;
i++
) {
const child = indicatorContainerRef.current.children[i];
if (currentIndex === i) {
child.classList.add('active');
} else {
child.classList.remove('active');
}
}
}
};
const element = containerRef.current;
element.addEventListener('scroll', handle);
return () => {
element.removeEventListener('scroll', handle);
};
}
}, []);
return (
<>
<Container ref={containerRef}>
<PraiseItem
name={'Riccardo Suardi'}
position={'Nibol CEO'}
description={
<>
In Nibol we decided to use Iconoir to speed up the design process.
We want to focus on the product and let Iconoir help us with the
design.
</>
}
imageUrl={'./riccardo-suardi.png'}
logoUrl={'./nibol-logo.svg'}
logoLink={'https://www.nibol.com/'}
logoAlt={'Nibol Logo'}
/>
<PraiseItem
name={'Fabrizio Rinaldi'}
position={'Mailbrew and Typefully founder'}
description={
<>
There's no shortage of icon packs, and yet I always find myself
browsing iconoir. I love the style and attention to detail, and
how easy it is to grab the perfect icons for my projects.
</>
}
imageUrl={'./fabrizio-rinaldi.png'}
logoUrl={'./typefully-logo.png'}
logoLink={'https://typefully.com/'}
logoAlt={'Typefully Logo'}
/>
<PraiseItem
name={'Chris Messina'}
position={'Entrepreneur and # inventor'}
description={
<>
It's the tiny details that determine the degree of delight your
customers experience from your product. Adopting Iconoir icons
will easily boost your app's delight by a factor of 10!
</>
}
imageUrl={'./chris-messina.png'}
logoUrl={'./twitter-logo.png'}
logoLink={'https://twitter.com/chrismessina'}
logoAlt={'Twitter Logo'}
/>
</Container>
<IndicatorContainer ref={indicatorContainerRef}>
<Indicator className={'active'} />
<Indicator />
<Indicator />
</IndicatorContainer>
</>
);
}
const Container = styled.div`
max-width: 100%;
@ -38,7 +131,6 @@ const Container = styled.div`
}
}
`;
const Indicator = styled.div`
width: 6px;
height: 6px;
@ -49,7 +141,6 @@ const Indicator = styled.div`
background: var(--black);
}
`;
const IndicatorContainer = styled.div`
margin: 40px auto 0 auto;
display: flex;
@ -62,102 +153,3 @@ const IndicatorContainer = styled.div`
display: none;
}
`;
export function Praise() {
const containerRef = React.useRef<HTMLDivElement>(null);
const indicatorContainerRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
if (containerRef.current) {
const handle = () => {
if (indicatorContainerRef.current && containerRef.current) {
const currentScrollLeft = containerRef.current.scrollLeft;
const totalScroll = containerRef.current.scrollWidth;
const interval = totalScroll / NUM_PRAISE_ITEMS;
const currentIndex = currentScrollLeft >= containerRef.current.scrollWidth - window.innerWidth - 100
? indicatorContainerRef.current.children.length - 1
: Math.round(currentScrollLeft / interval);
for (
let i = 0;
i < indicatorContainerRef.current.children.length;
i++
) {
const child = indicatorContainerRef.current.children[i];
if (currentIndex === i) {
child.classList.add('active');
} else {
child.classList.remove('active');
}
}
}
};
const element = containerRef.current;
element.addEventListener('scroll', handle);
return () => {
element.removeEventListener('scroll', handle);
};
}
}, []);
return (
<>
<Container ref={containerRef}>
<PraiseItem
name="Riccardo Suardi"
position="Nibol CEO"
description={(
<>
In Nibol we decided to use Iconoir to speed up the design process.
We want to focus on the product and let Iconoir help us with the
design.
</>
)}
imageUrl="./riccardo-suardi.png"
logoUrl="./nibol-logo.svg"
logoLink="https://www.nibol.com/"
logoAlt="Nibol Logo"
/>
<PraiseItem
name="Fabrizio Rinaldi"
position="Mailbrew and Typefully founder"
description={(
<>
There&apos;s no shortage of icon packs, and yet I always find myself
browsing iconoir. I love the style and attention to detail, and
how easy it is to grab the perfect icons for my projects.
</>
)}
imageUrl="./fabrizio-rinaldi.png"
logoUrl="./typefully-logo.png"
logoLink="https://typefully.com/"
logoAlt="Typefully Logo"
/>
<PraiseItem
name="Chris Messina"
position="Entrepreneur and # inventor"
description={(
<>
It&apos;s the tiny details that determine the degree of delight your
customers experience from your product. Adopting Iconoir icons
will easily boost your app&apos;s delight by a factor of 10!
</>
)}
imageUrl="./chris-messina.png"
logoUrl="./twitter-logo.png"
logoLink="https://twitter.com/chrismessina"
logoAlt="Twitter Logo"
/>
</Container>
<IndicatorContainer ref={indicatorContainerRef}>
<Indicator className="active" />
<Indicator />
<Indicator />
</IndicatorContainer>
</>
);
}

View file

@ -1,42 +1,8 @@
import React from 'react';
import styled from 'styled-components';
import { media } from '../lib/responsive';
import { media } from './responsive';
import { Text14, Text18 } from './Typography';
const Container = styled.div`
display: flex;
align-items: flex-start;
flex-direction: row;
flex-shrink: 0;
width: calc(100vw - 60px);
scroll-snap-align: center;
${media.xs} {
width: 428px;
}
`;
const AuthorImage = styled.img`
height: 60px;
width: 60px;
margin-right: 28px;
`;
const Logo = styled.img`
height: 23px;
margin-top: 36px;
`;
const Header = styled(Text18)`
&&& {
font-weight: 700;
color: var(--black);
}
`;
const Body = styled(Text18)`
margin-top: 8px;
`;
export interface PraiseItemProps {
name: string;
position: string;
@ -46,7 +12,6 @@ export interface PraiseItemProps {
logoAlt: string;
imageUrl: string;
}
export function PraiseItem({
name,
position,
@ -63,10 +28,38 @@ export function PraiseItem({
<Header>{name}</Header>
<Text14>{position}</Text14>
<Body>{description}</Body>
<a href={logoLink} target="_blank" rel="noreferrer">
<a href={logoLink} target={'_blank'} rel={'noreferrer'}>
<Logo src={logoUrl} alt={logoAlt} />
</a>
</div>
</Container>
);
}
const Container = styled.div`
display: flex;
align-items: flex-start;
flex-direction: row;
flex-shrink: 0;
width: calc(100vw - 60px);
scroll-snap-align: center;
${media.xs} {
width: 428px;
}
`;
const AuthorImage = styled.img`
height: 60px;
width: 60px;
margin-right: 28px;
`;
const Logo = styled.img`
height: 23px;
margin-top: 36px;
`;
const Header = styled(Text18)`
font-weight: 700;
color: var(--black);
`;
const Body = styled(Text18)`
margin-top: 8px;
`;

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