mirror of
https://github.com/iconoir-icons/iconoir
synced 2026-03-15 14:25:46 +01:00
Compare commits
No commits in common. "main" and "v6.6.0" have entirely different histories.
7256 changed files with 195697 additions and 21746 deletions
14
.eslintrc.cjs
Normal file
14
.eslintrc.cjs
Normal 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'],
|
||||
},
|
||||
};
|
||||
14
.github/ISSUE_TEMPLATE/bug_report.md
vendored
14
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
|
@ -13,21 +13,21 @@ Before reporting an issue, please search to see if someone has filed a similar i
|
|||
|
||||
## Prerequisites
|
||||
|
||||
- Version:
|
||||
- Are you running from source/main:
|
||||
- Are you using a released build:
|
||||
- Operating system:
|
||||
* Version:
|
||||
* Are you running from source/main:
|
||||
* Are you using a released build:
|
||||
* Operating system:
|
||||
|
||||
## Step to reproduce
|
||||
|
||||
_(Type here)_
|
||||
*(Type here)*
|
||||
|
||||
### Actual behavior
|
||||
|
||||
## Any message or error
|
||||
|
||||
_(Type here)_
|
||||
*(Type here)*
|
||||
|
||||
## Additional info or screenshots
|
||||
|
||||
- Screenshots
|
||||
* Screenshots
|
||||
|
|
|
|||
6
.github/ISSUE_TEMPLATE/icon_request.md
vendored
6
.github/ISSUE_TEMPLATE/icon_request.md
vendored
|
|
@ -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:
|
||||
|
|
|
|||
36
.github/actions/setup/action.yml
vendored
36
.github/actions/setup/action.yml
vendored
|
|
@ -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
|
||||
34
.github/workflows/build.yaml
vendored
34
.github/workflows/build.yaml
vendored
|
|
@ -13,15 +13,37 @@ jobs:
|
|||
if: github.repository_owner == 'iconoir-icons'
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- name: Build CSS
|
||||
run: pnpm run build css
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 7
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
|
||||
- name: Commit changes
|
||||
uses: stefanzweifel/git-auto-commit-action@v6
|
||||
uses: stefanzweifel/git-auto-commit-action@v4
|
||||
with:
|
||||
commit_message: Update build artifacts
|
||||
|
|
|
|||
28
.github/workflows/ci.yaml
vendored
28
.github/workflows/ci.yaml
vendored
|
|
@ -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 }}
|
||||
49
.github/workflows/release.yaml
vendored
49
.github/workflows/release.yaml
vendored
|
|
@ -9,20 +9,38 @@ jobs:
|
|||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
# We have to checkout main or PNPM fails. Tag should be on main anyway.
|
||||
ref: main
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-registry: https://registry.npmjs.org
|
||||
node-version: 16
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 7
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Generate changelog file
|
||||
uses: rhysd/changelog-from-release/action@v3
|
||||
|
|
@ -41,7 +59,7 @@ jobs:
|
|||
TAG_NAME: ${{ github.ref_name }}
|
||||
|
||||
- name: Commit release
|
||||
uses: stefanzweifel/git-auto-commit-action@v6
|
||||
uses: stefanzweifel/git-auto-commit-action@v4
|
||||
with:
|
||||
commit_message: Release Version ${{ github.ref_name }}
|
||||
branch: main
|
||||
|
|
@ -55,10 +73,21 @@ jobs:
|
|||
run: pnpm -r publish --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
NPM_CONFIG_PROVENANCE: true
|
||||
continue-on-error: true
|
||||
|
||||
- name: Publish Flutter
|
||||
uses: k-paxian/dart-package-publisher@v1.6
|
||||
uses: k-paxian/dart-package-publisher@v1.5.1
|
||||
with:
|
||||
credentialJson: ${{ secrets.PUB_CREDENTIAL_JSON }}
|
||||
relativePath: ./packages/iconoir-flutter
|
||||
|
||||
- name: Trigger Website Workflow
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
github.rest.actions.createWorkflowDispatch({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
workflow_id: 'website.yaml',
|
||||
ref: 'main',
|
||||
})
|
||||
|
|
|
|||
62
.github/workflows/website.yaml
vendored
62
.github/workflows/website.yaml
vendored
|
|
@ -1,59 +1,79 @@
|
|||
name: Website
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
workflow_run:
|
||||
workflows:
|
||||
- Release
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}
|
||||
group: pages
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- name: Build
|
||||
run: pnpm run build react
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 7
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build packages
|
||||
run: pnpm run dist
|
||||
|
||||
- name: Build website
|
||||
run: pnpm run build
|
||||
run: ./node_modules/.bin/next build
|
||||
working-directory: iconoir.com
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Export website
|
||||
run: ./node_modules/.bin/next export
|
||||
working-directory: iconoir.com
|
||||
|
||||
- name: Setup GitHub Pages
|
||||
uses: actions/configure-pages@v5
|
||||
uses: actions/configure-pages@v2
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
uses: actions/upload-pages-artifact@v1
|
||||
with:
|
||||
path: ./iconoir.com/out
|
||||
path: './iconoir.com/out'
|
||||
|
||||
deploy:
|
||||
name: Deploy
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
uses: actions/deploy-pages@v1
|
||||
|
|
|
|||
8
.gitignore
vendored
8
.gitignore
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
22
|
||||
1
.npmrc
Normal file
1
.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
strict-peer-dependencies=false
|
||||
3
.prettierrc.json
Normal file
3
.prettierrc.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"singleQuote": true
|
||||
}
|
||||
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"recommendations": ["dbaeumer.vscode-eslint"]
|
||||
}
|
||||
51
.vscode/settings.json
vendored
51
.vscode/settings.json
vendored
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
105
README.md
105
README.md
|
|
@ -1,70 +1,73 @@
|
|||
# Iconoir
|
||||
|
||||
[](https://github.com/iconoir-icons/iconoir/releases)
|
||||
[](https://github.com/iconoir-icons/iconoir)
|
||||
[](https://github.com/iconoir-icons/iconoir/blob/main/LICENSE)
|
||||
[](https://discord.gg/txXcKCAmKW)
|
||||
<div>
|
||||
|
||||
<a href="https://github.com/lucaburgio/iconoir/releases">
|
||||
<img src="https://img.shields.io/github/v/release/lucaburgio/iconoir?style=flat-square" alt="Version" />
|
||||
</a>
|
||||
<a href="https://github.com/lucaburgio/iconoir">
|
||||
<img src="https://img.shields.io/github/stars/lucaburgio/iconoir?style=flat-square" alt="Project Stars" />
|
||||
</a>
|
||||
<a href="https://www.npmjs.com/package/iconoir-react">
|
||||
<img src="https://img.shields.io/npm/dm/iconoir-react?color=98E8F3&label=react&style=flat-square" alt="React Library" />
|
||||
</a>
|
||||
<a href="https://github.com/lucaburgio/iconoir/blob/main/LICENSE">
|
||||
<img src="https://img.shields.io/github/license/lucaburgio/iconoir?style=flat-square" alt="License" />
|
||||
</a>
|
||||
<a href="https://discord.gg/txXcKCAmKW">
|
||||
<img src="https://img.shields.io/discord/998909400234348615?color=5865f2&label=Discord&style=flat-square" alt="Discord" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
## What is Iconoir?
|
||||
|
||||
Iconoir is an open-source library with 1600+ unique SVG icons, designed on a 24x24 pixels grid.
|
||||
Iconoir is an open-source library with 1300+ unique SVG icons, designed on a 24x24 pixels grid. No premium icons, no email sign-up, no newsletters.
|
||||
|
||||
<a href="https://iconoir.com"><strong>Browse at iconoir.com →</strong></a>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Basic Usage
|
||||
|
||||
You can download any icon of the pack directly from https://iconoir.com or get them from this repository.
|
||||
|
||||
The icons are also available via the [`iconoir`](https://www.npmjs.com/package/iconoir) NPM package:
|
||||
|
||||
| npm | Yarn | pnpm | Bun |
|
||||
| --------------- | ------------------ | ------------------ | ----------------- |
|
||||
| `npm i iconoir` | `yarn add iconoir` | `pnpm add iconoir` | `bun add iconoir` |
|
||||
Additionally, the icons are available via the `iconoir` NPM package:
|
||||
```bash
|
||||
yarn add iconoir
|
||||
# or
|
||||
npm i iconoir
|
||||
```
|
||||
|
||||
Example usage:
|
||||
|
||||
```js
|
||||
import Iconoir from 'iconoir/icons/iconoir.svg';
|
||||
import Iconoir from 'iconoir/icons/iconoir.svg'
|
||||
```
|
||||
|
||||
## React
|
||||
|
||||
A React library is available under the name `iconoir-react`.
|
||||
|
||||
For more details, see the package [README](./packages/iconoir-react).
|
||||
A React library is available to install under the name `iconoir-react`. For more details, see the package [README](./packages/iconoir-react).
|
||||
|
||||
## React Native
|
||||
|
||||
A React Native library is available under the name `iconoir-react-native`.
|
||||
|
||||
For more details, see the package [README](./packages/iconoir-react-native).
|
||||
|
||||
## 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/iconoir-icons/iconoir@main/css/iconoir.css">
|
||||
```
|
||||
|
||||
Here is an example in HTML:
|
||||
|
|
@ -72,7 +75,6 @@ Here is an example in HTML:
|
|||
```html
|
||||
<i class="iconoir-hand-brake"></i>
|
||||
```
|
||||
|
||||
The class must always be "iconoir-" and then the name of the icon. You can find the names of the icons [here](https://iconoir.com).
|
||||
|
||||
The icons are `display: inline-block` and default to the current font size. You can control this
|
||||
|
|
@ -82,39 +84,6 @@ by adjusting the `::before` styles of the element (which is where the icons are
|
|||
|
||||
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
BIN
assets/cover.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
542
bin/build.js
Normal file
542
bin/build.js
Normal file
|
|
@ -0,0 +1,542 @@
|
|||
import execa from 'execa';
|
||||
import { promises as fs, readFileSync, existsSync } from 'fs';
|
||||
import { generateTemplateFilesBatch } from 'generate-template-files';
|
||||
import { Listr } from 'listr2';
|
||||
import os from 'os';
|
||||
import path, { basename, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { incompatibleNames, flutterIncompatibleNames } from '../constants.js';
|
||||
|
||||
// Paths
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const rootDir = path.join(__dirname, '..');
|
||||
const iconoirIconsDir = path.join(rootDir, 'icons');
|
||||
const ignoreCleanFilenames = ['IconoirContext.tsx', 'server'];
|
||||
|
||||
// Targets for building icons
|
||||
const targets = {
|
||||
'meta-data': { path: 'meta-data.json' },
|
||||
css: { path: 'css/iconoir.css' },
|
||||
'iconoir-flutter': { flutter: true, path: 'packages/iconoir-flutter' },
|
||||
'iconoir-react': { react: true, path: 'packages/iconoir-react' },
|
||||
'iconoir-react-native': {
|
||||
react: true,
|
||||
path: 'packages/iconoir-react-native',
|
||||
},
|
||||
};
|
||||
|
||||
// Get targets from command line arguments
|
||||
// (build all targets if no arguments)
|
||||
const args = process.argv.slice(2);
|
||||
const cliTargets = [];
|
||||
args.forEach((target) => {
|
||||
if (target in targets) {
|
||||
cliTargets.push(target);
|
||||
} else {
|
||||
console.error(`Target '${target}' doesn't exist!\n\nPossible targets are:`);
|
||||
for (const [targetName] of Object.entries(targets)) {
|
||||
console.log(`- ${targetName}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Build tasks
|
||||
const tasks = new Listr(
|
||||
[
|
||||
{
|
||||
title: 'Fetching icons',
|
||||
task: async (ctx) => {
|
||||
ctx.iconoirIconsFiles = await fs.readdir(iconoirIconsDir);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Building targets',
|
||||
skip: (ctx) => !ctx.iconoirIconsFiles,
|
||||
task: (_, task) =>
|
||||
task.newListr(
|
||||
[
|
||||
{
|
||||
title: 'Building meta-data file',
|
||||
enabled: () =>
|
||||
cliTargets.length === 0 || cliTargets.includes('meta-data'),
|
||||
task: async (ctx) => {
|
||||
await fs.writeFile(
|
||||
path.join(rootDir, targets['meta-data'].path),
|
||||
JSON.stringify({ icons: ctx.iconoirIconsFiles })
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Building CSS file',
|
||||
enabled: () =>
|
||||
cliTargets.length === 0 || cliTargets.includes('css'),
|
||||
task: async (ctx) => {
|
||||
const content = [
|
||||
(
|
||||
await fs.readFile(
|
||||
path.join(__dirname, 'header.css'),
|
||||
'utf8'
|
||||
)
|
||||
).replace('[YEAR]', new Date().getFullYear()),
|
||||
];
|
||||
ctx.iconoirIconsFiles.forEach((file) => {
|
||||
const fileContents = readFileSync(
|
||||
path.join(__dirname, '../icons/', file)
|
||||
)
|
||||
.toString()
|
||||
.replace(/\n/g, '')
|
||||
.replace(/(width|height)="[0-9]+"/g, '')
|
||||
.replace(/[ ]+/g, ' ');
|
||||
content.push(
|
||||
`.iconoir-${
|
||||
path.parse(file).name
|
||||
}::before{mask-image:url('data:image/svg+xml;charset=utf-8,${fileContents}');-webkit-mask-image:url('data:image/svg+xml;charset=utf-8,${fileContents}');}`
|
||||
);
|
||||
});
|
||||
await fs.writeFile(
|
||||
path.join(rootDir, targets.css.path),
|
||||
content
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Building React libraries',
|
||||
enabled: () =>
|
||||
cliTargets.length === 0 ||
|
||||
cliTargets.filter((cliTarget) => targets[cliTarget]?.react)
|
||||
.length > 0,
|
||||
task: (_, task) =>
|
||||
task.newListr(
|
||||
[
|
||||
{
|
||||
title: 'Creating temporary directory',
|
||||
task: async (ctx) => {
|
||||
try {
|
||||
ctx.tmpDir = await fs.mkdtemp(
|
||||
path.join(os.tmpdir(), 'iconoir-')
|
||||
);
|
||||
} catch (err) {
|
||||
ctx.skip = true;
|
||||
throw new Error(err.message);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'Copying icon files to temporary directory, while renaming icons with incompatible names',
|
||||
skip: (ctx) => ctx.skip,
|
||||
task: async (ctx) => {
|
||||
try {
|
||||
const promises = ctx.iconoirIconsFiles.map((file) => {
|
||||
const srcFilePath = path.join(
|
||||
iconoirIconsDir,
|
||||
file
|
||||
);
|
||||
const iconName = file.split('.')[0];
|
||||
const dstFileName =
|
||||
iconName in incompatibleNames
|
||||
? incompatibleNames[iconName]
|
||||
: iconName;
|
||||
const dstFilePath = path.join(
|
||||
ctx.tmpDir,
|
||||
`${dstFileName}.svg`
|
||||
);
|
||||
|
||||
return fs.copyFile(srcFilePath, dstFilePath);
|
||||
});
|
||||
return Promise.all(promises).catch((err) => {
|
||||
ctx.skip = true;
|
||||
throw new Error(err.message);
|
||||
});
|
||||
} catch (err) {
|
||||
ctx.skip = true;
|
||||
throw new Error(err.message);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
skip: (ctx) => ctx.skip,
|
||||
task: (_, task) => {
|
||||
const targetsToBuild =
|
||||
cliTargets.length > 0
|
||||
? cliTargets.filter(
|
||||
(cliTarget) => targets[cliTarget]?.react
|
||||
)
|
||||
: Object.keys(targets).filter(
|
||||
(target) => targets[target].react
|
||||
);
|
||||
const tasks = targetsToBuild.map((target) => {
|
||||
const builtIconsDir = path.join(
|
||||
rootDir,
|
||||
targets[target].path,
|
||||
'src'
|
||||
);
|
||||
return {
|
||||
title: `Building ${target}`,
|
||||
task: (_, task) =>
|
||||
task.newListr(
|
||||
[
|
||||
{
|
||||
title: 'Cleaning target directory',
|
||||
task: async (ctx) => {
|
||||
try {
|
||||
const files = await fs.readdir(
|
||||
builtIconsDir
|
||||
);
|
||||
const serverFiles = existsSync(
|
||||
path.join(builtIconsDir, 'server')
|
||||
)
|
||||
? (
|
||||
await fs.readdir(
|
||||
path.join(
|
||||
builtIconsDir,
|
||||
'server'
|
||||
)
|
||||
)
|
||||
).map((file) => `server/${file}`)
|
||||
: [];
|
||||
const promises = [
|
||||
...files,
|
||||
...serverFiles,
|
||||
]
|
||||
.filter(
|
||||
(file) =>
|
||||
!ignoreCleanFilenames.includes(
|
||||
path.basename(file)
|
||||
)
|
||||
)
|
||||
.map((file) => {
|
||||
return fs.unlink(
|
||||
path.join(builtIconsDir, file)
|
||||
);
|
||||
});
|
||||
return Promise.all(promises).catch(
|
||||
(err) => {
|
||||
ctx[target] = { skip: true };
|
||||
throw new Error(err.message);
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
ctx[target] = { skip: true };
|
||||
throw new Error(err.message);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Building icon files',
|
||||
skip: (ctx) => ctx[target]?.skip,
|
||||
task: async (ctx) => {
|
||||
try {
|
||||
await execa(
|
||||
'svgr',
|
||||
[
|
||||
'--config-file',
|
||||
path.join(
|
||||
targets[target].path,
|
||||
'.svgrrc.json'
|
||||
),
|
||||
'--out-dir',
|
||||
builtIconsDir,
|
||||
'--template',
|
||||
'bin/templates/icon-template.cjs',
|
||||
'--index-template',
|
||||
'bin/templates/index-template.cjs',
|
||||
ctx.tmpDir,
|
||||
],
|
||||
{ preferLocal: true }
|
||||
);
|
||||
} catch (err) {
|
||||
throw new Error(err.message);
|
||||
}
|
||||
},
|
||||
},
|
||||
...(target === 'iconoir-react'
|
||||
? [
|
||||
{
|
||||
title:
|
||||
'Building icon files (server components)',
|
||||
skip: (ctx) => ctx[target]?.skip,
|
||||
task: async (ctx) => {
|
||||
try {
|
||||
await execa(
|
||||
'svgr',
|
||||
[
|
||||
'--config-file',
|
||||
path.join(
|
||||
targets[target].path,
|
||||
'.svgrrc.json'
|
||||
),
|
||||
'--out-dir',
|
||||
path.join(
|
||||
builtIconsDir,
|
||||
'server'
|
||||
),
|
||||
'--template',
|
||||
'bin/templates/icon-template-server-component.cjs',
|
||||
'--index-template',
|
||||
'bin/templates/index-template.cjs',
|
||||
ctx.tmpDir,
|
||||
],
|
||||
{ preferLocal: true }
|
||||
);
|
||||
} catch (err) {
|
||||
throw new Error(err.message);
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
{ concurrent: false, exitOnError: false }
|
||||
),
|
||||
};
|
||||
});
|
||||
return task.newListr(tasks, {
|
||||
concurrent: true,
|
||||
rendererOptions: { collapse: false },
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
{ concurrent: false }
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Building Flutter libraries',
|
||||
enabled: () =>
|
||||
cliTargets.length === 0 ||
|
||||
cliTargets.filter((cliTarget) => targets[cliTarget]?.flutter)
|
||||
.length > 0,
|
||||
task: (_, task) =>
|
||||
task.newListr(
|
||||
[
|
||||
{
|
||||
title: 'Creating temporary directory',
|
||||
task: async (ctx) => {
|
||||
try {
|
||||
ctx.flutterTmpDir = await fs.mkdtemp(
|
||||
path.join(os.tmpdir(), 'iconoir-')
|
||||
);
|
||||
} catch (err) {
|
||||
ctx.skip = true;
|
||||
throw new Error(err.message);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'Copying icon files to temporary directory, while renaming icons with incompatible names',
|
||||
skip: (ctx) => ctx.skip,
|
||||
task: async (ctx) => {
|
||||
try {
|
||||
const promises = ctx.iconoirIconsFiles.map((file) => {
|
||||
const srcFilePath = path.join(
|
||||
iconoirIconsDir,
|
||||
file
|
||||
);
|
||||
const iconName = file.split('.')[0];
|
||||
const dstFileName =
|
||||
iconName in flutterIncompatibleNames
|
||||
? flutterIncompatibleNames[iconName]
|
||||
: iconName;
|
||||
const dstFilePath = path.join(
|
||||
ctx.flutterTmpDir,
|
||||
`${dstFileName}.svg`
|
||||
);
|
||||
|
||||
ctx.dstFilePaths = [
|
||||
...(ctx.dstFilePaths ?? []),
|
||||
dstFilePath,
|
||||
];
|
||||
|
||||
return fs.copyFile(srcFilePath, dstFilePath);
|
||||
});
|
||||
return Promise.all(promises).catch((err) => {
|
||||
ctx.skip = true;
|
||||
throw new Error(err.message);
|
||||
});
|
||||
} catch (err) {
|
||||
ctx.skip = true;
|
||||
throw new Error(err.message);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
skip: (ctx) => ctx.skip,
|
||||
task: (_, task) => {
|
||||
const targetsToBuild =
|
||||
cliTargets.length > 0
|
||||
? cliTargets.filter(
|
||||
(cliTarget) => targets[cliTarget]?.flutter
|
||||
)
|
||||
: Object.keys(targets).filter(
|
||||
(target) => targets[target].flutter
|
||||
);
|
||||
const tasks = targetsToBuild.map((target) => {
|
||||
const builtIconsDir = path.join(
|
||||
rootDir,
|
||||
targets[target].path,
|
||||
'lib'
|
||||
);
|
||||
return {
|
||||
title: `Building ${target}`,
|
||||
task: (_, task) =>
|
||||
task.newListr(
|
||||
[
|
||||
{
|
||||
title: 'Cleaning target directory',
|
||||
task: async (ctx) => {
|
||||
try {
|
||||
const files = await fs.readdir(
|
||||
builtIconsDir
|
||||
);
|
||||
const promises = files.map((file) => {
|
||||
return fs.unlink(
|
||||
path.join(builtIconsDir, file)
|
||||
);
|
||||
});
|
||||
return Promise.all(promises).catch(
|
||||
(err) => {
|
||||
ctx[target] = { skip: true };
|
||||
throw new Error(err.message);
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
ctx[target] = { skip: true };
|
||||
throw new Error(err.message);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Create entry file',
|
||||
task: async () => {
|
||||
await fs.writeFile(
|
||||
path.join(
|
||||
builtIconsDir,
|
||||
'iconoir_flutter.dart'
|
||||
),
|
||||
'library iconoir_flutter;\n\n'
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Building icon files',
|
||||
skip: (ctx) => ctx[target]?.skip,
|
||||
task: async (ctx) => {
|
||||
const finalFileNames = [];
|
||||
try {
|
||||
await Promise.all(
|
||||
ctx.dstFilePaths.map(async (file) => {
|
||||
const svgfilename =
|
||||
path.parse(file).name;
|
||||
// Prefix with Svg if icon name starts with a number
|
||||
const iconname = `${
|
||||
/^\d/.test(svgfilename)
|
||||
? 'Svg'
|
||||
: ''
|
||||
}${svgfilename}`;
|
||||
|
||||
const svgfilecontent = (
|
||||
await fs.readFile(file)
|
||||
).toString();
|
||||
|
||||
await generateTemplateFilesBatch([
|
||||
{
|
||||
option:
|
||||
'Create Icon Flutter Widget',
|
||||
entry: {
|
||||
folderPath:
|
||||
'./bin/templates/__svgfilename__.dart',
|
||||
},
|
||||
dynamicReplacers: [
|
||||
{
|
||||
slot: '__icon__',
|
||||
slotValue: iconname,
|
||||
},
|
||||
{
|
||||
slot: '__svgfilecontent__',
|
||||
slotValue: svgfilecontent,
|
||||
},
|
||||
{
|
||||
slot: '__svgfilename__',
|
||||
slotValue: svgfilename,
|
||||
},
|
||||
],
|
||||
output: {
|
||||
path: './packages/iconoir-flutter/lib/__svgfilename__(snakeCase).dart',
|
||||
pathAndFileNameDefaultCase:
|
||||
'(snakeCase)',
|
||||
},
|
||||
async onComplete(results) {
|
||||
finalFileNames.push(
|
||||
results.output.path
|
||||
);
|
||||
},
|
||||
},
|
||||
]);
|
||||
})
|
||||
);
|
||||
|
||||
finalFileNames.sort();
|
||||
await fs.appendFile(
|
||||
path.join(
|
||||
builtIconsDir,
|
||||
'iconoir_flutter.dart'
|
||||
),
|
||||
finalFileNames
|
||||
.map(
|
||||
(fileName) =>
|
||||
`export './${basename(
|
||||
fileName
|
||||
)}';`
|
||||
)
|
||||
.join('\n')
|
||||
);
|
||||
} catch (err) {
|
||||
throw new Error(err.message);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
{ concurrent: false, exitOnError: false }
|
||||
),
|
||||
};
|
||||
});
|
||||
return task.newListr(tasks, {
|
||||
concurrent: true,
|
||||
rendererOptions: { collapse: false },
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
{ concurrent: false }
|
||||
),
|
||||
},
|
||||
],
|
||||
{ concurrent: true }
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Removing React temporary directory',
|
||||
skip: (ctx) => !ctx.tmpDir,
|
||||
task: async (ctx) => {
|
||||
await fs.rm(ctx.tmpDir, { recursive: true });
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Removing Flutter temporary directory',
|
||||
skip: (ctx) => !ctx.flutterTmpDir,
|
||||
task: async (ctx) => {
|
||||
await fs.rm(ctx.flutterTmpDir, { recursive: true });
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
concurrent: false,
|
||||
exitOnError: false,
|
||||
rendererOptions: { collapse: false, collapseErrors: false },
|
||||
}
|
||||
);
|
||||
|
||||
await tasks.run();
|
||||
|
|
@ -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();
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
};
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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'];
|
||||
|
|
@ -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};
|
||||
`;
|
||||
};
|
||||
}
|
||||
|
|
@ -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'],
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -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(),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
21
bin/header.css
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/*!
|
||||
* Iconoir
|
||||
* Copyright (c) [YEAR] Luca Burgio - https://iconoir.com
|
||||
* License - https://github.com/lucaburgio/iconoir/blob/main/LICENSE (Code: MIT License)
|
||||
* CSS file created by Till Esser (@Wiwaltill) and automated by Pascal Jufer (@paescuj)
|
||||
*/
|
||||
|
||||
*[class^="iconoir-"]::before,
|
||||
*[class*=" iconoir-"]::before {
|
||||
content: " ";
|
||||
display: block;
|
||||
background: currentColor;
|
||||
mask-size: cover;
|
||||
-webkit-mask-size: cover;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
*[class^="iconoir-"],
|
||||
*[class*=" iconoir-"] {
|
||||
display: inline-block;
|
||||
}
|
||||
|
|
@ -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');
|
||||
|
|
|
|||
21
bin/templates/__svgfilename__.dart
Normal file
21
bin/templates/__svgfilename__.dart
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
class __icon__(pascalCase) extends StatelessWidget {
|
||||
final Color? color;
|
||||
final double? width;
|
||||
final double? height;
|
||||
|
||||
const __icon__(pascalCase)({Key? key, this.color, this.width, this.height})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => SvgPicture.string(
|
||||
'''
|
||||
__svgfilecontent__''',
|
||||
colorFilter:
|
||||
color != null ? ColorFilter.mode(color!, BlendMode.srcIn) : null,
|
||||
width: width,
|
||||
height: height,
|
||||
);
|
||||
}
|
||||
22
bin/templates/icon-template-server-component.cjs
Normal file
22
bin/templates/icon-template-server-component.cjs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
const template = (
|
||||
{ template },
|
||||
opts,
|
||||
{ imports, interfaces, componentName, props, jsx, exports }
|
||||
) => {
|
||||
const plugins = ['jsx'];
|
||||
if (opts.typescript) {
|
||||
plugins.push('typescript');
|
||||
}
|
||||
const typeScriptTpl = template.smart({ plugins });
|
||||
return typeScriptTpl.ast`${imports}
|
||||
|
||||
${interfaces}
|
||||
|
||||
function ${componentName}(${props}) {
|
||||
return ${jsx};
|
||||
}
|
||||
${exports}
|
||||
`;
|
||||
};
|
||||
|
||||
module.exports = template;
|
||||
26
bin/templates/icon-template.cjs
Normal file
26
bin/templates/icon-template.cjs
Normal 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;
|
||||
15
bin/templates/index-template.cjs
Normal file
15
bin/templates/index-template.cjs
Normal 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;
|
||||
17
constants.js
Normal file
17
constants.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
export const incompatibleNames = {
|
||||
'1st-medal': 'medal-1st',
|
||||
'4k-display': 'display-4k',
|
||||
'2x2-cell': 'cell-2x2',
|
||||
'360-view': 'view360',
|
||||
github: 'gitHub',
|
||||
'github-outline': 'gitHubOutline',
|
||||
'gitlab-full': 'gitLabFull',
|
||||
linkedin: 'linkedIn',
|
||||
tiktok: 'tikTok',
|
||||
youtube: 'youTube',
|
||||
};
|
||||
|
||||
export const flutterIncompatibleNames = {
|
||||
...incompatibleNames,
|
||||
'color-filter': 'color-filter-icon'
|
||||
};
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
# Iconoir - CSS
|
||||
# Iconoir CSS
|
||||
|
||||
Import the CSS file:
|
||||
Import the CSS File:
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/iconoir-icons/iconoir@main/css/iconoir.css" />
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/iconoir-icons/iconoir@main/css/iconoir.css">
|
||||
```
|
||||
|
||||
Here is an example in HTML:
|
||||
|
|
@ -11,8 +11,9 @@ Here is an example in HTML:
|
|||
```html
|
||||
<i class="iconoir-hand-brake"></i>
|
||||
```
|
||||
|
||||
The class must always be "iconoir-" and then the name of the icon. You can find the names of the icons [here](https://iconoir.com).
|
||||
|
||||
The icons are `display: inline-block` and default to the current font size. You can control this
|
||||
by adjusting the `::before` styles of the element (which is where the icons are added as a mask).
|
||||
|
||||
<SuggestLibrary />
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -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 />
|
||||
|
|
|
|||
115
eslint.config.js
115
eslint.config.js
|
|
@ -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',
|
||||
},
|
||||
});
|
||||
41
examples/next/.gitignore
vendored
41
examples/next/.gitignore
vendored
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
import type { NextConfig } from 'next';
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
experimental: {
|
||||
optimizePackageImports: ['iconoir-react'],
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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"]
|
||||
}
|
||||
36
examples/react-native/.gitignore
vendored
36
examples/react-native/.gitignore
vendored
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"extends": "expo/tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
30
examples/vue/.gitignore
vendored
30
examples/vue/.gitignore
vendored
|
|
@ -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
|
||||
1
examples/vue/env.d.ts
vendored
1
examples/vue/env.d.ts
vendored
|
|
@ -1 +0,0 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
|
||||
createApp(App).mount('#app');
|
||||
|
|
@ -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__/*"]
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
{
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
}
|
||||
],
|
||||
"files": []
|
||||
}
|
||||
|
|
@ -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.*"
|
||||
]
|
||||
}
|
||||
|
|
@ -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)),
|
||||
},
|
||||
},
|
||||
});
|
||||
16
iconoir.com/.eslintrc.json
Normal file
16
iconoir.com/.eslintrc.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"root": true,
|
||||
"extends": ["next/core-web-vitals"],
|
||||
"rules": {
|
||||
"react/no-unescaped-entities": ["off"]
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.js"],
|
||||
"parser": "espree",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2020
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
34
iconoir.com/README.md
Normal file
34
iconoir.com/README.md
Normal 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.
|
||||
|
|
@ -1,53 +1,51 @@
|
|||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const AdContainer = styled.div`
|
||||
#carbonads {
|
||||
margin: 24px 0 0 0;
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
.carbon-wrap {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
> :first-child {
|
||||
margin-right: 12px;
|
||||
}
|
||||
& > a > img {
|
||||
width: 100px;
|
||||
height: 74px;
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
}
|
||||
.carbon-text {
|
||||
color: var(--black-80);
|
||||
font-size: 14px;
|
||||
}
|
||||
.carbon-poweredby {
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: var(--black-40);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export function Ad() {
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
const addedScript = React.useRef(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
|
||||
if (container && !addedScript.current) {
|
||||
addedScript.current = true;
|
||||
const script = document.createElement('script');
|
||||
script.async = true;
|
||||
script.type = 'text/javascript';
|
||||
script.src = '//cdn.carbonads.com/carbon.js?serve=CESDK5QJ&placement=iconoircom';
|
||||
script.src =
|
||||
'//cdn.carbonads.com/carbon.js?serve=CESDK5QJ&placement=iconoircom';
|
||||
script.id = '_carbonads_js';
|
||||
container.appendChild(script);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return <AdContainer ref={containerRef} />;
|
||||
}
|
||||
|
||||
const AdContainer = styled.div`
|
||||
#carbonads {
|
||||
margin: 24px 0 0 0;
|
||||
a {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
.carbon-wrap {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
> :first-child {
|
||||
margin-right: 12px;
|
||||
}
|
||||
& > a > img {
|
||||
width:100px;
|
||||
height:74px;
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
}
|
||||
.carbon-text {
|
||||
color: var(--black-80);
|
||||
font-size: 14px;
|
||||
}
|
||||
.carbon-poweredby {
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: var(--black-40);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import anime from 'animejs';
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
type SetInstances = (instances: anime.AnimeInstance[]) => void;
|
||||
|
||||
function playWithLines1(setInstances: SetInstances): anime.AnimeInstance[] {
|
||||
|
|
@ -10,7 +11,7 @@ function playWithLines1(setInstances: SetInstances): anime.AnimeInstance[] {
|
|||
strokeDashoffset: [anime.setDashoffset, 0],
|
||||
easing: 'easeInOutSine',
|
||||
duration: 1500,
|
||||
delay(_el, i) {
|
||||
delay: function (el, i) {
|
||||
return i * 250;
|
||||
},
|
||||
direction: 'alternate',
|
||||
|
|
@ -18,7 +19,6 @@ function playWithLines1(setInstances: SetInstances): anime.AnimeInstance[] {
|
|||
}),
|
||||
];
|
||||
}
|
||||
|
||||
function playWithLines2(setInstances: SetInstances): anime.AnimeInstance[] {
|
||||
return [
|
||||
anime({
|
||||
|
|
@ -26,7 +26,7 @@ function playWithLines2(setInstances: SetInstances): anime.AnimeInstance[] {
|
|||
strokeDashoffset: [anime.setDashoffset, 0],
|
||||
easing: 'easeInOutSine',
|
||||
duration: 1500,
|
||||
delay(_el, i) {
|
||||
delay: function (el, i) {
|
||||
return i * 250;
|
||||
},
|
||||
direction: 'alternate',
|
||||
|
|
@ -34,7 +34,6 @@ function playWithLines2(setInstances: SetInstances): anime.AnimeInstance[] {
|
|||
}),
|
||||
];
|
||||
}
|
||||
|
||||
function playWithLines3(setInstances: SetInstances): anime.AnimeInstance[] {
|
||||
return [
|
||||
anime({
|
||||
|
|
@ -42,7 +41,7 @@ function playWithLines3(setInstances: SetInstances): anime.AnimeInstance[] {
|
|||
strokeDashoffset: [anime.setDashoffset, 0],
|
||||
easing: 'easeInOutSine',
|
||||
duration: 1500,
|
||||
delay(_el, i) {
|
||||
delay: function (el, i) {
|
||||
return i * 250;
|
||||
},
|
||||
direction: 'alternate',
|
||||
|
|
@ -59,7 +58,6 @@ function playWithLines3(setInstances: SetInstances): anime.AnimeInstance[] {
|
|||
}),
|
||||
];
|
||||
}
|
||||
|
||||
function playWithLines4(setInstances: SetInstances): anime.AnimeInstance[] {
|
||||
return [
|
||||
anime({
|
||||
|
|
@ -67,7 +65,7 @@ function playWithLines4(setInstances: SetInstances): anime.AnimeInstance[] {
|
|||
strokeDashoffset: [anime.setDashoffset, 0],
|
||||
easing: 'easeInOutSine',
|
||||
duration: 1500,
|
||||
delay(_el, i) {
|
||||
delay: function (el, i) {
|
||||
return i * 250;
|
||||
},
|
||||
direction: 'alternate',
|
||||
|
|
@ -78,19 +76,16 @@ function playWithLines4(setInstances: SetInstances): anime.AnimeInstance[] {
|
|||
|
||||
export function AnimatedSvg() {
|
||||
const instancesRef = React.useRef<anime.AnimeInstance[] | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
instancesRef.current = playWithLines1((instances) => {
|
||||
instancesRef.current = instances;
|
||||
});
|
||||
|
||||
return () => {
|
||||
for (const instance of instancesRef.current || []) {
|
||||
instance.pause();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<svg
|
||||
className="playWithLines2"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import styled, { css, keyframes } from 'styled-components';
|
||||
import styled, { keyframes } from 'styled-components';
|
||||
import useResizeObserver from 'use-resize-observer';
|
||||
import {
|
||||
FEEDBACK_LINK,
|
||||
|
|
@ -8,32 +8,86 @@ import {
|
|||
import { media } from '../lib/responsive';
|
||||
import { Text14 } from './Typography';
|
||||
|
||||
export function AvailableFor() {
|
||||
const { ref, width } = useResizeObserver();
|
||||
return (
|
||||
<>
|
||||
<MobileHeader>Available For</MobileHeader>
|
||||
<AvailableForOuter>
|
||||
<AvailableForContainer contentWidth={width || 0} ref={ref}>
|
||||
<DesktopHeader>Available for</DesktopHeader>
|
||||
<a href={LIBRARY_LINKS.React} target={'_blank'} rel={'noreferrer'}>
|
||||
<AvailableForImage
|
||||
src={'/logo-react.svg'}
|
||||
alt={'React Logo'}
|
||||
title={'React'}
|
||||
/>
|
||||
</a>
|
||||
<a href={LIBRARY_LINKS.Flutter} target={'_blank'} rel={'noreferrer'}>
|
||||
<AvailableForImage
|
||||
src={'/logo-flutter.svg'}
|
||||
alt={'Flutter Logo'}
|
||||
title={'Flutter'}
|
||||
/>
|
||||
</a>
|
||||
<a href={LIBRARY_LINKS.Figma} target={'_blank'} rel={'noreferrer'}>
|
||||
<AvailableForImage
|
||||
src={'/logo-figma.svg'}
|
||||
alt={'Figma Logo'}
|
||||
title={'Figma'}
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href={LIBRARY_LINKS.ReactNative}
|
||||
target={'_blank'}
|
||||
rel={'noreferrer'}
|
||||
>
|
||||
<AvailableForImage
|
||||
src={'/logo-react-native.svg'}
|
||||
alt={'React Native Logo'}
|
||||
title={'React Native'}
|
||||
/>
|
||||
</a>
|
||||
<a href={LIBRARY_LINKS.Framer} target={'_blank'} rel={'noreferrer'}>
|
||||
<AvailableForImage
|
||||
src={'/logo-framer.svg'}
|
||||
alt={'Framer Logo'}
|
||||
title={'Framer'}
|
||||
/>
|
||||
</a>
|
||||
<AreYouUsing>
|
||||
<a href={SUGGEST_LIBRARY_LINK} target={'_blank'} rel={'noreferrer'}>
|
||||
<Text14>More?</Text14>
|
||||
</a>
|
||||
<a href={FEEDBACK_LINK} target={'_blank'} rel={'noreferrer'}>
|
||||
<Text14>Are you using the library?</Text14>
|
||||
</a>
|
||||
</AreYouUsing>
|
||||
</AvailableForContainer>
|
||||
</AvailableForOuter>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const AreYouUsing = styled.div`
|
||||
* {
|
||||
white-space: nowrap;
|
||||
}
|
||||
`;
|
||||
|
||||
const MobileHeader = styled(Text14)`
|
||||
&&& {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
text-align: center;
|
||||
${media.lg} {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const DesktopHeader = styled(Text14)`
|
||||
&&& {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
text-align: center;
|
||||
${media.lg} {
|
||||
display: none;
|
||||
${media.lg} {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const DesktopHeader = styled(Text14)`
|
||||
display: none;
|
||||
${media.lg} {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
const AvailableForAnimation = keyframes`
|
||||
5% {
|
||||
transform: translateX(0);
|
||||
|
|
@ -48,7 +102,6 @@ const AvailableForAnimation = keyframes`
|
|||
transform: translateX(0);
|
||||
}
|
||||
`;
|
||||
|
||||
const AvailableForOuter = styled.div`
|
||||
max-width: 100vw;
|
||||
margin: 16px -30px 70px -30px;
|
||||
|
|
@ -59,19 +112,16 @@ const AvailableForOuter = styled.div`
|
|||
padding: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const AvailableForContainer = styled.div<{ $contentWidth: number }>`
|
||||
const AvailableForContainer = styled.div<{ contentWidth: number }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
width: max-content;
|
||||
--content-width: ${(props) => props.$contentWidth}px;
|
||||
${(props) =>
|
||||
props.$contentWidth
|
||||
&& css`
|
||||
animation: ${AvailableForAnimation} 40s cubic-bezier(0.37, 0, 0.63, 1)
|
||||
infinite;
|
||||
`}
|
||||
--content-width: ${(props) => props.contentWidth}px;
|
||||
${(props) => (props.contentWidth ? '&' : '&.noop')} {
|
||||
animation: ${AvailableForAnimation} 40s cubic-bezier(0.37, 0, 0.63, 1)
|
||||
infinite;
|
||||
}
|
||||
> :not(:last-child) {
|
||||
margin-right: 30px;
|
||||
}
|
||||
|
|
@ -82,11 +132,10 @@ const AvailableForContainer = styled.div<{ $contentWidth: number }>`
|
|||
}
|
||||
${media.md} {
|
||||
> :not(:last-child) {
|
||||
margin-right: 40px;
|
||||
margin-right: 60px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const AvailableForImage = styled.img`
|
||||
height: 40px;
|
||||
display: block;
|
||||
|
|
@ -99,107 +148,3 @@ const AvailableForImage = styled.img`
|
|||
height: 50px;
|
||||
}
|
||||
`;
|
||||
|
||||
export function AvailableFor() {
|
||||
const { ref, width } = useResizeObserver();
|
||||
|
||||
return (
|
||||
<>
|
||||
<MobileHeader>Available For</MobileHeader>
|
||||
<AvailableForOuter>
|
||||
<AvailableForContainer $contentWidth={width || 0} ref={ref}>
|
||||
<DesktopHeader>Available for</DesktopHeader>
|
||||
<a href={LIBRARY_LINKS.React} target="_blank" rel="noreferrer">
|
||||
<AvailableForImage
|
||||
src="/logo-react.svg"
|
||||
alt="React Logo"
|
||||
title="React"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/iconoir-icons/iconoir#swift-package"
|
||||
target="_blank"
|
||||
rel="nofollow noreferrer"
|
||||
>
|
||||
<AvailableForImage
|
||||
src="/logo-swift.svg"
|
||||
alt="Swift Logo"
|
||||
title="Swift"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href={LIBRARY_LINKS.Flutter}
|
||||
target="_blank"
|
||||
rel="nofollow noreferrer"
|
||||
>
|
||||
<AvailableForImage
|
||||
src="/logo-flutter.svg"
|
||||
alt="Flutter Logo"
|
||||
title="Flutter"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href={LIBRARY_LINKS.Figma}
|
||||
target="_blank"
|
||||
rel="nofollow noreferrer"
|
||||
>
|
||||
<AvailableForImage
|
||||
src="/logo-figma.svg"
|
||||
alt="Figma Logo"
|
||||
title="Figma"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href={LIBRARY_LINKS.ReactNative}
|
||||
target="_blank"
|
||||
rel="nofollow noreferrer"
|
||||
>
|
||||
<AvailableForImage
|
||||
src="/logo-react-native.svg"
|
||||
alt="React Native Logo"
|
||||
title="React Native"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href={LIBRARY_LINKS.Vue}
|
||||
target="_blank"
|
||||
rel="nofollow noreferrer"
|
||||
>
|
||||
<AvailableForImage
|
||||
src="/logo-vue.svg"
|
||||
alt="Vue Logo"
|
||||
title="Vue"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href={LIBRARY_LINKS.Framer}
|
||||
target="_blank"
|
||||
rel="nofollow noreferrer"
|
||||
>
|
||||
<AvailableForImage
|
||||
src="/logo-framer.svg"
|
||||
alt="Framer Logo"
|
||||
title="Framer"
|
||||
/>
|
||||
</a>
|
||||
<AreYouUsing>
|
||||
<a
|
||||
href={SUGGEST_LIBRARY_LINK}
|
||||
target="_blank"
|
||||
rel="nofollow noreferrer"
|
||||
>
|
||||
<Text14>More?</Text14>
|
||||
</a>
|
||||
<a
|
||||
href={FEEDBACK_LINK}
|
||||
target="_blank"
|
||||
rel="nofollow noreferrer"
|
||||
>
|
||||
<Text14>Are you using the library?</Text14>
|
||||
</a>
|
||||
</AreYouUsing>
|
||||
</AvailableForContainer>
|
||||
</AvailableForOuter>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,74 +12,68 @@ export const ResetButton = styled.button`
|
|||
`;
|
||||
|
||||
export const LargeButton = styled(ResetButton)`
|
||||
&&& {
|
||||
background: var(--white);
|
||||
height: 75px;
|
||||
border-radius: 10px 50px 50px 50px;
|
||||
border: solid 2px var(--g0);
|
||||
background: var(--white);
|
||||
height: 75px;
|
||||
border-radius: 10px 50px 50px 50px;
|
||||
border: solid 2px var(--g0);
|
||||
|
||||
box-shadow: 0px 8px 0px 0px var(--g0);
|
||||
box-shadow: 0px 8px 0px 0px var(--g0);
|
||||
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
color: var(--g0);
|
||||
padding: 0 70px;
|
||||
font-size: 20px;
|
||||
line-height: 26px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
color: var(--g0);
|
||||
padding: 0 70px;
|
||||
font-size: 20px;
|
||||
line-height: 26px;
|
||||
font-weight: 700;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
z-index: 12;
|
||||
> :not(:last-child) {
|
||||
margin-right: 15px;
|
||||
}
|
||||
* {
|
||||
font-weight: 700;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
z-index: 12;
|
||||
> :not(:last-child) {
|
||||
margin-right: 15px;
|
||||
}
|
||||
* {
|
||||
font-weight: 700;
|
||||
}
|
||||
transition: 0.2s;
|
||||
&:hover {
|
||||
box-shadow: 0px 3px 0px 0px var(--g0);
|
||||
transform: translateY(5px);
|
||||
}
|
||||
&:focus::after,
|
||||
&:hover::after {
|
||||
inset: -7px;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
transition: 0.2s;
|
||||
&:hover {
|
||||
box-shadow: 0px 3px 0px 0px var(--g0);
|
||||
transform: translateY(5px);
|
||||
}
|
||||
&:focus::after,
|
||||
&:hover::after {
|
||||
inset: -7px;
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
const Button = styled(LargeButton)`
|
||||
&&&& {
|
||||
height: 40px;
|
||||
font-size: 13px;
|
||||
line-height: 21px;
|
||||
padding: 0 18px;
|
||||
&::after {
|
||||
border-width: 2px;
|
||||
}
|
||||
&:focus::after,
|
||||
&:hover::after {
|
||||
inset: -4px;
|
||||
}
|
||||
&:active {
|
||||
background: var(--darker-gray);
|
||||
}
|
||||
export const Button = styled(LargeButton)`
|
||||
height: 40px;
|
||||
font-size: 13px;
|
||||
line-height: 21px;
|
||||
padding: 0 18px;
|
||||
&::after {
|
||||
border-width: 2px;
|
||||
}
|
||||
&:focus::after,
|
||||
&:hover::after {
|
||||
inset: -4px;
|
||||
}
|
||||
&:active {
|
||||
background: var(--darker-gray);
|
||||
}
|
||||
`;
|
||||
|
||||
export const CopyButton = styled(Button)`
|
||||
&&&&& {
|
||||
text-transform: uppercase;
|
||||
background: var(--white);
|
||||
height: 30px;
|
||||
padding: 0 12px;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.12em;
|
||||
line-height: 17.6px;
|
||||
font-weight: 700;
|
||||
color: var(--black);
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
`;
|
||||
text-transform: uppercase;
|
||||
background: var(--white);
|
||||
height: 30px;
|
||||
padding: 0 12px;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.12em;
|
||||
line-height: 17.6px;
|
||||
font-weight: 700;
|
||||
color: var(--black);
|
||||
font-family: var(--font-family) !important;
|
||||
`;
|
||||
|
|
@ -2,6 +2,25 @@ import styled from 'styled-components';
|
|||
import { media } from '../lib/responsive';
|
||||
import { Text15 } from './Typography';
|
||||
|
||||
export interface CategoryRowProps {
|
||||
category: string;
|
||||
numIcons: number;
|
||||
style?: any;
|
||||
}
|
||||
export function CategoryRow({ category, numIcons, style }: CategoryRowProps) {
|
||||
return (
|
||||
<Container style={style}>
|
||||
<InnerContainer>
|
||||
<Title>{category}</Title>
|
||||
<Text15>
|
||||
{numIcons} Icon{numIcons === 1 ? '' : 's'}
|
||||
</Text15>
|
||||
<Separator />
|
||||
</InnerContainer>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const InnerContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -13,7 +32,6 @@ const InnerContainer = styled.div`
|
|||
margin-right: 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
|
|
@ -23,42 +41,15 @@ const Container = styled.div`
|
|||
padding-bottom: 40px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Title = styled(Text15)`
|
||||
&&& {
|
||||
font-weight: 700;
|
||||
color: var(--g0);
|
||||
background-color: var(--g6);
|
||||
border-radius: 8px;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
font-weight: 700;
|
||||
color: var(--g0);
|
||||
background-color: var(--g6);
|
||||
border-radius: 8px;
|
||||
padding: 6px 10px;
|
||||
`;
|
||||
|
||||
const Separator = styled.div`
|
||||
height: 1px;
|
||||
flex: 1;
|
||||
background: var(--g6);
|
||||
`;
|
||||
|
||||
export interface CategoryRowProps {
|
||||
category: string;
|
||||
numIcons: number;
|
||||
style?: any;
|
||||
}
|
||||
|
||||
export function CategoryRow({ category, numIcons, style }: CategoryRowProps) {
|
||||
return (
|
||||
<Container style={style}>
|
||||
<InnerContainer>
|
||||
<Title>{category}</Title>
|
||||
<Text15>
|
||||
{numIcons}
|
||||
{' '}
|
||||
Icon
|
||||
{numIcons === 1 ? '' : 's'}
|
||||
</Text15>
|
||||
<Separator />
|
||||
</InnerContainer>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,97 +1,21 @@
|
|||
import type { MDXRemoteSerializeResult } from 'next-mdx-remote';
|
||||
import { BoxIso } from 'iconoir-react';
|
||||
import moment from 'moment';
|
||||
import { MDXRemoteSerializeResult } from 'next-mdx-remote';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { media } from '../lib/responsive';
|
||||
import { CopyButton } from './Button';
|
||||
import { MDXRemote } from './MDXRemote';
|
||||
import { media } from '../lib/responsive';
|
||||
import { Code, Text15, Text18 } from './Typography';
|
||||
import { CopyButton } from './Button';
|
||||
|
||||
const EXPAND_HEIGHT = 400;
|
||||
|
||||
const Container = styled.div`
|
||||
margin: 40px 0;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
${media.lg} {
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
margin: 24px 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const ContainerLeft = styled.div`
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12px;
|
||||
${media.lg} {
|
||||
margin-bottom: 0;
|
||||
margin-right: 30px;
|
||||
}
|
||||
`;
|
||||
|
||||
const ContainerIcon = styled.div`
|
||||
font-size: 18px;
|
||||
color: var(--black);
|
||||
margin-right: 18px;
|
||||
`;
|
||||
|
||||
const TitleContainer = styled.div`
|
||||
width: 100px;
|
||||
`;
|
||||
|
||||
const EntryTitle = styled(Text18)`
|
||||
&&& {
|
||||
color: var(--black);
|
||||
font-weight: 700;
|
||||
}
|
||||
`;
|
||||
|
||||
const ExpandContainer = styled.div`
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
right: 23px;
|
||||
`;
|
||||
|
||||
const EntryBody = styled(Code)<{ $expanded?: boolean }>`
|
||||
&&& {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
max-height: ${(props) => (props.$expanded ? 'none' : `${EXPAND_HEIGHT}px`)};
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
* {
|
||||
font-family: var(--code-family);
|
||||
}
|
||||
ul {
|
||||
list-style: none none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
li {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
code {
|
||||
display: inline-block;
|
||||
background: var(--g5);
|
||||
color: var(--black);
|
||||
font-family: var(--font-family) !important;
|
||||
padding: 0 4px;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export interface ChangelogEntryProps {
|
||||
name: string;
|
||||
url: string;
|
||||
created_at: string;
|
||||
body?: MDXRemoteSerializeResult;
|
||||
}
|
||||
|
||||
export function ChangelogEntry({
|
||||
name,
|
||||
url,
|
||||
|
|
@ -101,16 +25,14 @@ export function ChangelogEntry({
|
|||
const [expanded, setExpanded] = React.useState(false);
|
||||
const [shouldExpand, setShouldExpand] = React.useState(false);
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (
|
||||
containerRef.current
|
||||
&& containerRef.current.clientHeight > EXPAND_HEIGHT
|
||||
containerRef.current &&
|
||||
containerRef.current.clientHeight > EXPAND_HEIGHT
|
||||
) {
|
||||
setShouldExpand(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Container ref={containerRef}>
|
||||
<ContainerLeft>
|
||||
|
|
@ -120,8 +42,8 @@ export function ChangelogEntry({
|
|||
<TitleContainer>
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
target={'_blank'}
|
||||
rel={'noreferrer'}
|
||||
style={{ textDecoration: 'none' }}
|
||||
>
|
||||
<EntryTitle>{name}</EntryTitle>
|
||||
|
|
@ -129,18 +51,81 @@ export function ChangelogEntry({
|
|||
<Text15>{moment(created_at).format('MMM DD, YYYY')}</Text15>
|
||||
</TitleContainer>
|
||||
</ContainerLeft>
|
||||
<EntryBody $expanded={expanded}>
|
||||
<EntryBody expanded={expanded}>
|
||||
{body ? <MDXRemote {...body} /> : 'No changelog'}
|
||||
{shouldExpand
|
||||
? (
|
||||
<ExpandContainer>
|
||||
<CopyButton onClick={() => setExpanded((e) => !e)}>
|
||||
{expanded ? 'Collapse' : 'Expand'}
|
||||
</CopyButton>
|
||||
</ExpandContainer>
|
||||
)
|
||||
: null}
|
||||
{shouldExpand ? (
|
||||
<ExpandContainer>
|
||||
<CopyButton onClick={() => setExpanded((e) => !e)}>
|
||||
{expanded ? 'Collapse' : 'Expand'}
|
||||
</CopyButton>
|
||||
</ExpandContainer>
|
||||
) : null}
|
||||
</EntryBody>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
margin: 40px 0 !important;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
${media.lg} {
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
margin: 24px 0 !important;
|
||||
}
|
||||
`;
|
||||
const ContainerLeft = styled.div`
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12px;
|
||||
${media.lg} {
|
||||
margin-bottom: 0;
|
||||
margin-right: 30px;
|
||||
}
|
||||
`;
|
||||
const ContainerIcon = styled.div`
|
||||
font-size: 18px;
|
||||
color: var(--black);
|
||||
margin-right: 18px;
|
||||
`;
|
||||
const TitleContainer = styled.div`
|
||||
width: 100px;
|
||||
`;
|
||||
const EntryTitle = styled(Text18)`
|
||||
color: var(--black);
|
||||
font-weight: 700;
|
||||
`;
|
||||
const ExpandContainer = styled.div`
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
right: 23px;
|
||||
`;
|
||||
const EntryBody = styled(Code)<{ expanded?: boolean }>`
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
max-height: ${(props) => (props.expanded ? 'none' : `${EXPAND_HEIGHT}px`)};
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
* {
|
||||
font-family: var(--code-family);
|
||||
}
|
||||
ul {
|
||||
list-style: none none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
li {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
code {
|
||||
display: inline-block;
|
||||
background: var(--g5) !important;
|
||||
color: var(--black);
|
||||
font-family: var(--font-family) !important;
|
||||
padding: 0 4px;
|
||||
font-size: 18px !important;
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -2,8 +2,20 @@ import Link from 'next/link';
|
|||
import styled from 'styled-components';
|
||||
import { Text13 } from './Typography';
|
||||
|
||||
export interface CurrentVersionProps {
|
||||
version: string;
|
||||
}
|
||||
export function CurrentVersion({ version }: CurrentVersionProps) {
|
||||
return (
|
||||
<Link href={'/docs/changelog'} passHref legacyBehavior>
|
||||
<Container as={'a'}>
|
||||
{version}
|
||||
</Container>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
const Container = styled(Text13)`
|
||||
&&& {
|
||||
color: var(--g1);
|
||||
font-weight: 700;
|
||||
background: var(--g5);
|
||||
|
|
@ -11,25 +23,10 @@ const Container = styled(Text13)`
|
|||
padding: 7px 16px;
|
||||
border-radius: 200px;
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
transition:
|
||||
color 0.1s linear,
|
||||
background 0.1s linear;
|
||||
text-decoration: none !important;
|
||||
transition: color 0.1s linear, background 0.1s linear;
|
||||
&:hover {
|
||||
background: var(--black);
|
||||
background: var(--black) !important;
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export interface CurrentVersionProps {
|
||||
version: string;
|
||||
}
|
||||
|
||||
export function CurrentVersion({ version }: CurrentVersionProps) {
|
||||
return (
|
||||
<Link href="/docs/changelog" passHref legacyBehavior>
|
||||
<Container as="a">{version}</Container>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,62 +1,17 @@
|
|||
import type { IconListCustomizations } from './IconList';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { media } from '../lib/responsive';
|
||||
import { DEFAULT_CUSTOMIZATIONS } from './IconList';
|
||||
import { Button } from './Button';
|
||||
import { DEFAULT_CUSTOMIZATIONS, IconListCustomizations } from './IconList';
|
||||
import { ColorButton, ColorInput } from './Input';
|
||||
import { Slider } from './Slider';
|
||||
import { Text13, Text15 } from './Typography';
|
||||
|
||||
const CustomizationBox = styled.div`
|
||||
background-color: var(--g7);
|
||||
width: 84%;
|
||||
padding: 8%;
|
||||
border-radius: 10px;
|
||||
margin: 24px 0;
|
||||
display: none;
|
||||
${media.md} {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
||||
const Header = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 30px;
|
||||
border-bottom: solid 1px var(--g6);
|
||||
padding-bottom: 10px;
|
||||
`;
|
||||
|
||||
const Field = styled.div`
|
||||
margin-bottom: 18px;
|
||||
`;
|
||||
|
||||
const HorizontalField = styled(Field)`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
const ResetButton = styled(Field)`
|
||||
&&& {
|
||||
margin: initial;
|
||||
text-decoration: underline;
|
||||
color: var(--dark-gray);
|
||||
font-size: 13px;
|
||||
|
||||
&:hover {
|
||||
color: var(--black);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
`;
|
||||
import { media } from '../lib/responsive';
|
||||
|
||||
export interface CustomizationEditorProps {
|
||||
customizations: IconListCustomizations;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
onChange: (customizations: IconListCustomizations) => void;
|
||||
}
|
||||
|
||||
export function CustomizationEditor({
|
||||
customizations,
|
||||
onChange,
|
||||
|
|
@ -64,11 +19,9 @@ export function CustomizationEditor({
|
|||
const [, startTransition] = (React as any).useTransition();
|
||||
const [color, setColor] = React.useState(customizations.hexColor);
|
||||
const [size, setSize] = React.useState(customizations.size);
|
||||
|
||||
const [strokeWidth, setStrokeWidth] = React.useState(
|
||||
customizations.strokeWidth,
|
||||
customizations.strokeWidth
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
setColor(customizations.hexColor);
|
||||
setSize(customizations.size);
|
||||
|
|
@ -86,55 +39,93 @@ export function CustomizationEditor({
|
|||
|
||||
return (
|
||||
<>
|
||||
<CustomizationBox>
|
||||
<Header>
|
||||
<Text15 style={{ fontWeight: 700, color: 'var(--black)' }}>
|
||||
Customize
|
||||
</Text15>
|
||||
<ResetButton onClick={() => onChange(DEFAULT_CUSTOMIZATIONS)}>
|
||||
Reset
|
||||
</ResetButton>
|
||||
</Header>
|
||||
<Field>
|
||||
<Slider
|
||||
label="Optical Size"
|
||||
minValue={16}
|
||||
maxValue={64}
|
||||
value={[size]}
|
||||
formatOptions={{ maximumFractionDigits: 0 }}
|
||||
onChange={(values) => {
|
||||
setSize(values[0]);
|
||||
updateCustomizations({ size: values[0] });
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<Slider
|
||||
label="Stroke Weight"
|
||||
minValue={0.5}
|
||||
maxValue={3}
|
||||
value={[strokeWidth]}
|
||||
step={0.1}
|
||||
formatOptions={{ maximumFractionDigits: 1 }}
|
||||
onChange={(values) => {
|
||||
setStrokeWidth(values[0]);
|
||||
updateCustomizations({ strokeWidth: values[0] });
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
<HorizontalField>
|
||||
<Text13>Color</Text13>
|
||||
<ColorInput
|
||||
type="color"
|
||||
value={color}
|
||||
onChange={(e) => {
|
||||
setColor(e.target.value);
|
||||
updateCustomizations({ hexColor: e.target.value });
|
||||
}}
|
||||
/>
|
||||
<ColorButton />
|
||||
</HorizontalField>
|
||||
</CustomizationBox>
|
||||
<CustomizationBox>
|
||||
<Header>
|
||||
<Text15 style={{ fontWeight: 700, color: 'var(--black)' }}>
|
||||
Customize
|
||||
</Text15>
|
||||
<ResetButton onClick={() => onChange(DEFAULT_CUSTOMIZATIONS)}>Reset</ResetButton>
|
||||
</Header>
|
||||
<Field>
|
||||
<Slider
|
||||
label={'Optical Size'}
|
||||
minValue={16}
|
||||
maxValue={64}
|
||||
value={[size]}
|
||||
formatOptions={{ maximumFractionDigits: 0 }}
|
||||
onChange={(values) => {
|
||||
setSize(values[0]);
|
||||
updateCustomizations({ size: values[0] });
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<Slider
|
||||
label={'Stroke Weight'}
|
||||
minValue={0.5}
|
||||
maxValue={3}
|
||||
value={[strokeWidth]}
|
||||
step={0.1}
|
||||
formatOptions={{ maximumFractionDigits: 1 }}
|
||||
onChange={(values) => {
|
||||
setStrokeWidth(values[0]);
|
||||
updateCustomizations({ strokeWidth: values[0] });
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
<HorizontalField>
|
||||
<Text13>Color</Text13>
|
||||
<ColorInput
|
||||
type={'color'}
|
||||
value={color}
|
||||
onChange={(e) => {
|
||||
setColor(e.target.value);
|
||||
updateCustomizations({ hexColor: e.target.value });
|
||||
}}
|
||||
/>
|
||||
<ColorButton />
|
||||
|
||||
</HorizontalField>
|
||||
</CustomizationBox>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const CustomizationBox = styled.div`
|
||||
background-color:var(--g7);
|
||||
width: 84%;
|
||||
padding: 8%;
|
||||
border-radius: 10px;
|
||||
margin: 24px 0;
|
||||
display: none;
|
||||
${media.md} {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
const Header = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 30px;
|
||||
border-bottom: solid 1px var(--g6);
|
||||
padding-bottom: 10px;
|
||||
`;
|
||||
const Field = styled.div`
|
||||
margin-bottom: 24px;
|
||||
`;
|
||||
const HorizontalField = styled(Field)`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
const ResetButton = styled(Field)`
|
||||
margin: initial;
|
||||
text-decoration: underline;
|
||||
color: var(--dark-gray);
|
||||
font-size: 13px;
|
||||
|
||||
&:hover{
|
||||
color: var(--black);
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,103 +1,15 @@
|
|||
import type { DocumentationItem } from '../pages/docs/[...slug]';
|
||||
import { NavArrowUp } from 'iconoir-react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { DocumentationItem } from '../pages/docs/[...slug]';
|
||||
import { media } from '../lib/responsive';
|
||||
|
||||
const HeaderItemIcon = styled.div<{ $active?: boolean }>`
|
||||
font-size: 13px;
|
||||
transition: transform 0.25s linear;
|
||||
transform: rotate(${(props) => (props.$active ? 180 : 0)}deg);
|
||||
margin-right: 7px;
|
||||
position: relative;
|
||||
top: 6px;
|
||||
svg {
|
||||
display: block;
|
||||
}
|
||||
${media.lg} {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const ChildrenContainer = styled.div<{ $expanded?: boolean }>`
|
||||
display: ${(props) => (props.$expanded ? 'block' : 'none')};
|
||||
${media.lg} {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
||||
const HeaderItem = styled.div`
|
||||
padding: 10px 30px;
|
||||
font-size: 15px;
|
||||
line-height: 19px;
|
||||
color: var(--g0);
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
cursor: pointer;
|
||||
${media.lg} {
|
||||
padding: 22px 45px;
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
&:not(:first-child) {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const NavigationItem = styled.div<{ $active?: boolean }>`
|
||||
padding: 12px 12px 12px 75px;
|
||||
transition:
|
||||
background 0.1s linear,
|
||||
color 0.1s linear;
|
||||
font-size: 16px;
|
||||
line-height: 14.5px;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--g1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
span {
|
||||
font-weight: 400;
|
||||
}
|
||||
> :not(:last-child) {
|
||||
margin-right: 14px;
|
||||
}
|
||||
&:hover,
|
||||
${(props) => (props.$active ? '&' : '&.noop')} {
|
||||
color: var(--g0);
|
||||
text-decoration: underline;
|
||||
}
|
||||
${(props) => (props.$active ? 'span' : '&.noop')} {
|
||||
font-weight: 500;
|
||||
}
|
||||
${media.lg} {
|
||||
padding: 12px 12px 12px 65px;
|
||||
}
|
||||
`;
|
||||
|
||||
const NavigationItemLabel = styled.span`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
line-height: 17.6px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--g1);
|
||||
background: var(--g5);
|
||||
`;
|
||||
|
||||
export interface DocumentationNavigationProps {
|
||||
documentationItems: DocumentationItem[];
|
||||
pathPrefix?: string[];
|
||||
}
|
||||
|
||||
export function DocumentationNavigation({
|
||||
documentationItems,
|
||||
pathPrefix,
|
||||
|
|
@ -105,53 +17,46 @@ export function DocumentationNavigation({
|
|||
const router = useRouter();
|
||||
const activePath = router.asPath.replace('/docs/', '');
|
||||
const [expandedTitles, setExpandedTitles] = React.useState<string[]>([]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const expandedItems = documentationItems.filter((item) => {
|
||||
const normalized = activePath.replace((pathPrefix || []).join('/'), '');
|
||||
|
||||
return (
|
||||
normalized === item.path
|
||||
|| item.children?.some((child) => {
|
||||
normalized === item.path ||
|
||||
item.children?.some((child) => {
|
||||
return activePath.startsWith(
|
||||
[item.path, child.path].filter(Boolean).join('/'),
|
||||
[item.path, child.path].filter(Boolean).join('/')
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
setExpandedTitles(expandedItems.map((item) => item.title));
|
||||
}, [activePath, pathPrefix, documentationItems]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{documentationItems.map((documentationItem) => {
|
||||
const path = [...(pathPrefix || []), documentationItem.path]
|
||||
.filter(Boolean)
|
||||
.join('/');
|
||||
|
||||
if (documentationItem.children?.length) {
|
||||
const active = expandedTitles.includes(documentationItem.title);
|
||||
|
||||
return (
|
||||
<React.Fragment key={documentationItem.title}>
|
||||
<HeaderItem
|
||||
onClick={() => {
|
||||
setExpandedTitles((et) => {
|
||||
const includes = et.includes(documentationItem.title);
|
||||
|
||||
return includes
|
||||
? et.filter((i) => i !== documentationItem.title)
|
||||
: [...et, documentationItem.title];
|
||||
});
|
||||
}}
|
||||
>
|
||||
<HeaderItemIcon $active={active}>
|
||||
<HeaderItemIcon active={active}>
|
||||
<NavArrowUp />
|
||||
</HeaderItemIcon>
|
||||
{documentationItem.title}
|
||||
</HeaderItem>
|
||||
<ChildrenContainer $expanded={active}>
|
||||
<ChildrenContainer expanded={active}>
|
||||
<DocumentationNavigation
|
||||
documentationItems={documentationItem.children}
|
||||
pathPrefix={[
|
||||
|
|
@ -170,15 +75,13 @@ export function DocumentationNavigation({
|
|||
legacyBehavior
|
||||
key={documentationItem.path}
|
||||
>
|
||||
<NavigationItem as="a" $active={activePath === path}>
|
||||
<NavigationItem as={'a'} active={activePath === path}>
|
||||
<span>{documentationItem.title}</span>
|
||||
{documentationItem.label
|
||||
? (
|
||||
<NavigationItemLabel>
|
||||
{documentationItem.label}
|
||||
</NavigationItemLabel>
|
||||
)
|
||||
: null}
|
||||
{documentationItem.label ? (
|
||||
<NavigationItemLabel>
|
||||
{documentationItem.label}
|
||||
</NavigationItemLabel>
|
||||
) : null}
|
||||
</NavigationItem>
|
||||
</Link>
|
||||
);
|
||||
|
|
@ -187,3 +90,85 @@ export function DocumentationNavigation({
|
|||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const HeaderItemIcon = styled.div<{ active?: boolean }>`
|
||||
font-size: 13px;
|
||||
transition: transform 0.25s linear;
|
||||
transform: rotate(${(props) => (props.active ? 180 : 0)}deg);
|
||||
margin-right: 7px;
|
||||
position: relative;
|
||||
top: 6px;
|
||||
svg {
|
||||
display: block;
|
||||
}
|
||||
${media.lg} {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
const ChildrenContainer = styled.div<{ expanded?: boolean }>`
|
||||
display: ${(props) => (props.expanded ? 'block' : 'none')};
|
||||
${media.lg} {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
const HeaderItem = styled.div`
|
||||
padding: 10px 30px;
|
||||
font-size: 15px;
|
||||
line-height: 19px;
|
||||
color: var(--g0);
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
cursor: pointer;
|
||||
${media.lg} {
|
||||
padding: 22px 45px;
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
&:not(:first-child) {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
const NavigationItem = styled.div<{ active?: boolean }>`
|
||||
padding: 12px 45px 12px 75px;
|
||||
transition: background 0.1s linear, color 0.1s linear;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
line-height: 14.5px;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--g1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
span {
|
||||
font-weight: 500;
|
||||
}
|
||||
> :not(:last-child) {
|
||||
margin-right: 14px;
|
||||
}
|
||||
&:hover,
|
||||
${(props) => (props.active ? '&' : '&.noop')} {
|
||||
color: var(--g0);
|
||||
text-decoration: underline;
|
||||
}
|
||||
${(props) => (props.active ? 'span' : '&.noop')} {
|
||||
font-weight: 700;
|
||||
}
|
||||
${media.lg} {
|
||||
padding: 12px 45px 12px 65px;
|
||||
}
|
||||
`;
|
||||
const NavigationItemLabel = styled.span`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
line-height: 17.6px;
|
||||
font-weight: 700 !important;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--g1);
|
||||
background: var(--g5);
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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}>×</CloseButton>
|
||||
</PopupContent>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,72 +1,24 @@
|
|||
import type { Icon, IconListFilters } from './IconList';
|
||||
import { IconoirProvider } from 'iconoir-react';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { media } from '../lib/responsive';
|
||||
import { Ad } from './Ad';
|
||||
import { Sponsor } from './Sponsor';
|
||||
import { CustomizationEditor } from './CustomizationEditor';
|
||||
import { FiltersEditor } from './FiltersEditor';
|
||||
import { IconList } from './IconList';
|
||||
import { Streamline } from './Streamline';
|
||||
import { Icon, IconList, IconListFilters } from './IconList';
|
||||
import { media } from '../lib/responsive';
|
||||
import { useCustomizationPersistence } from './useCustomizationPersistence';
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
${media.md} {
|
||||
align-items: flex-start;
|
||||
flex-direction: row;
|
||||
}
|
||||
`;
|
||||
|
||||
const Left = styled.div`
|
||||
flex: 1;
|
||||
min-height: calc(100vh - 100px);
|
||||
background: white;
|
||||
${media.md} {
|
||||
background: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const Right = styled.div`
|
||||
position: sticky;
|
||||
top: 20px;
|
||||
width: 275px;
|
||||
display: block;
|
||||
z-index: -1;
|
||||
margin: 110px auto;
|
||||
${media.md} {
|
||||
margin-left: 68px;
|
||||
z-index: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
const FilterContainer = styled.div<{ $isMobile?: boolean }>`
|
||||
display: ${(props) => (props.$isMobile ? 'block' : 'none')};
|
||||
margin-bottom: 40px;
|
||||
position: sticky;
|
||||
top: 20px;
|
||||
z-index: 100;
|
||||
width: 100%;
|
||||
${media.md} {
|
||||
position: relative;
|
||||
top: 0;
|
||||
display: ${(props) => (props.$isMobile ? 'none' : 'block')};
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
export interface ExploreProps {
|
||||
allIcons: Icon[];
|
||||
}
|
||||
|
||||
export function Explore({ allIcons }: ExploreProps) {
|
||||
const [filters, setFilters] = React.useState<IconListFilters>({});
|
||||
const [customizations, setCustomizations] = useCustomizationPersistence();
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Left>
|
||||
<FilterContainer $isMobile>
|
||||
<FilterContainer isMobile>
|
||||
<FiltersEditor filters={filters} onChange={setFilters} />
|
||||
</FilterContainer>
|
||||
<IconoirProvider
|
||||
|
|
@ -86,7 +38,8 @@ export function Explore({ allIcons }: ExploreProps) {
|
|||
<FilterContainer>
|
||||
<FiltersEditor filters={filters} onChange={setFilters} />
|
||||
</FilterContainer>
|
||||
<Streamline />
|
||||
<Sponsor />
|
||||
<Ad />
|
||||
<CustomizationEditor
|
||||
customizations={customizations}
|
||||
onChange={setCustomizations}
|
||||
|
|
@ -95,3 +48,45 @@ export function Explore({ allIcons }: ExploreProps) {
|
|||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
${media.md} {
|
||||
align-items: flex-start;
|
||||
flex-direction: row;
|
||||
}
|
||||
`;
|
||||
const Left = styled.div`
|
||||
flex: 1;
|
||||
min-height: calc(100vh - 100px);
|
||||
background: white;
|
||||
${media.md} {
|
||||
background: none;
|
||||
}
|
||||
`;
|
||||
const Right = styled.div`
|
||||
position: sticky;
|
||||
top: 50px;
|
||||
width: 275px;
|
||||
margin-left: 68px;
|
||||
display: block;
|
||||
z-index: -1;
|
||||
${media.md} {
|
||||
z-index: 1;
|
||||
}
|
||||
`;
|
||||
const FilterContainer = styled.div<{ isMobile?: boolean }>`
|
||||
display: ${(props) => (props.isMobile ? 'block' : 'none')};
|
||||
margin-bottom: 40px;
|
||||
position: sticky;
|
||||
top: 20px;
|
||||
z-index: 100;
|
||||
width: 100%;
|
||||
${media.md} {
|
||||
position: relative;
|
||||
top: 0;
|
||||
display: ${(props) => (props.isMobile ? 'none' : 'block')};
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import type { IconListFilters } from './IconList';
|
||||
import React from 'react';
|
||||
import { IconListFilters } from './IconList';
|
||||
import { LargeInput } from './Input';
|
||||
|
||||
export interface FiltersEditorProps {
|
||||
filters: IconListFilters;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
onChange: (filters: IconListFilters) => void;
|
||||
}
|
||||
|
||||
export function FiltersEditor({ filters, onChange }: FiltersEditorProps) {
|
||||
const [, startTransition] = (React as any).useTransition();
|
||||
const [search, setSearch] = React.useState(filters.search);
|
||||
|
|
@ -14,15 +14,12 @@ export function FiltersEditor({ filters, onChange }: FiltersEditorProps) {
|
|||
// Keep track if the user hits tab before scrolling, so we can scroll the search
|
||||
// field to the top of the page automatically.
|
||||
const didScrollRef = React.useRef(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
const scrollEvent = () => {
|
||||
didScrollRef.current = true;
|
||||
window.removeEventListener('scroll', scrollEvent);
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', scrollEvent);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('scroll', scrollEvent);
|
||||
};
|
||||
|
|
@ -43,10 +40,10 @@ export function FiltersEditor({ filters, onChange }: FiltersEditorProps) {
|
|||
|
||||
return (
|
||||
<LargeInput
|
||||
placeholder="Search..."
|
||||
placeholder={'Search...'}
|
||||
value={search}
|
||||
type="search"
|
||||
autoCapitalize="none"
|
||||
type={'search'}
|
||||
autoCapitalize={'none'}
|
||||
tabIndex={1}
|
||||
onFocus={(e) => {
|
||||
if (!didScrollRef.current) {
|
||||
|
|
|
|||
|
|
@ -1,20 +1,69 @@
|
|||
import { PeaceHand } from 'iconoir-react';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
GITHUB_LINK,
|
||||
ISSUE_LINK,
|
||||
LICENSE_LINK,
|
||||
PRIVACY_LINK,
|
||||
SUPPORT_LINK,
|
||||
} from '../lib/constants';
|
||||
import { LICENSE_LINK } from '../lib/constants';
|
||||
import { Logo, LogoContainer, LogoIcon } from './Header';
|
||||
import { NavigationItemContainer } from './NavigationItem';
|
||||
import { Text13, Text17 } from './Typography';
|
||||
|
||||
export interface FooterCategoryProps {
|
||||
category: string;
|
||||
links: { name: string, url: string }[];
|
||||
}
|
||||
function FooterCategory({ category, links }: FooterCategoryProps) {
|
||||
return (
|
||||
<FooterCategoryContainer>
|
||||
<FooterCategoryTitle>{category}</FooterCategoryTitle>
|
||||
<FooterCategoryLinks>
|
||||
{links.map((link) => (
|
||||
<FooterCategoryLink key={link.url} href={link.url}>{link.name}</FooterCategoryLink>
|
||||
))}
|
||||
</FooterCategoryLinks>
|
||||
</FooterCategoryContainer>
|
||||
);
|
||||
}
|
||||
export function Footer() {
|
||||
return (
|
||||
<Container>
|
||||
<LogoContainer>
|
||||
<LogoIcon>
|
||||
<PeaceHand />
|
||||
</LogoIcon>
|
||||
<Logo src={'/iconoir-logo.svg'} alt={'Iconoir Logo'} />
|
||||
</LogoContainer>
|
||||
<FooterCategories>
|
||||
<FooterCategory category={'Project'} links={[
|
||||
{ name: 'Our Mission', url:'/support' },
|
||||
{ name: 'Contribute', url:'/docs/contributing' },
|
||||
{ name: 'Donate', url:'https://opencollective.com/iconoir/donate' }
|
||||
]} />
|
||||
<FooterCategory category={'Support'} links={[
|
||||
{ name: 'License', url:'https://github.com/iconoir-icons/iconoir/blob/main/LICENSE' },
|
||||
{ name: 'GitHub Repository', url:'https://github.com/iconoir-icons/iconoir' },
|
||||
{ name: 'File a Request', url:'https://github.com/iconoir-icons/iconoir/issues/new/choose' }
|
||||
]} />
|
||||
<FooterCategory category={'Developers'} links={[
|
||||
{ name: 'Changelog',url:'/docs/changelog' },
|
||||
{ name: 'React and React Native', url:'/docs/libraries/iconoir-react' },
|
||||
{ name: 'Flutter', url:'/docs/libraries/flutter'} ,
|
||||
{ name: 'Framer and Figma', url:'/docs/libraries/framer' },
|
||||
{ name: 'CSS', url:'/docs/libraries/css' }
|
||||
]} />
|
||||
|
||||
|
||||
</FooterCategories>
|
||||
<FooterEnd>
|
||||
<Text13 style={{ fontWeight: 400 }}>Parts of this content are ©2020-2023 by individual Iconoir contributors. Content available under a <a href={'https://github.com/iconoir-icons/iconoir/blob/main/LICENSE'} target={'_blank'} rel={'noreferrer'}>MIT License</a>.</Text13>
|
||||
<Text13 style={{ fontWeight: 400 }}><a href={'https://www.freeprivacypolicy.com/live/ba00d743-a0cd-44f8-8cb5-6f58911db0fb'} target={'_blank'} rel={'noreferrer'}>Privacy</a></Text13>
|
||||
</FooterEnd>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: block;
|
||||
margin-top: 110px;
|
||||
padding-top: 30px;
|
||||
margin-top: 100px;
|
||||
margin: 100px -50px -50px -50px; /* not ideal. To fix removing padding from body.*/
|
||||
padding: 84px 12%;
|
||||
background-color: var(--g7);
|
||||
align-items: center;
|
||||
|
|
@ -22,7 +71,6 @@ const Container = styled.div`
|
|||
margin-right: 50px;
|
||||
}
|
||||
`;
|
||||
|
||||
const FooterEnd = styled.div`
|
||||
border-top: 1px solid var(--g5);
|
||||
padding-top: 20px;
|
||||
|
|
@ -31,29 +79,24 @@ const FooterEnd = styled.div`
|
|||
display: flex;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
const FooterCategories = styled.div`
|
||||
width: 100%;
|
||||
margin-top: 54px;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const FooterCategoryContainer = styled.div`
|
||||
width: 28%;
|
||||
margin-right: 20px;
|
||||
`;
|
||||
|
||||
const FooterCategoryTitle = styled(Text17)`
|
||||
&&& {
|
||||
margin-bottom: 24px;
|
||||
font-weight: 600;
|
||||
display: block;
|
||||
color: var(--g0);
|
||||
}
|
||||
margin-bottom: 24px;
|
||||
font-weight: 600;
|
||||
display: block;
|
||||
color: var(--g0);
|
||||
`;
|
||||
const FooterCategoryLinks = styled.div`
|
||||
|
||||
const FooterCategoryLinks = styled.div``;
|
||||
|
||||
`;
|
||||
const FooterCategoryLink = styled.a`
|
||||
display: block;
|
||||
font-size: 17px;
|
||||
|
|
@ -61,105 +104,8 @@ const FooterCategoryLink = styled.a`
|
|||
width: fit-content;
|
||||
margin-bottom: 12px;
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
&:hover{
|
||||
text-decoration: underline;
|
||||
color: var(--g0);
|
||||
}
|
||||
`;
|
||||
|
||||
interface FooterCategoryProps {
|
||||
category: string;
|
||||
links: { name: string; url: string }[];
|
||||
}
|
||||
|
||||
function FooterCategory({ category, links }: FooterCategoryProps) {
|
||||
return (
|
||||
<FooterCategoryContainer>
|
||||
<FooterCategoryTitle>{category}</FooterCategoryTitle>
|
||||
<FooterCategoryLinks>
|
||||
{links.map((link) => (
|
||||
<FooterCategoryLink key={link.url} href={link.url}>
|
||||
{link.name}
|
||||
</FooterCategoryLink>
|
||||
))}
|
||||
</FooterCategoryLinks>
|
||||
</FooterCategoryContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export function Footer() {
|
||||
const year = new Date().getFullYear();
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<LogoContainer>
|
||||
<LogoIcon>
|
||||
<PeaceHand />
|
||||
</LogoIcon>
|
||||
<Logo src="/iconoir-logo.svg" alt="Iconoir Logo" />
|
||||
</LogoContainer>
|
||||
<FooterCategories>
|
||||
<FooterCategory
|
||||
category="Project"
|
||||
links={[
|
||||
{ name: 'Our Mission', url: '/support' },
|
||||
{ name: 'Contribute', url: '/docs/contributing' },
|
||||
{
|
||||
name: 'Donate',
|
||||
url: SUPPORT_LINK,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<FooterCategory
|
||||
category="Support"
|
||||
links={[
|
||||
{
|
||||
name: 'License',
|
||||
url: LICENSE_LINK,
|
||||
},
|
||||
{
|
||||
name: 'GitHub Repository',
|
||||
url: GITHUB_LINK,
|
||||
},
|
||||
{
|
||||
name: 'File a Request',
|
||||
url: ISSUE_LINK,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<FooterCategory
|
||||
category="Developers"
|
||||
links={[
|
||||
{ name: 'Changelog', url: '/docs/changelog' },
|
||||
{
|
||||
name: 'React and React Native',
|
||||
url: '/docs/packages/iconoir-react',
|
||||
},
|
||||
{ name: 'Flutter', url: '/docs/packages/iconoir-flutter' },
|
||||
{ name: 'Framer and Figma', url: '/docs/packages/framer' },
|
||||
{ name: 'CSS', url: '/docs/packages/css' },
|
||||
]}
|
||||
/>
|
||||
</FooterCategories>
|
||||
<FooterEnd>
|
||||
<Text13 style={{ fontWeight: 400 }}>
|
||||
Parts of this content are ©2020-
|
||||
{year}
|
||||
{' '}
|
||||
by individual Iconoir
|
||||
contributors. Content available under a
|
||||
{' '}
|
||||
<a href={LICENSE_LINK} target="_blank" rel="nofollow noreferrer">
|
||||
MIT License
|
||||
</a>
|
||||
.
|
||||
</Text13>
|
||||
<Text13 style={{ fontWeight: 400 }}>
|
||||
<a href={PRIVACY_LINK} target="_blank" rel="nofollow noreferrer">
|
||||
Privacy
|
||||
</a>
|
||||
</Text13>
|
||||
</FooterEnd>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ export function GA() {
|
|||
<>
|
||||
<Script
|
||||
src="https://www.googletagmanager.com/gtag/js?id=UA-33344001-9"
|
||||
strategy="afterInteractive"
|
||||
strategy={'afterInteractive'}
|
||||
/>
|
||||
<Script id="google-analytics" strategy="afterInteractive">
|
||||
<Script id={'google-analytics'} strategy={'afterInteractive'}>
|
||||
{`
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
|
|
|
|||
|
|
@ -1,28 +1,62 @@
|
|||
import { Discord, Menu, Sparks, Xmark } from 'iconoir-react';
|
||||
import { Heart } from 'iconoir-react/solid';
|
||||
import { Cancel, Heart, Menu } from 'iconoir-react';
|
||||
import Link from 'next/link';
|
||||
import React from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
import { DISCORD_LINK, SHARE_LINK } from '../lib/constants';
|
||||
import { media } from '../lib/responsive';
|
||||
import styled from 'styled-components';
|
||||
import { AnimatedSvg } from './AnimatedSvg';
|
||||
import { ResetButton } from './Button';
|
||||
import { SHARE_LINK } from '../lib/constants';
|
||||
import { CurrentVersion } from './CurrentVersion';
|
||||
import { NavigationItem } from './NavigationItem';
|
||||
import { NavigationItem, NavigationItemContainer } from './NavigationItem';
|
||||
import { media } from '../lib/responsive';
|
||||
import { Text15 } from './Typography';
|
||||
|
||||
const StyledDiscord = styled(Discord)<{ $isMobile?: boolean }>`
|
||||
display: none;
|
||||
${media.lg} {
|
||||
display: flex;
|
||||
margin: 0 0 0 16px;
|
||||
&:hover {
|
||||
scale: 1.1;
|
||||
transition: 0.2s;
|
||||
color: #7289da;
|
||||
}
|
||||
}
|
||||
`;
|
||||
export interface HeaderProps {
|
||||
currentVersion: string;
|
||||
}
|
||||
export function Header({ currentVersion }: HeaderProps) {
|
||||
const [menuVisible, setMenuVisible] = React.useState(false);
|
||||
return (
|
||||
<Container>
|
||||
<HeaderLeft>
|
||||
<Link href={'/'}>
|
||||
<LogoContainer>
|
||||
<LogoIcon>
|
||||
<AnimatedSvg />
|
||||
</LogoIcon>
|
||||
<Logo src={'/iconoir-logo.svg'} alt={'Iconoir Logo'} />
|
||||
</LogoContainer>
|
||||
</Link>
|
||||
<CurrentVersion version={currentVersion} />
|
||||
</HeaderLeft>
|
||||
<HeaderCenter>
|
||||
<MobileMenuContainer visible={menuVisible}>
|
||||
<NavigationItem href={'/'}>Icons</NavigationItem>
|
||||
<NavigationItem href={'/docs'}>Documentation</NavigationItem>
|
||||
<NavigationItem href={'/support'} style={{ marginRight: 0 }}>
|
||||
Donate — Our Mission
|
||||
</NavigationItem>
|
||||
<BuiltWith isMobile>
|
||||
Share with <Heart width={'1em'} height={'1em'} /> on{' '}
|
||||
<a href={SHARE_LINK} target={'_blank'} rel={'noreferrer'}>
|
||||
Twitter
|
||||
</a>{' '}
|
||||
</BuiltWith>
|
||||
</MobileMenuContainer>
|
||||
</HeaderCenter>
|
||||
<HeaderRight>
|
||||
<BuiltWith>
|
||||
Share with <Heart width={'1em'} height={'1em'} /> on{' '}
|
||||
<a href={SHARE_LINK} target={'_blank'} rel={'noreferrer'}>
|
||||
Twitter
|
||||
</a>{' '}
|
||||
</BuiltWith>
|
||||
<MobileMenuButton onClick={() => setMenuVisible((v) => !v)}>
|
||||
{menuVisible ? <Cancel /> : <Menu />}
|
||||
</MobileMenuButton>
|
||||
</HeaderRight>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export const LogoContainer = styled.div`
|
||||
position: relative;
|
||||
|
|
@ -30,51 +64,27 @@ export const LogoContainer = styled.div`
|
|||
display: inline-flex;
|
||||
align-items: center;
|
||||
transition: 0.1s;
|
||||
&:hover {
|
||||
&:hover{
|
||||
scale: 1.1;
|
||||
transition: 0.2s;
|
||||
}
|
||||
`;
|
||||
|
||||
const Banner = styled(Text15)`
|
||||
display: none;
|
||||
${media.lg} {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px 0;
|
||||
background: var(--g5);
|
||||
color: var(--g0);
|
||||
font-weight: 500;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
text-decoration: underline;
|
||||
top: 0;
|
||||
}
|
||||
> * {
|
||||
margin: 0 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
const MobileMenuButton = styled(ResetButton)`
|
||||
&&& {
|
||||
z-index: 101;
|
||||
color: var(--black);
|
||||
background: transparent;
|
||||
display: inline-block;
|
||||
margin-left: auto;
|
||||
cursor: pointer;
|
||||
svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
${media.lg} {
|
||||
display: none;
|
||||
}
|
||||
z-index: 101;
|
||||
color: var(--black);
|
||||
background: transparent;
|
||||
display: inline-block;
|
||||
margin-left: auto !important;
|
||||
cursor: pointer;
|
||||
svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
${media.lg} {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const MobileMenuContainer = styled.div<{ $visible?: boolean }>`
|
||||
const MobileMenuContainer = styled.div<{ visible?: boolean }>`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
|
@ -82,9 +92,7 @@ const MobileMenuContainer = styled.div<{ $visible?: boolean }>`
|
|||
z-index: 100;
|
||||
background: white;
|
||||
padding-top: 100px;
|
||||
transition:
|
||||
transform 0.5s cubic-bezier(0.16, 1, 0.3, 1),
|
||||
opacity 0.25s linear;
|
||||
transition: transform 0.5s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.25s linear;
|
||||
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-100%);
|
||||
pointer-events: none;
|
||||
|
|
@ -92,13 +100,11 @@ const MobileMenuContainer = styled.div<{ $visible?: boolean }>`
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
${(props) =>
|
||||
props.$visible
|
||||
&& css`
|
||||
pointer-events: all;
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
`}
|
||||
${(props) => (props.visible ? '&' : '&.noop')} {
|
||||
pointer-events: all;
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
${media.lg} {
|
||||
background: none;
|
||||
padding-top: 0;
|
||||
|
|
@ -115,16 +121,11 @@ const MobileMenuContainer = styled.div<{ $visible?: boolean }>`
|
|||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
${media.lg} {
|
||||
margin-top: 40px;
|
||||
}
|
||||
`;
|
||||
|
||||
const HeaderItem = styled.div`
|
||||
flex: 1;
|
||||
width: 33%;
|
||||
|
|
@ -133,144 +134,60 @@ const HeaderItem = styled.div`
|
|||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const HeaderCenter = styled(HeaderItem)`
|
||||
padding: 0 16px;
|
||||
> :not(:last-child) {
|
||||
margin-right: 16px;
|
||||
}
|
||||
`;
|
||||
|
||||
const HeaderLeft = styled(HeaderItem)`
|
||||
&&& {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
justify-content: flex-start;
|
||||
`;
|
||||
|
||||
const HeaderRight = styled(HeaderItem)`
|
||||
&&& {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
justify-content: flex-end;
|
||||
`;
|
||||
|
||||
export const Logo = styled.img`
|
||||
height: 24px;
|
||||
margin-top: -4px;
|
||||
color: var(--black);
|
||||
margin-right: 16px;
|
||||
margin-right: 16px !important;
|
||||
z-index: 101;
|
||||
`;
|
||||
export const LogoIcon = styled.div`
|
||||
color: var(--black);
|
||||
margin-right: 4px;
|
||||
margin-right: 4px !important;
|
||||
svg {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Share = styled(Text15)<{ $isMobile?: boolean }>`
|
||||
&&& {
|
||||
display: none;
|
||||
${(props) =>
|
||||
props.$isMobile
|
||||
&& css`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 12px 0;
|
||||
`}
|
||||
${media.lg} {
|
||||
display: ${(props) => (props.$isMobile ? 'none' : 'block')};
|
||||
}
|
||||
color: var(--black-60);
|
||||
white-space: pre-wrap;
|
||||
const BuiltWith = styled(Text15)<{ isMobile?: boolean }>`
|
||||
display: ${(props) => (props.isMobile ? 'flex' : 'none')};
|
||||
${media.lg} {
|
||||
display: ${(props) => (props.isMobile ? 'none' : 'flex')};
|
||||
}
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--black-60);
|
||||
border-bottom: none !important;
|
||||
svg {
|
||||
fill: var(--black);
|
||||
margin: 0 0.22em;
|
||||
}
|
||||
> * {
|
||||
margin: 0 0.22em;
|
||||
}
|
||||
a {
|
||||
color: var(--black);
|
||||
font-weight: 700;
|
||||
}
|
||||
> :last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
${media.lg} {
|
||||
justify-content: flex-start;
|
||||
a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: unset;
|
||||
text-decoration: unset;
|
||||
}
|
||||
svg,
|
||||
span {
|
||||
color: var(--black);
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export interface HeaderProps {
|
||||
currentVersion: string;
|
||||
}
|
||||
|
||||
export function Header({ currentVersion }: HeaderProps) {
|
||||
const [menuVisible, setMenuVisible] = React.useState(false);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Banner>
|
||||
<Sparks></Sparks>
|
||||
<a
|
||||
href="https://opencollective.com/iconoir/donate?interval=month&amount=10"
|
||||
target="_blank"
|
||||
>
|
||||
Your one-time or recurring contribution does a lot to keep Iconoir
|
||||
going.
|
||||
</a>
|
||||
</Banner>
|
||||
<HeaderLeft>
|
||||
<Link href="/">
|
||||
<LogoContainer>
|
||||
<LogoIcon>
|
||||
<AnimatedSvg />
|
||||
</LogoIcon>
|
||||
<Logo src="/iconoir-logo.svg" alt="Iconoir Logo" />
|
||||
</LogoContainer>
|
||||
</Link>
|
||||
<CurrentVersion version={currentVersion} />
|
||||
</HeaderLeft>
|
||||
<HeaderCenter>
|
||||
<MobileMenuContainer $visible={menuVisible}>
|
||||
<NavigationItem href="/">Icons</NavigationItem>
|
||||
<NavigationItem href="/docs/introduction" activeMatch="/docs">
|
||||
Documentation
|
||||
</NavigationItem>
|
||||
<NavigationItem href="/support" style={{ marginRight: 0 }}>
|
||||
Donate — 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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,53 @@
|
|||
import React, { useEffect, useRef } from 'react';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { media } from '../lib/responsive';
|
||||
import {useRef, useEffect} from 'react';
|
||||
|
||||
export interface HeaderBackgroundProps {
|
||||
children: React.ReactElement;
|
||||
}
|
||||
export function HeaderBackground({ children }: HeaderBackgroundProps) {
|
||||
const parallaxRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!parallaxRef.current) return;
|
||||
|
||||
const parallaxElements = parallaxRef.current.querySelectorAll('[data-parallax-factor]');
|
||||
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
const x = event.clientX / window.innerWidth;
|
||||
const y = event.clientY / window.innerHeight;
|
||||
parallaxElements.forEach((el) => {
|
||||
const factor = parseFloat(el.getAttribute('data-parallax-factor') || '1');
|
||||
(el as HTMLElement).style.transform = `translate3d(${x * factor * 40}px, ${y * factor * 80}px, 0)`;
|
||||
});
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
return (
|
||||
<HeaderContainer ref={parallaxRef}>
|
||||
<FloatingIconCellar data-parallax-factor="0.75"/>
|
||||
<FloatingIconPay data-parallax-factor="1.5"/>
|
||||
<FloatingFaceID data-parallax-factor="0.5"/>
|
||||
<FloatingCommand data-parallax-factor="1.25"/>
|
||||
<FloatingFill data-parallax-factor="2"/>
|
||||
{children}
|
||||
</HeaderContainer>
|
||||
);
|
||||
}
|
||||
|
||||
const HeaderContainer = styled.div`
|
||||
position: relative;
|
||||
width: fit-content;
|
||||
margin: auto;
|
||||
`;
|
||||
|
||||
const FloatingIcon = styled.div`
|
||||
position: absolute;
|
||||
display: none;
|
||||
|
|
@ -20,117 +60,63 @@ const FloatingIcon = styled.div`
|
|||
display: flex;
|
||||
}
|
||||
`;
|
||||
|
||||
const FloatingIconCellar = styled(FloatingIcon)`
|
||||
-webkit-transform: rotate(6deg);
|
||||
-moz-transform: rotate(6deg);
|
||||
-moz-transform: rotate(6deg);
|
||||
top: -120px;
|
||||
right: 0px;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
width:200px;
|
||||
height:200px;
|
||||
background-image: url(/cellar.gif);
|
||||
background-size: 70%;
|
||||
background-size:70%;
|
||||
${media.lg} {
|
||||
}
|
||||
`;
|
||||
|
||||
const FloatingIconPay = styled(FloatingIcon)`
|
||||
-webkit-transform: rotate(18deg);
|
||||
-moz-transform: rotate(18deg);
|
||||
-moz-transform: rotate(18deg);
|
||||
top: -50px;
|
||||
right: -100px;
|
||||
width: 130px;
|
||||
height: 130px;
|
||||
width:130px;
|
||||
height:130px;
|
||||
background-image: url(/pay-bitcoin.gif);
|
||||
background-size: 70%;
|
||||
background-size:70%;
|
||||
${media.lg} {
|
||||
}
|
||||
`;
|
||||
|
||||
const FloatingFaceID = styled(FloatingIcon)`
|
||||
-webkit-transform: rotate(6deg);
|
||||
-moz-transform: rotate(6deg);
|
||||
-moz-transform: rotate(6deg);
|
||||
top: -130px;
|
||||
right: 380px;
|
||||
width: 110px;
|
||||
height: 110px;
|
||||
width:110px;
|
||||
height:110px;
|
||||
background-image: url(/face-id.gif);
|
||||
background-size: 70%;
|
||||
background-size:70%;
|
||||
${media.lg} {
|
||||
}
|
||||
`;
|
||||
|
||||
const FloatingCommand = styled(FloatingIcon)`
|
||||
-webkit-transform: rotate(-7deg);
|
||||
-moz-transform: rotate(-7deg);
|
||||
-moz-transform: rotate(-7deg);
|
||||
top: -94px;
|
||||
left: 150px;
|
||||
width: 110px;
|
||||
height: 110px;
|
||||
width:110px;
|
||||
height:110px;
|
||||
background-image: url(/command.gif);
|
||||
background-size: 70%;
|
||||
background-size:70%;
|
||||
${media.lg} {
|
||||
}
|
||||
`;
|
||||
|
||||
const FloatingFill = styled(FloatingIcon)`
|
||||
-webkit-transform: rotate(-14deg);
|
||||
-moz-transform: rotate(-14deg);
|
||||
-moz-transform: rotate(-14deg);
|
||||
top: -64px;
|
||||
left: -75px;
|
||||
width: 110px;
|
||||
height: 110px;
|
||||
width:110px;
|
||||
height:110px;
|
||||
background-image: url(/fill.gif);
|
||||
background-size: 70%;
|
||||
background-size:70%;
|
||||
${media.lg} {
|
||||
}
|
||||
`;
|
||||
|
||||
export interface HeaderBackgroundProps {
|
||||
children: React.ReactElement;
|
||||
}
|
||||
|
||||
export function HeaderBackground({ children }: HeaderBackgroundProps) {
|
||||
const parallaxRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!parallaxRef.current)
|
||||
return;
|
||||
|
||||
const parallaxElements = parallaxRef.current.querySelectorAll(
|
||||
'[data-parallax-factor]',
|
||||
);
|
||||
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
const x = event.clientX / window.innerWidth;
|
||||
const y = event.clientY / window.innerHeight;
|
||||
|
||||
parallaxElements.forEach((el) => {
|
||||
const factor = Number.parseFloat(
|
||||
el.getAttribute('data-parallax-factor') || '1',
|
||||
);
|
||||
|
||||
(el as HTMLElement).style.transform = `translate3d(${
|
||||
x * factor * 40
|
||||
}px, ${y * factor * 80}px, 0)`;
|
||||
});
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<HeaderContainer ref={parallaxRef}>
|
||||
<FloatingIconCellar data-parallax-factor="0.75" />
|
||||
<FloatingIconPay data-parallax-factor="1.5" />
|
||||
<FloatingFaceID data-parallax-factor="0.5" />
|
||||
<FloatingCommand data-parallax-factor="1.25" />
|
||||
<FloatingFill data-parallax-factor="2" />
|
||||
{children}
|
||||
</HeaderContainer>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,22 @@
|
|||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { media } from '../lib/responsive';
|
||||
import {useRef, useEffect} from 'react';
|
||||
|
||||
export interface HeaderSecondaryProps {
|
||||
children: React.ReactElement;
|
||||
}
|
||||
export function HeaderSecondary({ children }: HeaderSecondaryProps) {
|
||||
|
||||
return (
|
||||
<HeaderContainer>
|
||||
{children}
|
||||
</HeaderContainer>
|
||||
);
|
||||
}
|
||||
|
||||
const HeaderContainer = styled.div`
|
||||
position: relative;
|
||||
width: fit-content;
|
||||
margin: auto;
|
||||
`;
|
||||
|
||||
export interface HeaderSecondaryProps {
|
||||
children: React.ReactElement;
|
||||
}
|
||||
|
||||
export function HeaderSecondary({ children }: HeaderSecondaryProps) {
|
||||
return <HeaderContainer>{children}</HeaderContainer>;
|
||||
}
|
||||
`;
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,29 +1,120 @@
|
|||
import type { Icon as IconType } from './IconList';
|
||||
import * as AllIcons from 'iconoir-react';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { showNotification } from '../lib/showNotification';
|
||||
import { ResetButton } from './Button';
|
||||
import { DEFAULT_CUSTOMIZATIONS } from './IconList';
|
||||
import { DEFAULT_CUSTOMIZATIONS, Icon as IconType } from './IconList';
|
||||
|
||||
const HEADER = '<?xml version="1.0" encoding="UTF-8"?>';
|
||||
|
||||
function bakeSvg(
|
||||
svgString: string,
|
||||
color: string,
|
||||
strokeWidth: string | number,
|
||||
strokeWidth: string | number
|
||||
) {
|
||||
return (
|
||||
HEADER
|
||||
+ svgString
|
||||
HEADER +
|
||||
svgString
|
||||
.replace(
|
||||
/stroke="currentColor"/g,
|
||||
`stroke="currentColor" stroke-width="${strokeWidth}"`,
|
||||
`stroke="currentColor" stroke-width="${strokeWidth}"`
|
||||
)
|
||||
.replace(/currentColor/g, color)
|
||||
);
|
||||
}
|
||||
|
||||
export interface IconProps {
|
||||
iconWidth: number;
|
||||
icon: IconType;
|
||||
}
|
||||
export function Icon({ iconWidth, icon }: IconProps) {
|
||||
const IconComponent = (AllIcons as any)[icon.iconComponentName];
|
||||
const iconContainerRef = React.useRef<HTMLDivElement>(null);
|
||||
const downloadRef = React.useRef<HTMLAnchorElement>(null);
|
||||
const htmlContentsRef = React.useRef<string>('');
|
||||
const iconContext = React.useContext(AllIcons.IconoirContext);
|
||||
const [supportsClipboard, setSupportsClipboard] = React.useState(false);
|
||||
React.useEffect(() => {
|
||||
setSupportsClipboard(
|
||||
typeof window !== 'undefined' &&
|
||||
typeof window?.navigator?.clipboard?.writeText !== 'undefined'
|
||||
);
|
||||
}, []);
|
||||
React.useEffect(() => {
|
||||
if (iconContainerRef.current) {
|
||||
htmlContentsRef.current = bakeSvg(
|
||||
iconContainerRef.current.innerHTML,
|
||||
iconContext.color || DEFAULT_CUSTOMIZATIONS.hexColor,
|
||||
iconContext.strokeWidth || DEFAULT_CUSTOMIZATIONS.strokeWidth
|
||||
);
|
||||
}
|
||||
}, [iconContext, supportsClipboard]);
|
||||
React.useEffect(() => {
|
||||
const element =
|
||||
downloadRef.current ||
|
||||
(iconContainerRef.current as unknown as HTMLAnchorElement);
|
||||
if (element) {
|
||||
element.href = `data:image/svg+xml;base64,${btoa(
|
||||
htmlContentsRef.current
|
||||
)}`;
|
||||
}
|
||||
}, [iconContext, supportsClipboard]);
|
||||
return (
|
||||
<div className={'icon-container'}>
|
||||
<BorderContainer iconWidth={iconWidth}>
|
||||
<IconContainer
|
||||
ref={iconContainerRef}
|
||||
{...((supportsClipboard
|
||||
? {}
|
||||
: {
|
||||
as: 'a',
|
||||
href: '#',
|
||||
rel: 'noreferrer',
|
||||
download: `${icon.filename}.svg`,
|
||||
}) as any)}
|
||||
>
|
||||
<IconComponent />
|
||||
</IconContainer>
|
||||
{supportsClipboard ? (
|
||||
<HoverContainer>
|
||||
<CornerBR/>
|
||||
<CornerTR/>
|
||||
<CornerBL/>
|
||||
<CornerTL/>
|
||||
<HoverButton
|
||||
onClick={() => {
|
||||
if (htmlContentsRef.current) {
|
||||
navigator.clipboard
|
||||
.writeText(htmlContentsRef.current)
|
||||
.then(() => {
|
||||
showNotification('SVG code copied!');
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Copy SVG
|
||||
</HoverButton>
|
||||
<HoverButton
|
||||
as={'a'}
|
||||
ref={downloadRef}
|
||||
href={'#'}
|
||||
rel={'noreferrer'}
|
||||
download={`${icon.filename}.svg`}
|
||||
>
|
||||
Download
|
||||
</HoverButton>
|
||||
</HoverContainer>
|
||||
) : null}
|
||||
</BorderContainer>
|
||||
<Subtitle iconWidth={iconWidth} title={icon.filename}>
|
||||
{icon.filename}
|
||||
</Subtitle>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Overlay = styled.div`
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
|
|
@ -32,68 +123,60 @@ const Overlay = styled.div`
|
|||
width: 8px;
|
||||
height: 8px;
|
||||
`;
|
||||
|
||||
const CornerBR = styled(Overlay)`
|
||||
bottom: -6px;
|
||||
right: -6px;
|
||||
z-index: 999;
|
||||
bottom: -6px;
|
||||
right: -6px;
|
||||
z-index: 999;
|
||||
`;
|
||||
|
||||
const CornerTR = styled(Overlay)`
|
||||
top: -6px;
|
||||
right: -6px;
|
||||
top: -6px;
|
||||
right: -6px;
|
||||
`;
|
||||
|
||||
const CornerBL = styled(Overlay)`
|
||||
bottom: -6px;
|
||||
left: -6px;
|
||||
bottom: -6px;
|
||||
left: -6px;
|
||||
`;
|
||||
|
||||
const CornerTL = styled(Overlay)`
|
||||
top: -6px;
|
||||
left: -6px;
|
||||
top: -6px;
|
||||
left: -6px;
|
||||
`;
|
||||
|
||||
const HoverContainer = styled.div`
|
||||
const HoverContainer = styled.div<{ supportsCopy?: boolean }>`
|
||||
position: absolute;
|
||||
display: ${(props) => (props.supportsCopy ? 'block' : 'none')};
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: stretch;
|
||||
flex-direction: column;
|
||||
|
||||
|
||||
transform: translateZ(0px); // Safari Fix
|
||||
transition: opacity 0.1s linear;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
`;
|
||||
|
||||
const HoverButton = styled(ResetButton)`
|
||||
&&& {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--white);
|
||||
border-radius: 0px;
|
||||
transition: background 0.1s linear;
|
||||
color: var(--g0);
|
||||
font-size: 14px;
|
||||
line-height: 23px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
&:hover,
|
||||
&:active {
|
||||
background: var(--g0);
|
||||
color: var(--white);
|
||||
}
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--white);
|
||||
border-radius: 0px !important;
|
||||
transition: background 0.1s linear;
|
||||
color: var(--g0);
|
||||
font-size: 14px;
|
||||
line-height: 23px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
&:hover,
|
||||
&:active {
|
||||
background: var(--g0);
|
||||
color: var(--white);
|
||||
}
|
||||
`;
|
||||
|
||||
const BorderContainer = styled.div<{ $iconWidth: number }>`
|
||||
width: ${(props) => props.$iconWidth}px;
|
||||
const BorderContainer = styled.div<{ iconWidth: number }>`
|
||||
width: ${(props) => props.iconWidth}px;
|
||||
box-sizing: border-box;
|
||||
padding-bottom: 100%;
|
||||
position: relative;
|
||||
|
|
@ -108,7 +191,6 @@ const BorderContainer = styled.div<{ $iconWidth: number }>`
|
|||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const IconContainer = styled.div`
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
|
|
@ -116,129 +198,14 @@ const IconContainer = styled.div`
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const IconTag = styled.div`
|
||||
background-color: var(--g6);
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
border-radius: 5px 10px;
|
||||
padding: 3px 6px;
|
||||
letter-spacing: 0.3px;
|
||||
font-weight: 600;
|
||||
font-size: 11px;
|
||||
color: var(--g0);
|
||||
`;
|
||||
|
||||
const Subtitle = styled.div<{ $iconWidth: number }>`
|
||||
const Subtitle = styled.div<{ iconWidth: number }>`
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 14.74px;
|
||||
color: var(--black-60);
|
||||
color: var(--black-40);
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: ${(props) => props.$iconWidth}px;
|
||||
width: ${(props) => props.iconWidth}px;
|
||||
`;
|
||||
|
||||
export interface IconProps {
|
||||
iconWidth: number;
|
||||
icon: IconType;
|
||||
}
|
||||
|
||||
export function Icon({ iconWidth, icon }: IconProps) {
|
||||
const IconComponent = (AllIcons as any)[icon.iconComponentName];
|
||||
const iconContainerRef = React.useRef<HTMLDivElement>(null);
|
||||
const downloadRef = React.useRef<HTMLAnchorElement>(null);
|
||||
const htmlContentsRef = React.useRef<string>('');
|
||||
const iconContext = React.useContext(AllIcons.IconoirContext);
|
||||
const [supportsClipboard, setSupportsClipboard] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
setSupportsClipboard(
|
||||
typeof window !== 'undefined'
|
||||
&& typeof window?.navigator?.clipboard?.writeText !== 'undefined',
|
||||
);
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (iconContainerRef.current) {
|
||||
htmlContentsRef.current = bakeSvg(
|
||||
(iconContainerRef.current.firstChild as SVGElement).outerHTML,
|
||||
iconContext.color || DEFAULT_CUSTOMIZATIONS.hexColor,
|
||||
iconContext.strokeWidth || DEFAULT_CUSTOMIZATIONS.strokeWidth,
|
||||
);
|
||||
}
|
||||
}, [iconContext, supportsClipboard]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const element = downloadRef.current || (iconContainerRef.current as unknown as HTMLAnchorElement);
|
||||
|
||||
if (element) {
|
||||
element.href = `data:image/svg+xml;base64,${btoa(
|
||||
htmlContentsRef.current,
|
||||
)}`;
|
||||
}
|
||||
}, [iconContext, supportsClipboard]);
|
||||
|
||||
return (
|
||||
<div className="icon-container">
|
||||
<BorderContainer $iconWidth={iconWidth}>
|
||||
<IconContainer
|
||||
ref={iconContainerRef}
|
||||
{...((supportsClipboard
|
||||
? {}
|
||||
: {
|
||||
as: 'a',
|
||||
href: '#',
|
||||
rel: 'noreferrer',
|
||||
download: `${icon.filename}.svg`,
|
||||
}) as any)}
|
||||
>
|
||||
<IconComponent />
|
||||
|
||||
{icon.filename.includes('-solid') ? <IconTag>SOLID</IconTag> : ''}
|
||||
</IconContainer>
|
||||
{supportsClipboard
|
||||
? (
|
||||
<HoverContainer>
|
||||
<CornerBR />
|
||||
<CornerTR />
|
||||
<CornerBL />
|
||||
<CornerTL />
|
||||
<HoverButton
|
||||
onClick={() => {
|
||||
if (htmlContentsRef.current) {
|
||||
navigator.clipboard
|
||||
.writeText(htmlContentsRef.current)
|
||||
.then(() => {
|
||||
showNotification('SVG code copied!');
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Copy SVG
|
||||
</HoverButton>
|
||||
<HoverButton
|
||||
as="a"
|
||||
ref={downloadRef}
|
||||
href="#"
|
||||
rel="noreferrer"
|
||||
download={`${icon.filename}.svg`}
|
||||
>
|
||||
Download
|
||||
</HoverButton>
|
||||
</HoverContainer>
|
||||
)
|
||||
: null}
|
||||
</BorderContainer>
|
||||
<Subtitle $iconWidth={iconWidth} title={icon.filename}>
|
||||
{icon.filename}
|
||||
</Subtitle>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,14 @@
|
|||
import type {
|
||||
ListChildComponentProps,
|
||||
} from 'react-window';
|
||||
import { chunk } from 'lodash';
|
||||
import React from 'react';
|
||||
import {
|
||||
areEqual,
|
||||
ListChildComponentProps,
|
||||
VariableSizeList as List,
|
||||
} from 'react-window';
|
||||
import styled from 'styled-components';
|
||||
import useResizeObserver from 'use-resize-observer';
|
||||
import { ICON_SPACE, ICON_WIDTH } from '../lib/constants';
|
||||
import { CategoryRow } from './CategoryRow';
|
||||
import { ICON_SPACE, ICON_WIDTH } from '../lib/constants';
|
||||
import { IconListEmpty } from './IconListEmpty';
|
||||
import { IconsRow } from './IconsRow';
|
||||
import { ReactWindowScroller } from './ReactWindowScroller';
|
||||
|
|
@ -43,22 +41,18 @@ function normalizeString(s: string) {
|
|||
function filterIcons(allIcons: Icon[], filters: IconListFilters): Icon[] {
|
||||
if (filters.search) {
|
||||
const normalSearch = normalizeString(filters.search!);
|
||||
let result = allIcons;
|
||||
|
||||
let result = allIcons
|
||||
for (const term of normalSearch.split(' ')) {
|
||||
result = result.filter((icon) => {
|
||||
return (
|
||||
normalizeString(icon.filename).includes(term)
|
||||
|| normalizeString(icon.category).includes(term)
|
||||
|| icon.tags.some((tag) => normalizeString(tag).includes(term))
|
||||
normalizeString(icon.filename).includes(term) ||
|
||||
normalizeString(icon.category).includes(term) ||
|
||||
icon.tags.some((tag) => normalizeString(tag).includes(term))
|
||||
);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
return result;
|
||||
} else {
|
||||
return allIcons;
|
||||
}
|
||||
} else return allIcons;
|
||||
}
|
||||
|
||||
interface IconCategoryRow {
|
||||
|
|
@ -69,34 +63,28 @@ interface IconIconsRow {
|
|||
icons: Icon[];
|
||||
}
|
||||
type IconRow = IconCategoryRow | IconIconsRow;
|
||||
|
||||
function isCategoryRow(iconRow: IconRow): iconRow is IconCategoryRow {
|
||||
return !!(iconRow as IconCategoryRow).category;
|
||||
}
|
||||
|
||||
function getRowsFromIcons(
|
||||
filteredIcons: Icon[],
|
||||
iconsPerRow: number,
|
||||
iconsPerRow: number
|
||||
): IconRow[] {
|
||||
const categoryGroups: Record<string, Icon[]> = {};
|
||||
|
||||
for (const icon of filteredIcons) {
|
||||
if (!categoryGroups[icon.category])
|
||||
categoryGroups[icon.category] = [];
|
||||
if (!categoryGroups[icon.category]) categoryGroups[icon.category] = [];
|
||||
categoryGroups[icon.category].push(icon);
|
||||
}
|
||||
|
||||
const result: IconRow[] = [];
|
||||
const sortedCategories = Object.keys(categoryGroups).sort();
|
||||
|
||||
for (const sortedCategory of sortedCategories) {
|
||||
result.push({
|
||||
category: sortedCategory,
|
||||
numIcons: categoryGroups[sortedCategory].length,
|
||||
});
|
||||
|
||||
const iconRows = chunk(categoryGroups[sortedCategory], iconsPerRow);
|
||||
|
||||
for (const iconRow of iconRows) {
|
||||
result.push({ icons: iconRow });
|
||||
}
|
||||
|
|
@ -109,7 +97,6 @@ const ICON_BOTTOM_PADDING = 65;
|
|||
const HEADER_HEIGHT = 150;
|
||||
const HEADER_INNER_HEIGHT = 15 + 40;
|
||||
const HEADER_TOP_PADDING = HEADER_HEIGHT - HEADER_INNER_HEIGHT;
|
||||
|
||||
function getItemSize(row: IconRow, iconWidth: number): number {
|
||||
if (isCategoryRow(row)) {
|
||||
return HEADER_HEIGHT;
|
||||
|
|
@ -122,67 +109,29 @@ interface IconListContextValue {
|
|||
iconWidth: number;
|
||||
iconsPerRow: number;
|
||||
}
|
||||
|
||||
const IconListContext = React.createContext<IconListContextValue | undefined>(undefined);
|
||||
|
||||
const Container = styled.div`
|
||||
width: 100%;
|
||||
margin-top: -${HEADER_TOP_PADDING}px;
|
||||
> :first-child {
|
||||
overflow: visible !important;
|
||||
> :first-child {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const Row = React.memo(
|
||||
({ data, index, style }: ListChildComponentProps<IconRow[]>) => {
|
||||
const { iconWidth } = React.useContext(IconListContext)!;
|
||||
const row = data[index];
|
||||
|
||||
if (isCategoryRow(row)) {
|
||||
return (
|
||||
<CategoryRow
|
||||
category={row.category}
|
||||
numIcons={row.numIcons}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return <IconsRow icons={row.icons} style={style} iconWidth={iconWidth} />;
|
||||
}
|
||||
},
|
||||
areEqual,
|
||||
);
|
||||
|
||||
Row.displayName = 'Row';
|
||||
export const IconListContext = React.createContext<
|
||||
IconListContextValue | undefined
|
||||
>(undefined);
|
||||
|
||||
export interface IconListProps {
|
||||
filters: IconListFilters;
|
||||
allIcons: Icon[];
|
||||
}
|
||||
|
||||
export function IconList({ filters, allIcons }: IconListProps) {
|
||||
const filteredIcons = filterIcons(allIcons, filters);
|
||||
const { ref, width = 400 } = useResizeObserver();
|
||||
|
||||
const iconsPerRow = width
|
||||
? Math.floor((width + ICON_SPACE) / (ICON_WIDTH + ICON_SPACE))
|
||||
: null;
|
||||
|
||||
let children = null;
|
||||
const listRef = React.useRef<List<IconRow[]> | null>(null);
|
||||
const listRef = React.useRef<List<IconRow[]> | null>();
|
||||
const [height, setHeight] = React.useState(400);
|
||||
|
||||
const iconWidth = iconsPerRow
|
||||
? Math.floor((width + ICON_SPACE) / iconsPerRow) - ICON_SPACE
|
||||
: null;
|
||||
|
||||
React.useEffect(() => {
|
||||
setHeight(window.innerHeight);
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (listRef.current) {
|
||||
listRef.current.resetAfterIndex(0, true);
|
||||
|
|
@ -191,17 +140,14 @@ export function IconList({ filters, allIcons }: IconListProps) {
|
|||
|
||||
if (filteredIcons.length && iconsPerRow && width && iconWidth) {
|
||||
const iconRows = getRowsFromIcons(filteredIcons, iconsPerRow);
|
||||
|
||||
children = (
|
||||
<IconListContext.Provider value={{ iconsPerRow, iconWidth }}>
|
||||
<ReactWindowScroller>
|
||||
{({ ref, outerRef, style, onScroll }: any) => (
|
||||
<List<IconRow[]>
|
||||
ref={(c) => {
|
||||
if (typeof ref === 'function')
|
||||
ref(c);
|
||||
else
|
||||
ref.current = c;
|
||||
if (typeof ref === 'function') ref(c);
|
||||
else ref.current = c;
|
||||
listRef.current = c;
|
||||
}}
|
||||
itemData={iconRows}
|
||||
|
|
@ -225,3 +171,34 @@ export function IconList({ filters, allIcons }: IconListProps) {
|
|||
|
||||
return <Container ref={ref}>{children}</Container>;
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
width: 100%;
|
||||
margin-top: -${HEADER_TOP_PADDING}px;
|
||||
> :first-child {
|
||||
overflow: visible !important;
|
||||
> :first-child {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const Row = React.memo(
|
||||
({ data, index, style }: ListChildComponentProps<IconRow[]>) => {
|
||||
const { iconWidth } = React.useContext(IconListContext)!;
|
||||
const row = data[index];
|
||||
if (isCategoryRow(row)) {
|
||||
return (
|
||||
<CategoryRow
|
||||
category={row.category}
|
||||
numIcons={row.numIcons}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return <IconsRow icons={row.icons} style={style} iconWidth={iconWidth} />;
|
||||
}
|
||||
},
|
||||
areEqual
|
||||
);
|
||||
Row.displayName = 'Row';
|
||||
|
|
|
|||
|
|
@ -3,6 +3,29 @@ import styled from 'styled-components';
|
|||
import { SUGGEST_ICON_LINK } from '../lib/constants';
|
||||
import { Text18 } from './Typography';
|
||||
|
||||
export interface IconListEmptyProps {
|
||||
searchTerm: string;
|
||||
}
|
||||
export function IconListEmpty({ searchTerm }: IconListEmptyProps) {
|
||||
return (
|
||||
<Container>
|
||||
<IconContainer>
|
||||
<SpockHandGesture />
|
||||
</IconContainer>
|
||||
<Title>
|
||||
Unfortunately there are no icons for '{searchTerm}'
|
||||
</Title>
|
||||
<Text18 style={{ color: 'var(--black-60)' }}>
|
||||
{"If you can't find the icon, you can make a"}
|
||||
<br />
|
||||
<a href={SUGGEST_ICON_LINK} target={'_blank'} rel={'noreferrer'}>
|
||||
suggestion on GitHub.
|
||||
</a>
|
||||
</Text18>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
margin-top: 90px;
|
||||
display: flex;
|
||||
|
|
@ -10,7 +33,6 @@ const Container = styled.div`
|
|||
flex-direction: column;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const IconContainer = styled.div`
|
||||
svg {
|
||||
width: 60px;
|
||||
|
|
@ -19,37 +41,8 @@ const IconContainer = styled.div`
|
|||
margin-bottom: 65px;
|
||||
color: var(--black);
|
||||
`;
|
||||
|
||||
const Title = styled(Text18)`
|
||||
&&& {
|
||||
font-weight: 700;
|
||||
margin-bottom: 30px;
|
||||
color: var(--black);
|
||||
}
|
||||
font-weight: 700;
|
||||
margin-bottom: 30px;
|
||||
color: var(--black);
|
||||
`;
|
||||
|
||||
export interface IconListEmptyProps {
|
||||
searchTerm: string;
|
||||
}
|
||||
|
||||
export function IconListEmpty({ searchTerm }: IconListEmptyProps) {
|
||||
return (
|
||||
<Container>
|
||||
<IconContainer>
|
||||
<SpockHandGesture />
|
||||
</IconContainer>
|
||||
<Title>
|
||||
Unfortunately there are no icons for '
|
||||
{searchTerm}
|
||||
'
|
||||
</Title>
|
||||
<Text18 style={{ color: 'var(--black-60)' }}>
|
||||
If you can't find the icon, you can make a
|
||||
<br />
|
||||
<a href={SUGGEST_ICON_LINK} target="_blank" rel="noreferrer">
|
||||
suggestion on GitHub.
|
||||
</a>
|
||||
</Text18>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,13 @@
|
|||
import type { Icon } from './IconList';
|
||||
import styled from 'styled-components';
|
||||
import { ICON_SPACE } from '../lib/constants';
|
||||
import { Icon as IconC } from './Icon';
|
||||
|
||||
const RowContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
> :not(:last-child) {
|
||||
margin-right: ${ICON_SPACE}px;
|
||||
}
|
||||
`;
|
||||
import { Icon } from './IconList';
|
||||
|
||||
export interface IconsRowProps {
|
||||
icons: Icon[];
|
||||
style?: any;
|
||||
iconWidth: number;
|
||||
}
|
||||
|
||||
export function IconsRow({ icons, style, iconWidth }: IconsRowProps) {
|
||||
return (
|
||||
<RowContainer style={style}>
|
||||
|
|
@ -26,3 +17,11 @@ export function IconsRow({ icons, style, iconWidth }: IconsRowProps) {
|
|||
</RowContainer>
|
||||
);
|
||||
}
|
||||
|
||||
const RowContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
> :not(:last-child) {
|
||||
margin-right: ${ICON_SPACE}px;
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -19,44 +19,40 @@ const ResetInput = styled.input`
|
|||
}
|
||||
`;
|
||||
|
||||
const Input = styled(ResetInput)`
|
||||
&&& {
|
||||
min-height: 35px;
|
||||
background: var(--white);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
padding: 6px;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
line-height: 21px;
|
||||
font-weight: 500;
|
||||
color: var(--black);
|
||||
border: solid 1px var(--g6);
|
||||
|
||||
&:hover {
|
||||
border: solid 2px var(--g0);
|
||||
}
|
||||
&:focus {
|
||||
border: solid 2px var(--g0);
|
||||
}
|
||||
export const Input = styled(ResetInput)`
|
||||
min-height: 35px;
|
||||
background: var(--white);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
padding: 6px;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
line-height: 21px;
|
||||
font-weight: 500;
|
||||
color: var(--black);
|
||||
border: solid 1px var(--g6);
|
||||
|
||||
&:hover{
|
||||
border: solid 2px var(--g0);
|
||||
}
|
||||
&:focus{
|
||||
border: solid 2px var(--g0);
|
||||
}
|
||||
`;
|
||||
|
||||
export const LargeInput = styled(Input)`
|
||||
&&& {
|
||||
height: 60px;
|
||||
font-size: 16px;
|
||||
line-height: 26px;
|
||||
border-radius: 12px;
|
||||
padding: 0 23px;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
transition: 0.2s;
|
||||
&:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
height: 60px;
|
||||
font-size: 16px;
|
||||
line-height: 26px;
|
||||
border-radius: 12px;
|
||||
padding: 0 23px;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
transition: 0.2s;
|
||||
&:hover{
|
||||
transform: scale(1.02);
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -69,7 +65,7 @@ export const ColorButton = styled.div`
|
|||
right: 20px;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
&:hover {
|
||||
&:hover{
|
||||
transition: 0.2s;
|
||||
scale: 1.2;
|
||||
}
|
||||
|
|
@ -86,10 +82,10 @@ export const ColorInput = styled.input`
|
|||
border: none;
|
||||
}
|
||||
background-color: var(--gray-200);
|
||||
opacity: 0;
|
||||
opacity:0;
|
||||
transition: 0.2s;
|
||||
&:hover + ${ColorButton} {
|
||||
transition: 0.2s;
|
||||
scale: 1.3;
|
||||
}
|
||||
`;
|
||||
`;
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { MDXRemoteProps } from 'next-mdx-remote';
|
||||
import { MDXRemote as CoreMDXRemote } from 'next-mdx-remote';
|
||||
import { MDXRemote as CoreMDXRemote, MDXRemoteProps } from 'next-mdx-remote';
|
||||
import { SuggestLibrary } from './SuggestLibrary';
|
||||
import { Table } from './Table';
|
||||
import { Body, Code, H1, H2, H3, InlineCode, Li, Pre } from './Typography';
|
||||
import { Body, Code, H1, H2, H3, Pre } from './Typography';
|
||||
|
||||
export function MDXRemote(props: MDXRemoteProps) {
|
||||
return (
|
||||
|
|
@ -16,8 +16,7 @@ export function MDXRemote(props: MDXRemoteProps) {
|
|||
h2: H2,
|
||||
h3: H3,
|
||||
table: Table,
|
||||
code: InlineCode,
|
||||
li: Li,
|
||||
SuggestLibrary,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,108 +5,77 @@ import styled from 'styled-components';
|
|||
import { media } from '../lib/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>
|
||||
<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;
|
||||
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: 500;
|
||||
padding: 0;
|
||||
color: var(--g0);
|
||||
width: auto;
|
||||
border-bottom: none !important;
|
||||
|
||||
transition: 0.2s;
|
||||
&::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;
|
||||
}
|
||||
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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 '../lib/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'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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,40 +3,6 @@ import styled from 'styled-components';
|
|||
import { media } from '../lib/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
Loading…
Add table
Add a link
Reference in a new issue