mirror of
https://github.com/iconoir-icons/iconoir
synced 2026-03-14 22:15:43 +01:00
Compare commits
331 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2911b580e9 |
||
|
|
6e6d2e27ea |
||
|
|
ec9e470fda |
||
|
|
320c603674 |
||
|
|
91f5ff77b3 |
||
|
|
3a0e86816e |
||
|
|
ea8474ed13 | ||
|
|
b12a69bf9e | ||
|
|
569fa8246e | ||
|
|
4d9e934c61 |
||
|
|
76b89cca9d |
||
|
|
fc38183e2b | ||
|
|
e048feaae0 |
||
|
|
48c6486a1d | ||
|
|
80608c712a | ||
|
|
99165c8e8e | ||
|
|
a0357830a5 |
||
|
|
36cf751ed3 |
||
|
|
b4b0e4bc54 | ||
|
|
5aab3794ad | ||
|
|
f581cf0571 | ||
|
|
02a224f444 |
||
|
|
1441fc20dc |
||
|
|
c5b1c480a8 |
||
|
|
4b26faf0d2 |
||
|
|
b386f30b5f |
||
|
|
0805c8fd36 |
||
|
|
60cd60b0e7 |
||
|
|
1ac85cc784 |
||
|
|
c2179807d1 |
||
|
|
51a93353db |
||
|
|
7c5fd6103b |
||
|
|
0d359ca1af |
||
|
|
2eb49e0fd0 |
||
|
|
2bf8087c26 |
||
|
|
a1003eb8bd |
||
|
|
e7919b7f5f |
||
|
|
401a53c779 |
||
|
|
a1d609dc47 | ||
|
|
8ac885f32e | ||
|
|
badca9126c | ||
|
|
f7395255f4 | ||
|
|
a95cf03fb2 | ||
|
|
e57a02c201 | ||
|
|
3a50eb9483 |
||
|
|
c9ab55448d |
||
|
|
06e672671c | ||
|
|
874784995b | ||
|
|
645e5d4200 | ||
|
|
47272d2a14 |
||
|
|
307cff3b2f |
||
|
|
08adaa2cd1 |
||
|
|
f748429080 |
||
|
|
45cf55d47a |
||
|
|
39fff4ccd7 |
||
|
|
65a0a1e2b6 | ||
|
|
276b734666 | ||
|
|
e5f3b49a55 | ||
|
|
db399c987f | ||
|
|
635fb03f77 |
||
|
|
062e4adc50 |
||
|
|
1542558ad8 |
||
|
|
c922ad1412 |
||
|
|
514a3b6f92 | ||
|
|
e5f1b85231 | ||
|
|
7cb5d92018 | ||
|
|
4498d4c609 |
||
|
|
d668898a5a |
||
|
|
932b424c54 | ||
|
|
eed1425e3d | ||
|
|
27a22d57ef |
||
|
|
186a212831 |
||
|
|
f372adfafd |
||
|
|
3d3215bda0 | ||
|
|
e36fae0ae1 | ||
|
|
ac19a0d84c | ||
|
|
433eab8e90 |
||
|
|
791583941b | ||
|
|
2124819115 | ||
|
|
28288e091c | ||
|
|
29255f5a62 | ||
|
|
68ac2c7515 | ||
|
|
670825979d | ||
|
|
0e6b9fccfa |
||
|
|
9f65fa8493 |
||
|
|
8cc0960eee |
||
|
|
b9ef950a5e |
||
|
|
ecda46b0b8 | ||
|
|
96cb5fc50a | ||
|
|
0f3f092228 | ||
|
|
b9d30ab235 |
||
|
|
2f8360ec24 |
||
|
|
235f6ed70c | ||
|
|
04bf4952a7 | ||
|
|
f9d372b606 | ||
|
|
d0a566a495 | ||
|
|
b0f4479a65 | ||
|
|
390f392ea5 | ||
|
|
f69b7f5c27 | ||
|
|
0f5f811e17 | ||
|
|
7cf649d870 | ||
|
|
04595f439d | ||
|
|
320c6df106 | ||
|
|
8ad02f8912 | ||
|
|
bdc17791a5 | ||
|
|
82a1fc0437 | ||
|
|
11da0d4b86 | ||
|
|
4fb8bf0b9f |
||
|
|
e102706245 | ||
|
|
2837f863b1 | ||
|
|
ff6dd8bd28 | ||
|
|
57ddd10bcc | ||
|
|
028db424bd | ||
|
|
1c7285fb8d | ||
|
|
0508eb3931 | ||
|
|
5e510db0c5 | ||
|
|
6c358fad4b | ||
|
|
777af4e27f | ||
|
|
cf4d16369c |
||
|
|
c4b034494e |
||
|
|
7adc35d582 | ||
|
|
8585a9df7c | ||
|
|
2bf04c12b0 | ||
|
|
55b5db79e2 | ||
|
|
8a93bdd98f | ||
|
|
6548056de5 | ||
|
|
044843a3c3 |
||
|
|
585e896a61 | ||
|
|
9908e62a90 | ||
|
|
29ad981ba8 | ||
|
|
ea5ec6c435 | ||
|
|
e6c9fe5e97 | ||
|
|
5ff2b0d5cd |
||
|
|
282cf0d0a7 | ||
|
|
e983f9b1aa |
||
|
|
abe3530887 |
||
|
|
46e146f41e | ||
|
|
d35ee729bc | ||
|
|
151e4b53ed | ||
|
|
bb1d22f970 | ||
|
|
f2f579af1d | ||
|
|
a315aacd42 | ||
|
|
1dac608fb5 | ||
|
|
df19c352bd | ||
|
|
2684fd353d |
||
|
|
c82cc456e1 | ||
|
|
7e9115bc4a | ||
|
|
0be38ff1d0 |
||
|
|
b0346bfed1 | ||
|
|
b4ce435b4b |
||
|
|
f9e029ca0d | ||
|
|
b56dc6bdfd | ||
|
|
2c1d46e800 | ||
|
|
c966cc500c | ||
|
|
b7ca56b873 |
||
|
|
4e322c234e |
||
|
|
1e659eec9b |
||
|
|
84670eea99 |
||
|
|
4b493bd49c |
||
|
|
9c4f6eefd9 |
||
|
|
d76649b0ae |
||
|
|
146b5e41c0 |
||
|
|
6751f840ab | ||
|
|
e1106df996 |
||
|
|
246be483fe |
||
|
|
4e970dc149 |
||
|
|
04bfbf1ff0 |
||
|
|
0cfeb20dd5 |
||
|
|
49dee5fc46 |
||
|
|
5f0007ce9a |
||
|
|
d30af45b0d | ||
|
|
fa9bfd716d |
||
|
|
decf990892 |
||
|
|
fd80cb43f0 |
||
|
|
90fdaa6303 |
||
|
|
9789d64baf |
||
|
|
1b58f4b357 |
||
|
|
4a49bb6e9e |
||
|
|
8a54b9f82e |
||
|
|
b3d29d8f4b |
||
|
|
1ec4d9ded7 |
||
|
|
ac36cbd737 |
||
|
|
60acd6d8b7 |
||
|
|
99f6dac1a6 |
||
|
|
ff209d4539 |
||
|
|
febdd03940 |
||
|
|
e0437a7cef |
||
|
|
660f8c51eb |
||
|
|
83f4e1ce49 |
||
|
|
a6596786a1 |
||
|
|
d73b7282dd |
||
|
|
9ea5993377 | ||
|
|
1adcd486b8 |
||
|
|
0e74439edb | ||
|
|
4c32e86994 | ||
|
|
784c9b2b12 | ||
|
|
6340e56995 | ||
|
|
65a8d6ebf5 | ||
|
|
c42febf216 | ||
|
|
850fafe579 | ||
|
|
b57280ed69 | ||
|
|
a4f099dd72 |
||
|
|
9b6f2b1e07 | ||
|
|
42176717dc | ||
|
|
8397c403c0 | ||
|
|
d5bd5a075f | ||
|
|
42728125fa | ||
|
|
8ab0a3d086 | ||
|
|
5ff75ea7bf |
||
|
|
6cafd6f2cd |
||
|
|
42c8c37a96 | ||
|
|
de8ba37023 |
||
|
|
78376407b5 | ||
|
|
3f1afb2ecb |
||
|
|
10cb09ab5d |
||
|
|
37700c8700 |
||
|
|
413c8d957b |
||
|
|
0a098b96dd |
||
|
|
d5a40498ee |
||
|
|
523a25b664 |
||
|
|
59fcd073e3 |
||
|
|
e6cbccdadc |
||
|
|
03a415b18e | ||
|
|
2025fb27b4 | ||
|
|
4af2028fb1 | ||
|
|
88ec0a3fac | ||
|
|
cd7e32d60e | ||
|
|
834d7184e1 | ||
|
|
e2f9f3f94a | ||
|
|
ed518798e9 | ||
|
|
696283ff8f | ||
|
|
23b582a5bd |
||
|
|
b74a8eaf34 | ||
|
|
f0167dbd54 | ||
|
|
c6a3fa1eca | ||
|
|
e078eb6001 | ||
|
|
df163b4f7b |
||
|
|
c9eaf33728 |
||
|
|
0650b85d94 |
||
|
|
fb280188e1 |
||
|
|
b7ba606c17 |
||
|
|
bd6de6b5df |
||
|
|
3b6f7aac37 |
||
|
|
7adad51d3d | ||
|
|
fc94c6a909 | ||
|
|
333b8c9f33 | ||
|
|
a9f4aba656 | ||
|
|
a4ca0a78b5 |
||
|
|
9599cc9ca2 | ||
|
|
7c1290f152 | ||
|
|
2949bf9df7 | ||
|
|
08811ba987 | ||
|
|
f0ed1f5a53 | ||
|
|
149a9b6c31 | ||
|
|
6e6979c91f | ||
|
|
a68e443f56 | ||
|
|
22c4815523 | ||
|
|
9b38a1a3d7 |
||
|
|
b9f886b321 |
||
|
|
555a8f5526 |
||
|
|
2720cccb2b |
||
|
|
c3a359f554 |
||
|
|
c8662542d6 |
||
|
|
619be7a1a9 | ||
|
|
f94f631d49 |
||
|
|
c9160cb913 |
||
|
|
74ea1f3e39 | ||
|
|
3a29bdbae7 | ||
|
|
876503b8bf | ||
|
|
75158676ef | ||
|
|
8d785436aa | ||
|
|
45bb894698 | ||
|
|
1795808486 | ||
|
|
4d2583efbb | ||
|
|
a16d3fd2e3 | ||
|
|
147509d54d | ||
|
|
75effa75c8 |
||
|
|
8ec9a33978 |
||
|
|
a1e16af3b5 | ||
|
|
888f634ccb |
||
|
|
82c9d5ab5f | ||
|
|
bc2cc38a72 | ||
|
|
2e1bda9e43 | ||
|
|
d6d95c40bd |
||
|
|
14112242cd | ||
|
|
50b53d5cbd | ||
|
|
c3ffb2239f | ||
|
|
601886da2b |
||
|
|
7416b7f1e1 | ||
|
|
dc6210ede7 |
||
|
|
0b8a2b6112 |
||
|
|
da547085a4 | ||
|
|
773d84f287 |
||
|
|
0d787b524c | ||
|
|
6c780cc9e9 |
||
|
|
450fed6ac8 |
||
|
|
9950179545 | ||
|
|
ed64b38a02 | ||
|
|
c39ba626d2 |
||
|
|
9af2ab63f8 | ||
|
|
47668b51dd | ||
|
|
9671e3d5d4 | ||
|
|
80c638b599 | ||
|
|
20a5bc0847 |
||
|
|
e30a858df7 |
||
|
|
42aa05f943 | ||
|
|
89640dc559 |
||
|
|
56f2168cf0 | ||
|
|
0be1e9a35b |
||
|
|
3ea613b934 |
||
|
|
d3179c5825 |
||
|
|
5ef52035c7 | ||
|
|
217d8e9257 | ||
|
|
c63b575f92 | ||
|
|
8e6297c3d4 |
||
|
|
ef47e9d3c9 | ||
|
|
b6ab3316e6 | ||
|
|
09e9a80cba | ||
|
|
3a2e4f61b0 |
||
|
|
2deaff56b2 |
||
|
|
cc59657579 | ||
|
|
1e31060d41 | ||
|
|
5a21d2d460 | ||
|
|
b05ad740bb | ||
|
|
1fb39edcae | ||
|
|
a99c6a5ad9 | ||
|
|
31078c16d9 | ||
|
|
50972c0760 | ||
|
|
908ce72b36 | ||
|
|
0c7cb31485 |
||
|
|
b927f54717 |
5656 changed files with 28871 additions and 131073 deletions
|
|
@ -1,14 +0,0 @@
|
|||
module.exports = {
|
||||
env: {
|
||||
es2021: true,
|
||||
node: true,
|
||||
},
|
||||
extends: ['eslint:recommended', 'plugin:prettier/recommended'],
|
||||
parserOptions: {
|
||||
ecmaVersion: 13,
|
||||
sourceType: 'module',
|
||||
},
|
||||
rules: {
|
||||
'prettier/prettier': ['error'],
|
||||
},
|
||||
};
|
||||
0
FUNDING.yml → .github/FUNDING.yml
vendored
0
FUNDING.yml → .github/FUNDING.yml
vendored
15
.github/ISSUE_TEMPLATE/bug_report.md
vendored
15
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
|
@ -13,22 +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:
|
||||
* Bits:
|
||||
- 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
Normal file
36
.github/actions/setup/action.yml
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
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
|
|
@ -9,23 +9,19 @@ jobs:
|
|||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
# Skip job on forks
|
||||
if: github.repository_owner == 'iconoir-icons'
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- uses: pnpm/action-setup@v2.1.0
|
||||
with:
|
||||
version: 7.8.0
|
||||
run_install: true
|
||||
- run: pnpm run build
|
||||
- uses: stefanzweifel/git-auto-commit-action@v4
|
||||
with:
|
||||
commit_message: Update build artifacts
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Build CSS
|
||||
run: pnpm run build css
|
||||
|
||||
- name: Commit changes
|
||||
uses: stefanzweifel/git-auto-commit-action@v6
|
||||
with:
|
||||
commit_message: Update build artifacts
|
||||
|
|
|
|||
28
.github/workflows/ci.yaml
vendored
Normal file
28
.github/workflows/ci.yaml
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
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 }}
|
||||
72
.github/workflows/release.yaml
vendored
72
.github/workflows/release.yaml
vendored
|
|
@ -9,48 +9,56 @@ jobs:
|
|||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main # We have to checkout main or PNPM fails. Tag should be on main anyway.
|
||||
- uses: actions/cache@v2
|
||||
# We have to checkout main or PNPM fails. Tag should be on main anyway.
|
||||
ref: main
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-
|
||||
- uses: actions/setup-node@v2
|
||||
node-registry: https://registry.npmjs.org
|
||||
|
||||
- name: Generate changelog file
|
||||
uses: rhysd/changelog-from-release/action@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- uses: pnpm/action-setup@v2.1.0
|
||||
with:
|
||||
version: 7.8.0
|
||||
run_install: true
|
||||
- uses: rhysd/changelog-from-release/action@v2
|
||||
with:
|
||||
file: packages/iconoir-flutter/CHANGELOG.md
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: packages/iconoir-flutter/CHANGELOG.md
|
||||
commit: false
|
||||
- run: pnpm run build
|
||||
- run: pnpm run prepublish-all
|
||||
args: -d=false
|
||||
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
|
||||
- name: Prepare packages
|
||||
run: pnpm run prepublish-all
|
||||
env:
|
||||
TAG_NAME: ${{ github.ref_name }}
|
||||
- uses: stefanzweifel/git-auto-commit-action@v4
|
||||
|
||||
- name: Commit release
|
||||
uses: stefanzweifel/git-auto-commit-action@v6
|
||||
with:
|
||||
commit_message: Release Version ${{ github.ref_name }}
|
||||
branch: main
|
||||
- run: git -c user.email="actions@github.com" -c user.name="GitHub Actions" tag -fa ${{ github.ref_name }} -m "${{ github.ref_name }}"
|
||||
- run: git push -f origin ${{ github.ref_name }}
|
||||
- run: npm publish --access public
|
||||
continue-on-error: true
|
||||
|
||||
- name: Update tag
|
||||
run: |
|
||||
git -c user.email="actions@github.com" -c user.name="GitHub Actions" tag -fa ${{ github.ref_name }} -m "${{ github.ref_name }}"
|
||||
git push -f origin ${{ github.ref_name }}
|
||||
|
||||
- name: Publish packages
|
||||
run: pnpm -r publish --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
- run: pnpm -r publish --filter './packages/**' --access public
|
||||
continue-on-error: true
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
# - uses: sakebook/actions-flutter-pub-publisher@v1.4.0
|
||||
# with:
|
||||
# credential: ${{ secrets.PUB_CREDENTIAL_JSON }}
|
||||
# package_directory: ./packages/iconoir-flutter
|
||||
NPM_CONFIG_PROVENANCE: true
|
||||
|
||||
- name: Publish Flutter
|
||||
uses: k-paxian/dart-package-publisher@v1.6
|
||||
with:
|
||||
credentialJson: ${{ secrets.PUB_CREDENTIAL_JSON }}
|
||||
relativePath: ./packages/iconoir-flutter
|
||||
|
|
|
|||
71
.github/workflows/website.yaml
vendored
71
.github/workflows/website.yaml
vendored
|
|
@ -1,62 +1,59 @@
|
|||
name: Website
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
workflow_run:
|
||||
workflows:
|
||||
- Release
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: pages
|
||||
group: ${{ github.workflow }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: main # We have to checkout main or PNPM fails. Tag should be on main anyway.
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- uses: pnpm/action-setup@v2.1.0
|
||||
with:
|
||||
version: 7.8.0
|
||||
run_install: true
|
||||
- name: Build Packages
|
||||
run: pnpm run dist
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Build
|
||||
run: ./node_modules/.bin/next build
|
||||
run: pnpm run build react
|
||||
|
||||
- name: Build website
|
||||
run: pnpm run build
|
||||
working-directory: iconoir.com
|
||||
- name: Export
|
||||
run: ./node_modules/.bin/next export
|
||||
working-directory: iconoir.com
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup GitHub Pages
|
||||
uses: actions/configure-pages@v5
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v1
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: './iconoir.com/out'
|
||||
path: ./iconoir.com/out
|
||||
|
||||
deploy:
|
||||
name: Deploy
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v1
|
||||
uses: actions/deploy-pages@v4
|
||||
|
|
|
|||
8
.gitignore
vendored
8
.gitignore
vendored
|
|
@ -1,2 +1,10 @@
|
|||
.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
.node-version
Normal file
1
.node-version
Normal file
|
|
@ -0,0 +1 @@
|
|||
22
|
||||
1
.npmrc
1
.npmrc
|
|
@ -1 +0,0 @@
|
|||
strict-peer-dependencies=false
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"singleQuote": true
|
||||
}
|
||||
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"recommendations": ["dbaeumer.vscode-eslint"]
|
||||
}
|
||||
51
.vscode/settings.json
vendored
Normal file
51
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
// 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
|
||||
|
|
|
|||
74
NAMING_CONVENTION.md
Normal file
74
NAMING_CONVENTION.md
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
# Naming Convention
|
||||
|
||||
## Hierarchy
|
||||
|
||||
The foundation of the naming convention in Iconoir is:
|
||||
|
||||
```
|
||||
[Object]-[Modifier]-[Container]
|
||||
```
|
||||
|
||||
### [Object]
|
||||
|
||||
Contains one or more objects.
|
||||
|
||||
### [Modifier]
|
||||
|
||||
It's often an additional and unique element added as a secondary object (`-minus`, `-plus`, `-warning`).
|
||||
|
||||
### [Container]
|
||||
|
||||
A shape used as a container for the object (`-square`, `-circle`).
|
||||
If a shape is directly part of an object, it's not considered a container.
|
||||
|
||||
\* You could notice a difference between icons such as
|
||||
`user-minus` and `minus-square`. The second one looks different
|
||||
because in this case the minus symbol is an _[Object]_, followed
|
||||
by the _[Container]_.
|
||||
|
||||
## Styles
|
||||
|
||||
An optional rule is regarding icons style. Iconoir is actually offering Regular and Solid icons. With the latter, icons names end with `-solid`. An example here:
|
||||
|
||||
- Regular: `check-circle`
|
||||
- Solid: `check-circle-solid`
|
||||
|
||||
## Object-Oriented Naming
|
||||
|
||||
Exceptions apart, icons follow an object-oriented naming and
|
||||
should not embed actions in their names.
|
||||
|
||||
Examples:
|
||||
|
||||
- `user-minus` is correct.
|
||||
- `remove-user` would be wrong.
|
||||
|
||||
## Physical Actions
|
||||
|
||||
Icons that represent a physical action or movement
|
||||
can embed that action in their name.
|
||||
|
||||
Examples:
|
||||
|
||||
- `walking`, `running`, `vehicle-fast`, `crane-lifting` are correct.
|
||||
|
||||
## Most-Used Modifiers and Shapes
|
||||
|
||||
### Modifiers
|
||||
|
||||
`-plus`, `-minus`, `-warning`, `-check`, `-xmark`, `-tag`,
|
||||
`-ban`, `-slash`
|
||||
|
||||
- `-plus-in` and `minus-in`: Differently from `-plus` and `-minus`, these are used when the icon
|
||||
has a bigger plus or minus icon in the center or inside the main object.
|
||||
- `-no-access`: It's regularly used when the icon has a restrict
|
||||
symbol in a corner.
|
||||
|
||||
### Shapes
|
||||
|
||||
`-square`, `-circle`
|
||||
|
||||
## Exceptions
|
||||
|
||||
If you spot an icon that is not following any of the rules,
|
||||
please open a [new issue](https://github.com/iconoir-icons/iconoir/issues/new/choose) on GitHub.
|
||||
112
README.md
112
README.md
|
|
@ -1,69 +1,70 @@
|
|||
<div align="center">
|
||||
<img src="assets/cover.png" alt="Iconoir" />
|
||||
</div>
|
||||
# Iconoir
|
||||
|
||||
<div align="center">
|
||||
Iconoir is an open-source library with 1000+ unique SVG icons, designed on a 24x24 pixels grid. No premium icons, no email sign-up, no newsletters.
|
||||
</div>
|
||||
[](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 align="center">
|
||||
<a href="https://iconoir.com"><strong>Browse at iconoir.com →</strong></a>
|
||||
</div>
|
||||
## What is Iconoir?
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/lucaburgio/iconoir/releases">
|
||||
<img src="https://img.shields.io/github/v/release/lucaburgio/iconoir?style=flat-square" alt="Version" />
|
||||
</a>
|
||||
<a href="https://github.com/lucaburgio/iconoir">
|
||||
<img src="https://img.shields.io/github/stars/lucaburgio/iconoir?style=flat-square" alt="Project Stars" />
|
||||
</a>
|
||||
<a href="https://www.npmjs.com/package/iconoir-react">
|
||||
<img src="https://img.shields.io/npm/dm/iconoir-react?color=98E8F3&label=react&style=flat-square" alt="React Library" />
|
||||
</a>
|
||||
<a href="https://github.com/lucaburgio/iconoir/blob/main/LICENSE">
|
||||
<img src="https://img.shields.io/github/license/lucaburgio/iconoir?style=flat-square" alt="License" />
|
||||
</a>
|
||||
</div>
|
||||
Iconoir is an open-source library with 1600+ unique SVG icons, designed on a 24x24 pixels grid.
|
||||
|
||||
<a href="https://iconoir.com"><strong>Browse at iconoir.com →</strong></a>
|
||||
|
||||
## Basic Usage
|
||||
|
||||
You can download any icon of the pack directly from https://iconoir.com or get them from this repository.
|
||||
|
||||
Additionally, the icons are available via the `iconoir` NPM package:
|
||||
```bash
|
||||
yarn add iconoir
|
||||
# or
|
||||
npm i iconoir
|
||||
```
|
||||
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` |
|
||||
|
||||
Example usage:
|
||||
|
||||
```js
|
||||
import Iconoir from 'iconoir/icons/iconoir.svg'
|
||||
import Iconoir from 'iconoir/icons/iconoir.svg';
|
||||
```
|
||||
|
||||
## React
|
||||
|
||||
A React library is available to install under the name `iconoir-react`. For more details, see the package [README](./packages/iconoir-react).
|
||||
A React library is available under the name `iconoir-react`.
|
||||
|
||||
For more details, see the package [README](./packages/iconoir-react).
|
||||
|
||||
## React Native
|
||||
|
||||
A React Native library is available to install under the name `iconoir-react-native`. For more details, see the package [README](./packages/iconoir-react-native).
|
||||
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).
|
||||
|
||||
## Flutter
|
||||
|
||||
A Flutter library is available to install under the name `iconoir_flutter`. For more details, see the package [README](./packages/iconoir-flutter).
|
||||
A Flutter library is available 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) now. To start using the icons: On the top menu, `Insert` > `Graphics` > `Iconoir`.
|
||||
Iconoir is happily part of [Framer](https://framer.com).
|
||||
|
||||
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/lucaburgio/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:
|
||||
|
|
@ -71,12 +72,49 @@ Here is an example in HTML:
|
|||
```html
|
||||
<i class="iconoir-hand-brake"></i>
|
||||
```
|
||||
|
||||
The class must always be "iconoir-" and then the name of the icon. You can find the names of the icons [here](https://iconoir.com).
|
||||
|
||||
The icons are `display: inline-block` and default to the current font size. You can control this
|
||||
by adjusting the `::before` styles of the element (which is where the icons are added as a mask).
|
||||
|
||||
## Figma
|
||||
|
||||
The library is available in the Figma community [here](https://www.figma.com/community/file/983248991460488027/Iconoir-Pack).
|
||||
|
||||
## Swift Package
|
||||
|
||||
To add `Iconoir-swift` to your Xcode project, follow these steps:
|
||||
|
||||
1. In Xcode, open your project and navigate to _File_ > _Swift Packages_ > _Add Package Dependency..._
|
||||
2. Enter the repository URL: `https://github.com/iconoir-icons/iconoir-swift.git`
|
||||
3. Choose the branch or version you want to add, and click _Next_.
|
||||
4. Select the target where you want to use the package, then click _Finish_.
|
||||
|
||||
### UIKit
|
||||
|
||||
```swift
|
||||
import UIKit
|
||||
import Iconoir
|
||||
|
||||
let imageView = UIImageView(image: Iconoir.bell.asUIImage)
|
||||
```
|
||||
|
||||
### SwiftUI
|
||||
|
||||
```swift
|
||||
import SwiftUI
|
||||
import Iconoir
|
||||
|
||||
struct ContentView: View {
|
||||
var body: some View {
|
||||
Iconoir.bell.asImage
|
||||
.foregroundColor(.blue)
|
||||
.font(.system(size: 24))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT License
|
||||
MIT License.
|
||||
|
|
|
|||
BIN
assets/cover.png
BIN
assets/cover.png
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
482
bin/build.js
482
bin/build.js
|
|
@ -1,482 +0,0 @@
|
|||
import execa from 'execa';
|
||||
import { promises as fs, readFileSync } from 'fs';
|
||||
import { generateTemplateFilesBatch } from 'generate-template-files';
|
||||
import { Listr } from 'listr2';
|
||||
import os from 'os';
|
||||
import path, { basename, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { incompatibleNames } from '../constants.js';
|
||||
|
||||
// Paths
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const rootDir = path.join(__dirname, '..');
|
||||
const iconoirIconsDir = path.join(rootDir, 'icons');
|
||||
const ignoreCleanFilenames = ['IconoirContext.tsx'];
|
||||
|
||||
// Targets for building icons
|
||||
const targets = {
|
||||
'meta-data': { path: 'meta-data.json' },
|
||||
css: { path: 'css/iconoir.css' },
|
||||
'iconoir-flutter': { flutter: true, path: 'packages/iconoir-flutter' },
|
||||
'iconoir-react': { react: true, path: 'packages/iconoir-react' },
|
||||
'iconoir-react-native': {
|
||||
react: true,
|
||||
path: 'packages/iconoir-react-native',
|
||||
},
|
||||
};
|
||||
|
||||
// Get targets from command line arguments
|
||||
// (build all targets if no arguments)
|
||||
const args = process.argv.slice(2);
|
||||
const cliTargets = [];
|
||||
args.forEach((target) => {
|
||||
if (target in targets) {
|
||||
cliTargets.push(target);
|
||||
} else {
|
||||
console.error(`Target '${target}' doesn't exist!\n\nPossible targets are:`);
|
||||
for (const [targetName] of Object.entries(targets)) {
|
||||
console.log(`- ${targetName}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Build tasks
|
||||
const tasks = new Listr(
|
||||
[
|
||||
{
|
||||
title: 'Fetching icons',
|
||||
task: async (ctx) => {
|
||||
ctx.iconoirIconsFiles = await fs.readdir(iconoirIconsDir);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Building targets',
|
||||
skip: (ctx) => !ctx.iconoirIconsFiles,
|
||||
task: (_, task) =>
|
||||
task.newListr(
|
||||
[
|
||||
{
|
||||
title: 'Building meta-data file',
|
||||
enabled: () =>
|
||||
cliTargets.length === 0 || cliTargets.includes('meta-data'),
|
||||
task: async (ctx) => {
|
||||
await fs.writeFile(
|
||||
path.join(rootDir, targets['meta-data'].path),
|
||||
JSON.stringify({ icons: ctx.iconoirIconsFiles })
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Building CSS file',
|
||||
enabled: () =>
|
||||
cliTargets.length === 0 || cliTargets.includes('css'),
|
||||
task: async (ctx) => {
|
||||
const content = [
|
||||
(
|
||||
await fs.readFile(
|
||||
path.join(__dirname, 'header.css'),
|
||||
'utf8'
|
||||
)
|
||||
).replace('[YEAR]', new Date().getFullYear()),
|
||||
];
|
||||
ctx.iconoirIconsFiles.forEach((file) => {
|
||||
const fileContents = readFileSync(
|
||||
path.join(__dirname, '../icons/', file)
|
||||
)
|
||||
.toString()
|
||||
.replace(/\n/g, '');
|
||||
content.push(
|
||||
`.iconoir-${
|
||||
path.parse(file).name
|
||||
}::before{mask-image:url('data:image/svg+xml;charset=utf-8,${fileContents}');-webkit-mask-image:url('data:image/svg+xml;charset=utf-8,${fileContents}');}`
|
||||
);
|
||||
});
|
||||
await fs.writeFile(
|
||||
path.join(rootDir, targets.css.path),
|
||||
content
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Building React libraries',
|
||||
enabled: () =>
|
||||
cliTargets.length === 0 ||
|
||||
cliTargets.filter((cliTarget) => targets[cliTarget]?.react)
|
||||
.length > 0,
|
||||
task: (_, task) =>
|
||||
task.newListr(
|
||||
[
|
||||
{
|
||||
title: 'Creating temporary directory',
|
||||
task: async (ctx) => {
|
||||
try {
|
||||
ctx.tmpDir = await fs.mkdtemp(
|
||||
path.join(os.tmpdir(), 'iconoir-')
|
||||
);
|
||||
} catch (err) {
|
||||
ctx.skip = true;
|
||||
throw new Error(err.message);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'Copying icon files to temporary directory, while renaming icons with incompatible names',
|
||||
skip: (ctx) => ctx.skip,
|
||||
task: async (ctx) => {
|
||||
try {
|
||||
const promises = ctx.iconoirIconsFiles.map((file) => {
|
||||
const srcFilePath = path.join(
|
||||
iconoirIconsDir,
|
||||
file
|
||||
);
|
||||
const iconName = file.split('.')[0];
|
||||
const dstFileName =
|
||||
iconName in incompatibleNames
|
||||
? incompatibleNames[iconName]
|
||||
: iconName;
|
||||
const dstFilePath = path.join(
|
||||
ctx.tmpDir,
|
||||
`${dstFileName}.svg`
|
||||
);
|
||||
|
||||
return fs.copyFile(srcFilePath, dstFilePath);
|
||||
});
|
||||
return Promise.all(promises).catch((err) => {
|
||||
ctx.skip = true;
|
||||
throw new Error(err.message);
|
||||
});
|
||||
} catch (err) {
|
||||
ctx.skip = true;
|
||||
throw new Error(err.message);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
skip: (ctx) => ctx.skip,
|
||||
task: (_, task) => {
|
||||
const targetsToBuild =
|
||||
cliTargets.length > 0
|
||||
? cliTargets.filter(
|
||||
(cliTarget) => targets[cliTarget]?.react
|
||||
)
|
||||
: Object.keys(targets).filter(
|
||||
(target) => targets[target].react
|
||||
);
|
||||
const tasks = targetsToBuild.map((target) => {
|
||||
const builtIconsDir = path.join(
|
||||
rootDir,
|
||||
targets[target].path,
|
||||
'src'
|
||||
);
|
||||
return {
|
||||
title: `Building ${target}`,
|
||||
task: (_, task) =>
|
||||
task.newListr(
|
||||
[
|
||||
{
|
||||
title: 'Cleaning target directory',
|
||||
task: async (ctx) => {
|
||||
try {
|
||||
const files = await fs.readdir(
|
||||
builtIconsDir
|
||||
);
|
||||
const promises = files
|
||||
.filter(
|
||||
(file) =>
|
||||
!ignoreCleanFilenames.includes(
|
||||
path.basename(file)
|
||||
)
|
||||
)
|
||||
.map((file) => {
|
||||
return fs.unlink(
|
||||
path.join(builtIconsDir, file)
|
||||
);
|
||||
});
|
||||
return Promise.all(promises).catch(
|
||||
(err) => {
|
||||
ctx[target] = { skip: true };
|
||||
throw new Error(err.message);
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
ctx[target] = { skip: true };
|
||||
throw new Error(err.message);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Building icon files',
|
||||
skip: (ctx) => ctx[target]?.skip,
|
||||
task: async (ctx) => {
|
||||
try {
|
||||
await execa(
|
||||
'svgr',
|
||||
[
|
||||
'--config-file',
|
||||
path.join(
|
||||
targets[target].path,
|
||||
'.svgrrc.json'
|
||||
),
|
||||
'--out-dir',
|
||||
builtIconsDir,
|
||||
'--template',
|
||||
'bin/templates/icon-template.cjs',
|
||||
'--index-template',
|
||||
'bin/templates/index-template.cjs',
|
||||
ctx.tmpDir,
|
||||
],
|
||||
{ preferLocal: true }
|
||||
);
|
||||
} catch (err) {
|
||||
throw new Error(err.message);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
{ concurrent: false, exitOnError: false }
|
||||
),
|
||||
};
|
||||
});
|
||||
return task.newListr(tasks, {
|
||||
concurrent: true,
|
||||
rendererOptions: { collapse: false },
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
{ concurrent: false }
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Building Flutter libraries',
|
||||
enabled: () =>
|
||||
cliTargets.length === 0 ||
|
||||
cliTargets.filter((cliTarget) => targets[cliTarget]?.flutter)
|
||||
.length > 0,
|
||||
task: (_, task) =>
|
||||
task.newListr(
|
||||
[
|
||||
{
|
||||
title: 'Creating temporary directory',
|
||||
task: async (ctx) => {
|
||||
try {
|
||||
ctx.tmpDir = await fs.mkdtemp(
|
||||
path.join(os.tmpdir(), 'iconoir-')
|
||||
);
|
||||
} catch (err) {
|
||||
ctx.skip = true;
|
||||
throw new Error(err.message);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'Copying icon files to temporary directory, while renaming icons with incompatible names',
|
||||
skip: (ctx) => ctx.skip,
|
||||
task: async (ctx) => {
|
||||
try {
|
||||
const promises = ctx.iconoirIconsFiles.map((file) => {
|
||||
const srcFilePath = path.join(
|
||||
iconoirIconsDir,
|
||||
file
|
||||
);
|
||||
const iconName = file.split('.')[0];
|
||||
const dstFileName =
|
||||
iconName in incompatibleNames
|
||||
? incompatibleNames[iconName]
|
||||
: iconName;
|
||||
const dstFilePath = path.join(
|
||||
ctx.tmpDir,
|
||||
`${dstFileName}.svg`
|
||||
);
|
||||
|
||||
ctx.dstFilePaths = [
|
||||
...(ctx.dstFilePaths ?? []),
|
||||
dstFilePath,
|
||||
];
|
||||
|
||||
return fs.copyFile(srcFilePath, dstFilePath);
|
||||
});
|
||||
return Promise.all(promises).catch((err) => {
|
||||
ctx.skip = true;
|
||||
throw new Error(err.message);
|
||||
});
|
||||
} catch (err) {
|
||||
ctx.skip = true;
|
||||
throw new Error(err.message);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
skip: (ctx) => ctx.skip,
|
||||
task: (_, task) => {
|
||||
const targetsToBuild =
|
||||
cliTargets.length > 0
|
||||
? cliTargets.filter(
|
||||
(cliTarget) => targets[cliTarget]?.flutter
|
||||
)
|
||||
: Object.keys(targets).filter(
|
||||
(target) => targets[target].flutter
|
||||
);
|
||||
const tasks = targetsToBuild.map((target) => {
|
||||
const builtIconsDir = path.join(
|
||||
rootDir,
|
||||
targets[target].path,
|
||||
'lib'
|
||||
);
|
||||
return {
|
||||
title: `Building ${target}`,
|
||||
task: (_, task) =>
|
||||
task.newListr(
|
||||
[
|
||||
{
|
||||
title: 'Cleaning target directory',
|
||||
task: async (ctx) => {
|
||||
try {
|
||||
const files = await fs.readdir(
|
||||
builtIconsDir
|
||||
);
|
||||
const promises = files.map((file) => {
|
||||
return fs.unlink(
|
||||
path.join(builtIconsDir, file)
|
||||
);
|
||||
});
|
||||
return Promise.all(promises).catch(
|
||||
(err) => {
|
||||
ctx[target] = { skip: true };
|
||||
throw new Error(err.message);
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
ctx[target] = { skip: true };
|
||||
throw new Error(err.message);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Create entry file',
|
||||
task: async () => {
|
||||
await fs.writeFile(
|
||||
path.join(
|
||||
builtIconsDir,
|
||||
'iconoir_flutter.dart'
|
||||
),
|
||||
'library iconoir_flutter;\n\n'
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Building icon files',
|
||||
skip: (ctx) => ctx[target]?.skip,
|
||||
task: async (ctx) => {
|
||||
const finalFileNames = [];
|
||||
try {
|
||||
await Promise.all(
|
||||
ctx.dstFilePaths.map(async (file) => {
|
||||
const svgfilename =
|
||||
path.parse(file).name;
|
||||
// Prefix with Svg if icon name starts with a number
|
||||
const iconname = `${
|
||||
/^\d/.test(svgfilename)
|
||||
? 'Svg'
|
||||
: ''
|
||||
}${svgfilename}`;
|
||||
|
||||
const svgfilecontent = (
|
||||
await fs.readFile(file)
|
||||
).toString();
|
||||
|
||||
await generateTemplateFilesBatch([
|
||||
{
|
||||
option:
|
||||
'Create Icon Flutter Widget',
|
||||
entry: {
|
||||
folderPath:
|
||||
'./bin/templates/__svgfilename__.dart',
|
||||
},
|
||||
dynamicReplacers: [
|
||||
{
|
||||
slot: '__icon__',
|
||||
slotValue: iconname,
|
||||
},
|
||||
{
|
||||
slot: '__svgfilecontent__',
|
||||
slotValue: svgfilecontent,
|
||||
},
|
||||
{
|
||||
slot: '__svgfilename__',
|
||||
slotValue: svgfilename,
|
||||
},
|
||||
],
|
||||
output: {
|
||||
path: './packages/iconoir-flutter/lib/__svgfilename__(snakeCase).dart',
|
||||
pathAndFileNameDefaultCase:
|
||||
'(snakeCase)',
|
||||
},
|
||||
async onComplete(results) {
|
||||
finalFileNames.push(
|
||||
results.output.path
|
||||
);
|
||||
},
|
||||
},
|
||||
]);
|
||||
})
|
||||
);
|
||||
|
||||
finalFileNames.sort();
|
||||
await fs.appendFile(
|
||||
path.join(
|
||||
builtIconsDir,
|
||||
'iconoir_flutter.dart'
|
||||
),
|
||||
finalFileNames
|
||||
.map(
|
||||
(fileName) =>
|
||||
`export './${basename(
|
||||
fileName
|
||||
)}';`
|
||||
)
|
||||
.join('\n')
|
||||
);
|
||||
} catch (err) {
|
||||
throw new Error(err.message);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
{ concurrent: false, exitOnError: false }
|
||||
),
|
||||
};
|
||||
});
|
||||
return task.newListr(tasks, {
|
||||
concurrent: true,
|
||||
rendererOptions: { collapse: false },
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
{ concurrent: false }
|
||||
),
|
||||
},
|
||||
],
|
||||
{ concurrent: true }
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Removing temporary directory',
|
||||
skip: (ctx) => !ctx.tmpDir,
|
||||
task: async (ctx) => {
|
||||
await fs.rm(ctx.tmpDir, { recursive: true });
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
concurrent: false,
|
||||
exitOnError: false,
|
||||
rendererOptions: { collapse: false, collapseErrors: false },
|
||||
}
|
||||
);
|
||||
|
||||
await tasks.run();
|
||||
139
bin/build/index.js
Normal file
139
bin/build/index.js
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
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();
|
||||
20
bin/build/lib/import-export.js
Normal file
20
bin/build/lib/import-export.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
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;
|
||||
}
|
||||
65
bin/build/lib/ts.js
Normal file
65
bin/build/lib/ts.js
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
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;
|
||||
}
|
||||
21
bin/build/targets/css/header.css
Normal file
21
bin/build/targets/css/header.css
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/*!
|
||||
* 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;
|
||||
}
|
||||
51
bin/build/targets/css/index.js
Normal file
51
bin/build/targets/css/index.js
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
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);
|
||||
};
|
||||
47
bin/build/targets/flutter/index.js
Normal file
47
bin/build/targets/flutter/index.js
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
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);
|
||||
}
|
||||
26
bin/build/targets/flutter/template.js
Normal file
26
bin/build/targets/flutter/template.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
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;
|
||||
263
bin/build/targets/react/index.js
Normal file
263
bin/build/targets/react/index.js
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
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);
|
||||
}
|
||||
36
bin/build/targets/react/resources/context-template.js
Normal file
36
bin/build/targets/react/resources/context-template.js
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
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'];
|
||||
34
bin/build/targets/react/resources/icon-template.js
Normal file
34
bin/build/targets/react/resources/icon-template.js
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
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};
|
||||
`;
|
||||
};
|
||||
}
|
||||
46
bin/build/targets/react/resources/svgr-options.js
Normal file
46
bin/build/targets/react/resources/svgr-options.js
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
/** @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'],
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
97
bin/build/targets/vue/index.js
Normal file
97
bin/build/targets/vue/index.js
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
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(),
|
||||
],
|
||||
});
|
||||
};
|
||||
21
bin/build/targets/vue/template.js
Normal file
21
bin/build/targets/vue/template.js
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
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;
|
||||
7
bin/build/worker.js
Normal file
7
bin/build/worker.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export default async ({ targetName, config, targetConfig }) => {
|
||||
const { default: task } = await import(
|
||||
`./targets/${targetConfig.target || targetName}/index.js`
|
||||
);
|
||||
|
||||
return task(config, targetConfig);
|
||||
};
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
/*!
|
||||
* Iconoir
|
||||
* Copyright (c) [YEAR] Luca Burgio - https://iconoir.com
|
||||
* License - https://github.com/lucaburgio/iconoir/blob/main/LICENSE (Code: MIT License)
|
||||
* CSS file created by Till Esser (@Wiwaltill) and automated by Pascal Jufer (@paescuj)
|
||||
*/
|
||||
|
||||
*[class^="iconoir-"]::before,
|
||||
*[class*=" iconoir-"]::before {
|
||||
content: " ";
|
||||
display: block;
|
||||
background: currentColor;
|
||||
mask-size: cover;
|
||||
-webkit-mask-size: cover;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
*[class^="iconoir-"],
|
||||
*[class*=" iconoir-"] {
|
||||
display: block;
|
||||
}
|
||||
|
|
@ -1,28 +1,34 @@
|
|||
import { updateYamlKey } from '@atomist/yaml-updater';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import semver from 'semver';
|
||||
/* eslint-disable no-console */
|
||||
|
||||
const PACKAGE_BASE = '';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import { updateYamlKey } from '@atomist/yaml-updater';
|
||||
import semver from 'semver';
|
||||
|
||||
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');
|
||||
}
|
||||
|
|
@ -33,13 +39,8 @@ 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');
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
class __icon__(pascalCase) extends StatelessWidget {
|
||||
final Color? color;
|
||||
final double? width;
|
||||
final double? height;
|
||||
|
||||
const __icon__(pascalCase)({Key? key, this.color, this.width, this.height})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => SvgPicture.string(
|
||||
'''
|
||||
__svgfilecontent__''',
|
||||
color: color,
|
||||
width: width,
|
||||
height: height,
|
||||
);
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
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;
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
const path = require('path');
|
||||
|
||||
function template(filePaths) {
|
||||
const exportEntries = filePaths.map((filePath) => {
|
||||
const basename = path.basename(filePath, path.extname(filePath));
|
||||
const exportName = /^\d/.test(basename) ? `Svg${basename}` : basename;
|
||||
return `export { default as ${exportName} } from './${basename}'`;
|
||||
});
|
||||
exportEntries.push(
|
||||
"export { IconoirProvider, IconoirContext, IconoirContextValue } from './IconoirContext'"
|
||||
);
|
||||
return exportEntries.join('\n');
|
||||
}
|
||||
|
||||
module.exports = template;
|
||||
12
constants.js
12
constants.js
|
|
@ -1,12 +0,0 @@
|
|||
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',
|
||||
};
|
||||
|
|
@ -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,7 +11,8 @@ 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).
|
||||
|
||||
<SuggestLibrary />
|
||||
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).
|
||||
|
|
|
|||
22
css/iconoir-regular.css
Normal file
22
css/iconoir-regular.css
Normal file
File diff suppressed because one or more lines are too long
22
css/iconoir-solid.css
Normal file
22
css/iconoir-solid.css
Normal file
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) 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.
|
||||
Iconoir is happily part of [Framer](https://framer.com).
|
||||
|
||||
<SuggestLibrary />
|
||||
To start using the icons: On the top menu, `Insert` > `Graphics` > `Iconoir`.
|
||||
|
||||
You can switch between icons from the right sidebar in the editor.
|
||||
|
|
|
|||
115
eslint.config.js
Normal file
115
eslint.config.js
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
// @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
Normal file
41
examples/next/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# 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
|
||||
17
examples/next/app/layout.tsx
Normal file
17
examples/next/app/layout.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
32
examples/next/app/page.tsx
Normal file
32
examples/next/app/page.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
9
examples/next/next.config.ts
Normal file
9
examples/next/next.config.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import type { NextConfig } from 'next';
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
experimental: {
|
||||
optimizePackageImports: ['iconoir-react'],
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
22
examples/next/package.json
Normal file
22
examples/next/package.json
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
27
examples/next/tsconfig.json
Normal file
27
examples/next/tsconfig.json
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"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
Normal file
36
examples/react-native/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# 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
|
||||
21
examples/react-native/App.tsx
Normal file
21
examples/react-native/App.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
21
examples/react-native/app.json
Normal file
21
examples/react-native/app.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
8
examples/react-native/index.ts
Normal file
8
examples/react-native/index.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
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);
|
||||
31
examples/react-native/metro.config.js
Normal file
31
examples/react-native/metro.config.js
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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,
|
||||
},
|
||||
});
|
||||
30
examples/react-native/package.json
Normal file
30
examples/react-native/package.json
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
6
examples/react-native/tsconfig.json
Normal file
6
examples/react-native/tsconfig.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"extends": "expo/tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
30
examples/vue/.gitignore
vendored
Normal file
30
examples/vue/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# 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
Normal file
1
examples/vue/env.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
||||
12
examples/vue/index.html
Normal file
12
examples/vue/index.html
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<!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>
|
||||
27
examples/vue/package.json
Normal file
27
examples/vue/package.json
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
26
examples/vue/src/App.vue
Normal file
26
examples/vue/src/App.vue
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<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>
|
||||
4
examples/vue/src/main.ts
Normal file
4
examples/vue/src/main.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
|
||||
createApp(App).mount('#app');
|
||||
13
examples/vue/tsconfig.app.json
Normal file
13
examples/vue/tsconfig.app.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"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__/*"]
|
||||
}
|
||||
11
examples/vue/tsconfig.json
Normal file
11
examples/vue/tsconfig.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
}
|
||||
],
|
||||
"files": []
|
||||
}
|
||||
19
examples/vue/tsconfig.node.json
Normal file
19
examples/vue/tsconfig.node.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"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.*"
|
||||
]
|
||||
}
|
||||
18
examples/vue/vite.config.ts
Normal file
18
examples/vue/vite.config.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
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)),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"extends": "next/core-web-vitals",
|
||||
"rules": {
|
||||
"react/no-unescaped-entities": ["off"]
|
||||
}
|
||||
}
|
||||
0
iconoir.com/.gitignore
vendored
Executable file → Normal file
0
iconoir.com/.gitignore
vendored
Executable file → Normal file
|
|
@ -1,34 +0,0 @@
|
|||
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,29 +1,11 @@
|
|||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export function Ad() {
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
const addedScript = React.useRef(false);
|
||||
React.useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (container && !addedScript.current) {
|
||||
addedScript.current = true;
|
||||
const script = document.createElement('script');
|
||||
script.async = true;
|
||||
script.type = 'text/javascript';
|
||||
script.src =
|
||||
'//cdn.carbonads.com/carbon.js?serve=CESDK5QJ&placement=iconoircom';
|
||||
script.id = '_carbonads_js';
|
||||
container.appendChild(script);
|
||||
}
|
||||
}, []);
|
||||
return <AdContainer ref={containerRef} />;
|
||||
}
|
||||
|
||||
const AdContainer = styled.div`
|
||||
#carbonads {
|
||||
margin: 24px 0 0 0;
|
||||
a {
|
||||
text-decoration: none !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
.carbon-wrap {
|
||||
display: flex;
|
||||
|
|
@ -31,6 +13,11 @@ const AdContainer = styled.div`
|
|||
> :first-child {
|
||||
margin-right: 12px;
|
||||
}
|
||||
& > a > img {
|
||||
width: 100px;
|
||||
height: 74px;
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
}
|
||||
.carbon-text {
|
||||
color: var(--black-80);
|
||||
|
|
@ -43,3 +30,24 @@ const AdContainer = styled.div`
|
|||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export function Ad() {
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
const addedScript = React.useRef(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
|
||||
if (container && !addedScript.current) {
|
||||
addedScript.current = true;
|
||||
const script = document.createElement('script');
|
||||
script.async = true;
|
||||
script.type = 'text/javascript';
|
||||
script.src = '//cdn.carbonads.com/carbon.js?serve=CESDK5QJ&placement=iconoircom';
|
||||
script.id = '_carbonads_js';
|
||||
container.appendChild(script);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return <AdContainer ref={containerRef} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import React from 'react';
|
||||
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[] {
|
||||
|
|
@ -11,7 +10,7 @@ function playWithLines1(setInstances: SetInstances): anime.AnimeInstance[] {
|
|||
strokeDashoffset: [anime.setDashoffset, 0],
|
||||
easing: 'easeInOutSine',
|
||||
duration: 1500,
|
||||
delay: function (el, i) {
|
||||
delay(_el, i) {
|
||||
return i * 250;
|
||||
},
|
||||
direction: 'alternate',
|
||||
|
|
@ -19,6 +18,7 @@ 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: function (el, i) {
|
||||
delay(_el, i) {
|
||||
return i * 250;
|
||||
},
|
||||
direction: 'alternate',
|
||||
|
|
@ -34,6 +34,7 @@ function playWithLines2(setInstances: SetInstances): anime.AnimeInstance[] {
|
|||
}),
|
||||
];
|
||||
}
|
||||
|
||||
function playWithLines3(setInstances: SetInstances): anime.AnimeInstance[] {
|
||||
return [
|
||||
anime({
|
||||
|
|
@ -41,7 +42,7 @@ function playWithLines3(setInstances: SetInstances): anime.AnimeInstance[] {
|
|||
strokeDashoffset: [anime.setDashoffset, 0],
|
||||
easing: 'easeInOutSine',
|
||||
duration: 1500,
|
||||
delay: function (el, i) {
|
||||
delay(_el, i) {
|
||||
return i * 250;
|
||||
},
|
||||
direction: 'alternate',
|
||||
|
|
@ -58,6 +59,7 @@ function playWithLines3(setInstances: SetInstances): anime.AnimeInstance[] {
|
|||
}),
|
||||
];
|
||||
}
|
||||
|
||||
function playWithLines4(setInstances: SetInstances): anime.AnimeInstance[] {
|
||||
return [
|
||||
anime({
|
||||
|
|
@ -65,7 +67,7 @@ function playWithLines4(setInstances: SetInstances): anime.AnimeInstance[] {
|
|||
strokeDashoffset: [anime.setDashoffset, 0],
|
||||
easing: 'easeInOutSine',
|
||||
duration: 1500,
|
||||
delay: function (el, i) {
|
||||
delay(_el, i) {
|
||||
return i * 250;
|
||||
},
|
||||
direction: 'alternate',
|
||||
|
|
@ -76,16 +78,19 @@ 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,90 +1,39 @@
|
|||
import React from 'react';
|
||||
import styled, { keyframes } from 'styled-components';
|
||||
import styled, { css, keyframes } from 'styled-components';
|
||||
import useResizeObserver from 'use-resize-observer';
|
||||
import { FEEDBACK_LINK, LIBRARY_LINKS, SUGGEST_LIBRARY } from './constants';
|
||||
import { media } from './responsive';
|
||||
import {
|
||||
FEEDBACK_LINK,
|
||||
LIBRARY_LINKS,
|
||||
SUGGEST_LIBRARY_LINK,
|
||||
} from '../lib/constants';
|
||||
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} 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: none;
|
||||
${media.lg} {
|
||||
&&& {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
text-align: center;
|
||||
${media.lg} {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const DesktopHeader = styled(Text14)`
|
||||
&&& {
|
||||
display: none;
|
||||
${media.lg} {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const AvailableForAnimation = keyframes`
|
||||
5% {
|
||||
transform: translateX(0);
|
||||
|
|
@ -99,26 +48,30 @@ const AvailableForAnimation = keyframes`
|
|||
transform: translateX(0);
|
||||
}
|
||||
`;
|
||||
|
||||
const AvailableForOuter = styled.div`
|
||||
max-width: 100vw;
|
||||
margin: 16px -30px 70px -30px;
|
||||
padding: 0 30px;
|
||||
overflow: hidden;
|
||||
${media.lg} {
|
||||
margin: 120px auto;
|
||||
margin: 80px auto;
|
||||
padding: 0;
|
||||
}
|
||||
`;
|
||||
const AvailableForContainer = styled.div<{ contentWidth: number }>`
|
||||
|
||||
const AvailableForContainer = styled.div<{ $contentWidth: number }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
width: max-content;
|
||||
--content-width: ${(props) => props.contentWidth}px;
|
||||
${(props) => (props.contentWidth ? '&' : '&.noop')} {
|
||||
animation: ${AvailableForAnimation} 40s cubic-bezier(0.37, 0, 0.63, 1)
|
||||
infinite;
|
||||
}
|
||||
--content-width: ${(props) => props.$contentWidth}px;
|
||||
${(props) =>
|
||||
props.$contentWidth
|
||||
&& css`
|
||||
animation: ${AvailableForAnimation} 40s cubic-bezier(0.37, 0, 0.63, 1)
|
||||
infinite;
|
||||
`}
|
||||
> :not(:last-child) {
|
||||
margin-right: 30px;
|
||||
}
|
||||
|
|
@ -129,14 +82,124 @@ const AvailableForContainer = styled.div<{ contentWidth: number }>`
|
|||
}
|
||||
${media.md} {
|
||||
> :not(:last-child) {
|
||||
margin-right: 60px;
|
||||
margin-right: 40px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const AvailableForImage = styled.img`
|
||||
height: 40px;
|
||||
display: block;
|
||||
transition: 0.2s;
|
||||
&:hover {
|
||||
scale: 1.2;
|
||||
transition: 0.2s;
|
||||
}
|
||||
${media.lg} {
|
||||
height: 50px;
|
||||
}
|
||||
`;
|
||||
|
||||
export function AvailableFor() {
|
||||
const { ref, width } = useResizeObserver();
|
||||
|
||||
return (
|
||||
<>
|
||||
<MobileHeader>Available For</MobileHeader>
|
||||
<AvailableForOuter>
|
||||
<AvailableForContainer $contentWidth={width || 0} ref={ref}>
|
||||
<DesktopHeader>Available for</DesktopHeader>
|
||||
<a href={LIBRARY_LINKS.React} target="_blank" rel="noreferrer">
|
||||
<AvailableForImage
|
||||
src="/logo-react.svg"
|
||||
alt="React Logo"
|
||||
title="React"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/iconoir-icons/iconoir#swift-package"
|
||||
target="_blank"
|
||||
rel="nofollow noreferrer"
|
||||
>
|
||||
<AvailableForImage
|
||||
src="/logo-swift.svg"
|
||||
alt="Swift Logo"
|
||||
title="Swift"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href={LIBRARY_LINKS.Flutter}
|
||||
target="_blank"
|
||||
rel="nofollow noreferrer"
|
||||
>
|
||||
<AvailableForImage
|
||||
src="/logo-flutter.svg"
|
||||
alt="Flutter Logo"
|
||||
title="Flutter"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href={LIBRARY_LINKS.Figma}
|
||||
target="_blank"
|
||||
rel="nofollow noreferrer"
|
||||
>
|
||||
<AvailableForImage
|
||||
src="/logo-figma.svg"
|
||||
alt="Figma Logo"
|
||||
title="Figma"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href={LIBRARY_LINKS.ReactNative}
|
||||
target="_blank"
|
||||
rel="nofollow noreferrer"
|
||||
>
|
||||
<AvailableForImage
|
||||
src="/logo-react-native.svg"
|
||||
alt="React Native Logo"
|
||||
title="React Native"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href={LIBRARY_LINKS.Vue}
|
||||
target="_blank"
|
||||
rel="nofollow noreferrer"
|
||||
>
|
||||
<AvailableForImage
|
||||
src="/logo-vue.svg"
|
||||
alt="Vue Logo"
|
||||
title="Vue"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href={LIBRARY_LINKS.Framer}
|
||||
target="_blank"
|
||||
rel="nofollow noreferrer"
|
||||
>
|
||||
<AvailableForImage
|
||||
src="/logo-framer.svg"
|
||||
alt="Framer Logo"
|
||||
title="Framer"
|
||||
/>
|
||||
</a>
|
||||
<AreYouUsing>
|
||||
<a
|
||||
href={SUGGEST_LIBRARY_LINK}
|
||||
target="_blank"
|
||||
rel="nofollow noreferrer"
|
||||
>
|
||||
<Text14>More?</Text14>
|
||||
</a>
|
||||
<a
|
||||
href={FEEDBACK_LINK}
|
||||
target="_blank"
|
||||
rel="nofollow noreferrer"
|
||||
>
|
||||
<Text14>Are you using the library?</Text14>
|
||||
</a>
|
||||
</AreYouUsing>
|
||||
</AvailableForContainer>
|
||||
</AvailableForOuter>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,55 +12,74 @@ export const ResetButton = styled.button`
|
|||
`;
|
||||
|
||||
export const LargeButton = styled(ResetButton)`
|
||||
background: var(--black);
|
||||
height: 75px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
color: var(--white);
|
||||
padding: 0 43px;
|
||||
font-size: 20px;
|
||||
line-height: 26px;
|
||||
font-weight: 700;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
z-index: 12;
|
||||
> :not(:last-child) {
|
||||
margin-right: 15px;
|
||||
}
|
||||
* {
|
||||
&&& {
|
||||
background: var(--white);
|
||||
height: 75px;
|
||||
border-radius: 10px 50px 50px 50px;
|
||||
border: solid 2px var(--g0);
|
||||
|
||||
box-shadow: 0px 8px 0px 0px var(--g0);
|
||||
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
color: var(--g0);
|
||||
padding: 0 70px;
|
||||
font-size: 20px;
|
||||
line-height: 26px;
|
||||
font-weight: 700;
|
||||
}
|
||||
&::after {
|
||||
content: ' ';
|
||||
display: block;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border: solid 3px var(--black);
|
||||
opacity: 0;
|
||||
transition: inset 0.5s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.15s linear;
|
||||
z-index: 10;
|
||||
}
|
||||
&:focus::after,
|
||||
&:hover::after {
|
||||
inset: -7px;
|
||||
opacity: 1;
|
||||
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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
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);
|
||||
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);
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,27 +1,7 @@
|
|||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { media } from './responsive';
|
||||
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;
|
||||
|
|
@ -33,6 +13,7 @@ const InnerContainer = styled.div`
|
|||
margin-right: 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
|
|
@ -42,12 +23,42 @@ const Container = styled.div`
|
|||
padding-bottom: 40px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Title = styled(Text15)`
|
||||
font-weight: 700;
|
||||
color: var(--black);
|
||||
&&& {
|
||||
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(--light-gray);
|
||||
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,70 +1,17 @@
|
|||
import type { MDXRemoteSerializeResult } from 'next-mdx-remote';
|
||||
import { BoxIso } from 'iconoir-react';
|
||||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Code, CopyButton, Text15, Text18 } from './Typography';
|
||||
import moment from 'moment';
|
||||
import { MDXRemoteSerializeResult } from 'next-mdx-remote';
|
||||
import { media } from '../lib/responsive';
|
||||
import { CopyButton } from './Button';
|
||||
import { MDXRemote } from './MDXRemote';
|
||||
import { FILE_PREFIX } from './constants';
|
||||
import { media } from './responsive';
|
||||
import { Code, Text15, Text18 } from './Typography';
|
||||
|
||||
const EXPAND_HEIGHT = 400;
|
||||
|
||||
export interface ChangelogEntryProps {
|
||||
name: string;
|
||||
body: MDXRemoteSerializeResult;
|
||||
created_at: string;
|
||||
}
|
||||
export function ChangelogEntry({
|
||||
name,
|
||||
body,
|
||||
created_at,
|
||||
}: ChangelogEntryProps) {
|
||||
const [expanded, setExpanded] = React.useState(false);
|
||||
const [shouldExpand, setShouldExpand] = React.useState(false);
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
React.useEffect(() => {
|
||||
if (
|
||||
containerRef.current &&
|
||||
containerRef.current.clientHeight > EXPAND_HEIGHT
|
||||
) {
|
||||
setShouldExpand(true);
|
||||
}
|
||||
}, []);
|
||||
return (
|
||||
<Container ref={containerRef}>
|
||||
<ContainerLeft>
|
||||
<ContainerIcon>
|
||||
<BoxIso />
|
||||
</ContainerIcon>
|
||||
<TitleContainer>
|
||||
<a
|
||||
href={`${FILE_PREFIX}/../../releases/tag/${name}`}
|
||||
target={'_blank'}
|
||||
rel={'noreferrer'}
|
||||
style={{ textDecoration: 'none' }}
|
||||
>
|
||||
<EntryTitle>{name}</EntryTitle>
|
||||
</a>
|
||||
<Text15>{moment(created_at).format('MMM DD, YYYY')}</Text15>
|
||||
</TitleContainer>
|
||||
</ContainerLeft>
|
||||
<EntryBody expanded={expanded}>
|
||||
<MDXRemote {...body} />
|
||||
{shouldExpand ? (
|
||||
<ExpandContainer>
|
||||
<CopyButton onClick={() => setExpanded((e) => !e)}>
|
||||
{expanded ? 'Collapse' : 'Expand'}
|
||||
</CopyButton>
|
||||
</ExpandContainer>
|
||||
) : null}
|
||||
</EntryBody>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
margin: 40px 0 !important;
|
||||
margin: 40px 0;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
|
|
@ -72,9 +19,10 @@ const Container = styled.div`
|
|||
${media.lg} {
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
margin: 24px 0 !important;
|
||||
margin: 24px 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const ContainerLeft = styled.div`
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
|
@ -84,46 +32,115 @@ const ContainerLeft = styled.div`
|
|||
margin-right: 30px;
|
||||
}
|
||||
`;
|
||||
|
||||
const ContainerIcon = styled.div`
|
||||
font-size: 18px;
|
||||
color: var(--black);
|
||||
margin-right: 18px;
|
||||
`;
|
||||
|
||||
const TitleContainer = styled.div`
|
||||
width: 100px;
|
||||
`;
|
||||
|
||||
const EntryTitle = styled(Text18)`
|
||||
color: var(--black);
|
||||
font-weight: 700;
|
||||
&&& {
|
||||
color: var(--black);
|
||||
font-weight: 700;
|
||||
}
|
||||
`;
|
||||
|
||||
const ExpandContainer = styled.div`
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
right: 23px;
|
||||
`;
|
||||
const EntryBody = styled(Code)<{ expanded?: boolean }>`
|
||||
flex: 1;
|
||||
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;
|
||||
|
||||
const EntryBody = styled(Code)<{ $expanded?: boolean }>`
|
||||
&&& {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
li {
|
||||
margin-bottom: 8px;
|
||||
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;
|
||||
}
|
||||
}
|
||||
code {
|
||||
display: inline-block;
|
||||
background: var(--gray) !important;
|
||||
color: var(--black);
|
||||
font-family: var(--font-family) !important;
|
||||
padding: 0 4px;
|
||||
font-size: 18px !important;
|
||||
}
|
||||
`;
|
||||
|
||||
export interface ChangelogEntryProps {
|
||||
name: string;
|
||||
url: string;
|
||||
created_at: string;
|
||||
body?: MDXRemoteSerializeResult;
|
||||
}
|
||||
|
||||
export function ChangelogEntry({
|
||||
name,
|
||||
url,
|
||||
body,
|
||||
created_at,
|
||||
}: ChangelogEntryProps) {
|
||||
const [expanded, setExpanded] = React.useState(false);
|
||||
const [shouldExpand, setShouldExpand] = React.useState(false);
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (
|
||||
containerRef.current
|
||||
&& containerRef.current.clientHeight > EXPAND_HEIGHT
|
||||
) {
|
||||
setShouldExpand(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Container ref={containerRef}>
|
||||
<ContainerLeft>
|
||||
<ContainerIcon>
|
||||
<BoxIso />
|
||||
</ContainerIcon>
|
||||
<TitleContainer>
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={{ textDecoration: 'none' }}
|
||||
>
|
||||
<EntryTitle>{name}</EntryTitle>
|
||||
</a>
|
||||
<Text15>{moment(created_at).format('MMM DD, YYYY')}</Text15>
|
||||
</TitleContainer>
|
||||
</ContainerLeft>
|
||||
<EntryBody $expanded={expanded}>
|
||||
{body ? <MDXRemote {...body} /> : 'No changelog'}
|
||||
{shouldExpand
|
||||
? (
|
||||
<ExpandContainer>
|
||||
<CopyButton onClick={() => setExpanded((e) => !e)}>
|
||||
{expanded ? 'Collapse' : 'Expand'}
|
||||
</CopyButton>
|
||||
</ExpandContainer>
|
||||
)
|
||||
: null}
|
||||
</EntryBody>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,29 +1,35 @@
|
|||
import Link from 'next/link';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Text13 } from './Typography';
|
||||
|
||||
export interface CurrentVersionProps {
|
||||
version: string;
|
||||
color?: string;
|
||||
}
|
||||
export function CurrentVersion({ version, color }: CurrentVersionProps) {
|
||||
return (
|
||||
<Link href={'/docs/changelog'} passHref>
|
||||
<Container as={'a'} style={color ? { background: color } : undefined}>
|
||||
{version}
|
||||
</Container>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
const Container = styled(Text13)`
|
||||
color: var(--black);
|
||||
&&& {
|
||||
color: var(--g1);
|
||||
font-weight: 700;
|
||||
background: var(--pink);
|
||||
background: var(--g5);
|
||||
line-height: 1;
|
||||
padding: 7px 16px;
|
||||
border-radius: 200px;
|
||||
display: block;
|
||||
text-decoration: none !important;
|
||||
text-decoration: none;
|
||||
transition:
|
||||
color 0.1s linear,
|
||||
background 0.1s linear;
|
||||
&:hover {
|
||||
background: var(--black);
|
||||
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,16 +1,62 @@
|
|||
import type { IconListCustomizations } from './IconList';
|
||||
import React from 'react';
|
||||
import { DEFAULT_CUSTOMIZATIONS, IconListCustomizations } from './IconList';
|
||||
import styled from 'styled-components';
|
||||
import { Text13, Text18 } from './Typography';
|
||||
import { Button } from './Button';
|
||||
import { ColorInput } from './Input';
|
||||
import { media } from '../lib/responsive';
|
||||
import { DEFAULT_CUSTOMIZATIONS } 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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export interface CustomizationEditorProps {
|
||||
customizations: IconListCustomizations;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
onChange: (customizations: IconListCustomizations) => void;
|
||||
}
|
||||
|
||||
export function CustomizationEditor({
|
||||
customizations,
|
||||
onChange,
|
||||
|
|
@ -18,9 +64,11 @@ 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);
|
||||
|
|
@ -38,65 +86,55 @@ export function CustomizationEditor({
|
|||
|
||||
return (
|
||||
<>
|
||||
<Header>
|
||||
<Text18 style={{ fontWeight: 700, color: 'var(--black)' }}>
|
||||
Customize
|
||||
</Text18>
|
||||
<Button onClick={() => onChange(DEFAULT_CUSTOMIZATIONS)}>Reset</Button>
|
||||
</Header>
|
||||
<Field>
|
||||
<Slider
|
||||
label={'Size'}
|
||||
minValue={12}
|
||||
maxValue={128}
|
||||
value={[size]}
|
||||
formatOptions={{ maximumFractionDigits: 0 }}
|
||||
onChange={(values) => {
|
||||
setSize(values[0]);
|
||||
updateCustomizations({ size: values[0] });
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<Slider
|
||||
label={'Stroke Width'}
|
||||
minValue={0.5}
|
||||
maxValue={3}
|
||||
value={[strokeWidth]}
|
||||
step={0.01}
|
||||
formatOptions={{ maximumFractionDigits: 1 }}
|
||||
onChange={(values) => {
|
||||
setStrokeWidth(values[0]);
|
||||
updateCustomizations({ strokeWidth: values[0] });
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
<HorizontalField>
|
||||
<Text13>Color</Text13>
|
||||
<ColorInput
|
||||
type={'color'}
|
||||
value={color}
|
||||
onChange={(e) => {
|
||||
setColor(e.target.value);
|
||||
updateCustomizations({ hexColor: e.target.value });
|
||||
}}
|
||||
/>
|
||||
</HorizontalField>
|
||||
<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 Header = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 45px;
|
||||
`;
|
||||
const Field = styled.div`
|
||||
margin-bottom: 35px;
|
||||
`;
|
||||
const HorizontalField = styled(Field)`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,95 +1,15 @@
|
|||
import React from 'react';
|
||||
import { DocumentationItem } from '../pages/docs/[...slug]';
|
||||
import styled from 'styled-components';
|
||||
import type { DocumentationItem } from '../pages/docs/[...slug]';
|
||||
import { NavArrowUp } from 'iconoir-react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { media } from './responsive';
|
||||
import { NavArrowUp } from 'iconoir-react';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { media } from '../lib/responsive';
|
||||
|
||||
export interface DocumentationNavigationProps {
|
||||
documentationItems: DocumentationItem[];
|
||||
pathPrefix?: string[];
|
||||
}
|
||||
export function DocumentationNavigation({
|
||||
documentationItems,
|
||||
pathPrefix,
|
||||
}: DocumentationNavigationProps) {
|
||||
const router = useRouter();
|
||||
const activePath = router.asPath.replace('/docs/', '');
|
||||
const [expandedTitles, setExpandedTitles] = React.useState<string[]>([]);
|
||||
React.useEffect(() => {
|
||||
const expandedItems = documentationItems.filter((item) => {
|
||||
const normalized = activePath.replace((pathPrefix || []).join('/'), '');
|
||||
return (
|
||||
normalized === item.path ||
|
||||
item.children?.some((child) => {
|
||||
return activePath.startsWith(
|
||||
[item.path, child.path].filter(Boolean).join('/')
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
setExpandedTitles(expandedItems.map((item) => item.title));
|
||||
}, [activePath, pathPrefix, documentationItems]);
|
||||
return (
|
||||
<>
|
||||
{documentationItems.map((documentationItem) => {
|
||||
const path = [...(pathPrefix || []), documentationItem.path]
|
||||
.filter(Boolean)
|
||||
.join('/');
|
||||
if (documentationItem.children?.length) {
|
||||
const active = expandedTitles.includes(documentationItem.title);
|
||||
return (
|
||||
<React.Fragment key={documentationItem.title}>
|
||||
<HeaderItem
|
||||
onClick={() => {
|
||||
setExpandedTitles((et) => {
|
||||
const includes = et.includes(documentationItem.title);
|
||||
return includes
|
||||
? et.filter((i) => i !== documentationItem.title)
|
||||
: [...et, documentationItem.title];
|
||||
});
|
||||
}}
|
||||
>
|
||||
<HeaderItemIcon active={active}>
|
||||
<NavArrowUp />
|
||||
</HeaderItemIcon>
|
||||
{documentationItem.title}
|
||||
</HeaderItem>
|
||||
<ChildrenContainer expanded={active}>
|
||||
<DocumentationNavigation
|
||||
documentationItems={documentationItem.children}
|
||||
pathPrefix={[
|
||||
...(pathPrefix || []),
|
||||
documentationItem.path,
|
||||
].filter(Boolean)}
|
||||
/>
|
||||
</ChildrenContainer>
|
||||
</React.Fragment>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Link href={`/docs/${path}`} passHref key={documentationItem.path}>
|
||||
<NavigationItem as={'a'} active={activePath === path}>
|
||||
<span>{documentationItem.title}</span>
|
||||
{documentationItem.label ? (
|
||||
<NavigationItemLabel>
|
||||
{documentationItem.label}
|
||||
</NavigationItemLabel>
|
||||
) : null}
|
||||
</NavigationItem>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const HeaderItemIcon = styled.div<{ active?: boolean }>`
|
||||
const HeaderItemIcon = styled.div<{ $active?: boolean }>`
|
||||
font-size: 13px;
|
||||
transition: transform 0.25s linear;
|
||||
transform: rotate(${(props) => (props.active ? 180 : 0)}deg);
|
||||
transform: rotate(${(props) => (props.$active ? 180 : 0)}deg);
|
||||
margin-right: 7px;
|
||||
position: relative;
|
||||
top: 6px;
|
||||
|
|
@ -100,19 +20,19 @@ const HeaderItemIcon = styled.div<{ active?: boolean }>`
|
|||
display: none;
|
||||
}
|
||||
`;
|
||||
const ChildrenContainer = styled.div<{ expanded?: boolean }>`
|
||||
display: ${(props) => (props.expanded ? 'block' : 'none')};
|
||||
|
||||
const ChildrenContainer = styled.div<{ $expanded?: boolean }>`
|
||||
display: ${(props) => (props.$expanded ? 'block' : 'none')};
|
||||
${media.lg} {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
||||
const HeaderItem = styled.div`
|
||||
padding: 10px 30px;
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
font-size: 15px;
|
||||
line-height: 19px;
|
||||
color: var(--black);
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--g0);
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
|
@ -126,45 +46,144 @@ const HeaderItem = styled.div`
|
|||
}
|
||||
}
|
||||
`;
|
||||
const NavigationItem = styled.div<{ active?: boolean }>`
|
||||
padding: 12px 45px 12px 75px;
|
||||
transition: background 0.1s linear, color 0.1s linear;
|
||||
font-weight: 500;
|
||||
|
||||
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(--black-60);
|
||||
color: var(--g1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
span {
|
||||
font-weight: 500;
|
||||
font-weight: 400;
|
||||
}
|
||||
> :not(:last-child) {
|
||||
margin-right: 14px;
|
||||
}
|
||||
&:hover,
|
||||
${(props) => (props.active ? '&' : '&.noop')} {
|
||||
background: var(--light-gray);
|
||||
color: var(--black);
|
||||
${(props) => (props.$active ? '&' : '&.noop')} {
|
||||
color: var(--g0);
|
||||
text-decoration: underline;
|
||||
}
|
||||
${(props) => (props.active ? 'span' : '&.noop')} {
|
||||
font-weight: 700;
|
||||
${(props) => (props.$active ? 'span' : '&.noop')} {
|
||||
font-weight: 500;
|
||||
}
|
||||
${media.lg} {
|
||||
padding: 12px 45px 12px 65px;
|
||||
padding: 12px 12px 12px 65px;
|
||||
}
|
||||
`;
|
||||
|
||||
const NavigationItemLabel = styled.span`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 4px;
|
||||
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(--white);
|
||||
background: var(--black);
|
||||
color: var(--g1);
|
||||
background: var(--g5);
|
||||
`;
|
||||
|
||||
export interface DocumentationNavigationProps {
|
||||
documentationItems: DocumentationItem[];
|
||||
pathPrefix?: string[];
|
||||
}
|
||||
|
||||
export function DocumentationNavigation({
|
||||
documentationItems,
|
||||
pathPrefix,
|
||||
}: DocumentationNavigationProps) {
|
||||
const router = useRouter();
|
||||
const activePath = router.asPath.replace('/docs/', '');
|
||||
const [expandedTitles, setExpandedTitles] = React.useState<string[]>([]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const expandedItems = documentationItems.filter((item) => {
|
||||
const normalized = activePath.replace((pathPrefix || []).join('/'), '');
|
||||
|
||||
return (
|
||||
normalized === item.path
|
||||
|| item.children?.some((child) => {
|
||||
return activePath.startsWith(
|
||||
[item.path, child.path].filter(Boolean).join('/'),
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
setExpandedTitles(expandedItems.map((item) => item.title));
|
||||
}, [activePath, pathPrefix, documentationItems]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{documentationItems.map((documentationItem) => {
|
||||
const path = [...(pathPrefix || []), documentationItem.path]
|
||||
.filter(Boolean)
|
||||
.join('/');
|
||||
|
||||
if (documentationItem.children?.length) {
|
||||
const active = expandedTitles.includes(documentationItem.title);
|
||||
|
||||
return (
|
||||
<React.Fragment key={documentationItem.title}>
|
||||
<HeaderItem
|
||||
onClick={() => {
|
||||
setExpandedTitles((et) => {
|
||||
const includes = et.includes(documentationItem.title);
|
||||
|
||||
return includes
|
||||
? et.filter((i) => i !== documentationItem.title)
|
||||
: [...et, documentationItem.title];
|
||||
});
|
||||
}}
|
||||
>
|
||||
<HeaderItemIcon $active={active}>
|
||||
<NavArrowUp />
|
||||
</HeaderItemIcon>
|
||||
{documentationItem.title}
|
||||
</HeaderItem>
|
||||
<ChildrenContainer $expanded={active}>
|
||||
<DocumentationNavigation
|
||||
documentationItems={documentationItem.children}
|
||||
pathPrefix={[
|
||||
...(pathPrefix || []),
|
||||
documentationItem.path,
|
||||
].filter(Boolean)}
|
||||
/>
|
||||
</ChildrenContainer>
|
||||
</React.Fragment>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Link
|
||||
href={`/docs/${path}`}
|
||||
passHref
|
||||
legacyBehavior
|
||||
key={documentationItem.path}
|
||||
>
|
||||
<NavigationItem as="a" $active={activePath === path}>
|
||||
<span>{documentationItem.title}</span>
|
||||
{documentationItem.label
|
||||
? (
|
||||
<NavigationItemLabel>
|
||||
{documentationItem.label}
|
||||
</NavigationItemLabel>
|
||||
)
|
||||
: null}
|
||||
</NavigationItem>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
96
iconoir.com/components/DonationPopup.tsx
Normal file
96
iconoir.com/components/DonationPopup.tsx
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
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,23 +1,72 @@
|
|||
import type { Icon, IconListFilters } from './IconList';
|
||||
import { IconoirProvider } from 'iconoir-react';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Ad } from './Ad';
|
||||
import { media } from '../lib/responsive';
|
||||
import { CustomizationEditor } from './CustomizationEditor';
|
||||
import { FiltersEditor } from './FiltersEditor';
|
||||
import { Icon, IconList, IconListFilters } from './IconList';
|
||||
import { media } from './responsive';
|
||||
import { IconList } from './IconList';
|
||||
import { Streamline } from './Streamline';
|
||||
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
|
||||
|
|
@ -37,45 +86,12 @@ export function Explore({ allIcons }: ExploreProps) {
|
|||
<FilterContainer>
|
||||
<FiltersEditor filters={filters} onChange={setFilters} />
|
||||
</FilterContainer>
|
||||
<Streamline />
|
||||
<CustomizationEditor
|
||||
customizations={customizations}
|
||||
onChange={setCustomizations}
|
||||
/>
|
||||
<Ad />
|
||||
</Right>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-direction: row;
|
||||
`;
|
||||
const Left = styled.div`
|
||||
flex: 1;
|
||||
min-height: calc(100vh - 100px);
|
||||
`;
|
||||
const Right = styled.div`
|
||||
position: sticky;
|
||||
top: 50px;
|
||||
width: 275px;
|
||||
margin-left: 68px;
|
||||
display: none;
|
||||
${media.md} {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
const FilterContainer = styled.div<{ isMobile?: boolean }>`
|
||||
display: ${(props) => (props.isMobile ? 'block' : 'none')};
|
||||
margin-bottom: 40px;
|
||||
position: sticky;
|
||||
top: 20px;
|
||||
z-index: 100;
|
||||
${media.md} {
|
||||
position: relative;
|
||||
top: 0;
|
||||
display: ${(props) => (props.isMobile ? 'none' : 'block')};
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,33 @@
|
|||
import type { IconListFilters } from './IconList';
|
||||
import React from 'react';
|
||||
import { IconListFilters } from './IconList';
|
||||
import { LargeInput } from './Input';
|
||||
|
||||
export interface FiltersEditorProps {
|
||||
filters: IconListFilters;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
onChange: (filters: IconListFilters) => void;
|
||||
}
|
||||
|
||||
export function FiltersEditor({ filters, onChange }: FiltersEditorProps) {
|
||||
const [, startTransition] = (React as any).useTransition();
|
||||
const [search, setSearch] = React.useState(filters.search);
|
||||
|
||||
// Keep track if the user hits tab before scrolling, so we can scroll the search
|
||||
// field to the top of the page automatically.
|
||||
const didScrollRef = React.useRef(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
const scrollEvent = () => {
|
||||
didScrollRef.current = true;
|
||||
window.removeEventListener('scroll', scrollEvent);
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', scrollEvent);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('scroll', scrollEvent);
|
||||
};
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
setSearch(filters.search);
|
||||
}, [filters]);
|
||||
|
|
@ -25,10 +43,19 @@ 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) {
|
||||
e.target.scrollIntoView({
|
||||
block: 'start',
|
||||
behavior: 'auto',
|
||||
});
|
||||
}
|
||||
}}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setSearch(value);
|
||||
|
|
|
|||
|
|
@ -1,42 +1,165 @@
|
|||
import { PeaceHand } from 'iconoir-react';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { LICENSE_LINK } from './constants';
|
||||
import {
|
||||
GITHUB_LINK,
|
||||
ISSUE_LINK,
|
||||
LICENSE_LINK,
|
||||
PRIVACY_LINK,
|
||||
SUPPORT_LINK,
|
||||
} from '../lib/constants';
|
||||
import { Logo, LogoContainer, LogoIcon } from './Header';
|
||||
import { NavigationItemContainer } from './NavigationItem';
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<Container>
|
||||
<LogoContainer>
|
||||
<LogoIcon>
|
||||
<PeaceHand />
|
||||
</LogoIcon>
|
||||
<Logo src={'/iconoir-logo.svg'} alt={'Iconoir Logo'} />
|
||||
</LogoContainer>
|
||||
<FooterNavigationItem
|
||||
as={'a'}
|
||||
href={LICENSE_LINK}
|
||||
target={'_blank'}
|
||||
rel={'noreferrer'}
|
||||
>
|
||||
License
|
||||
</FooterNavigationItem>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
import { Text13, Text17 } from './Typography';
|
||||
|
||||
const Container = styled.div`
|
||||
margin-top: 100px;
|
||||
display: block;
|
||||
margin-top: 110px;
|
||||
padding-top: 30px;
|
||||
border-top: solid 2px var(--light-gray);
|
||||
display: flex;
|
||||
margin-top: 100px;
|
||||
padding: 84px 12%;
|
||||
background-color: var(--g7);
|
||||
align-items: center;
|
||||
> :not(:last-child) {
|
||||
margin-right: 50px;
|
||||
}
|
||||
`;
|
||||
|
||||
const FooterNavigationItem = styled(NavigationItemContainer)`
|
||||
color: var(--black);
|
||||
const FooterEnd = styled.div`
|
||||
border-top: 1px solid var(--g5);
|
||||
padding-top: 20px;
|
||||
margin-top: 74px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
const FooterCategories = styled.div`
|
||||
width: 100%;
|
||||
margin-top: 54px;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const FooterCategoryContainer = styled.div`
|
||||
width: 28%;
|
||||
margin-right: 20px;
|
||||
`;
|
||||
|
||||
const FooterCategoryTitle = styled(Text17)`
|
||||
&&& {
|
||||
margin-bottom: 24px;
|
||||
font-weight: 600;
|
||||
display: block;
|
||||
color: var(--g0);
|
||||
}
|
||||
`;
|
||||
|
||||
const FooterCategoryLinks = styled.div``;
|
||||
|
||||
const FooterCategoryLink = styled.a`
|
||||
display: block;
|
||||
font-size: 17px;
|
||||
color: var(--g1);
|
||||
width: fit-content;
|
||||
margin-bottom: 12px;
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
color: var(--g0);
|
||||
}
|
||||
`;
|
||||
|
||||
interface FooterCategoryProps {
|
||||
category: string;
|
||||
links: { name: string; url: string }[];
|
||||
}
|
||||
|
||||
function FooterCategory({ category, links }: FooterCategoryProps) {
|
||||
return (
|
||||
<FooterCategoryContainer>
|
||||
<FooterCategoryTitle>{category}</FooterCategoryTitle>
|
||||
<FooterCategoryLinks>
|
||||
{links.map((link) => (
|
||||
<FooterCategoryLink key={link.url} href={link.url}>
|
||||
{link.name}
|
||||
</FooterCategoryLink>
|
||||
))}
|
||||
</FooterCategoryLinks>
|
||||
</FooterCategoryContainer>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import React from 'react';
|
||||
import Script from 'next/script';
|
||||
|
||||
export function GA() {
|
||||
|
|
@ -6,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,95 +1,80 @@
|
|||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { AUTHOR_LINKS } from './constants';
|
||||
import { Cancel, Heart, Menu } from 'iconoir-react';
|
||||
import { CurrentVersion } from './CurrentVersion';
|
||||
import { media } from './responsive';
|
||||
import { ResetButton } from './Button';
|
||||
import { AnimatedSvg } from './AnimatedSvg';
|
||||
import { NavigationItem, NavigationItemContainer } from './NavigationItem';
|
||||
import { Discord, Menu, Sparks, Xmark } from 'iconoir-react';
|
||||
import { Heart } from 'iconoir-react/solid';
|
||||
import Link from 'next/link';
|
||||
import React from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
import { DISCORD_LINK, SHARE_LINK } from '../lib/constants';
|
||||
import { media } from '../lib/responsive';
|
||||
import { AnimatedSvg } from './AnimatedSvg';
|
||||
import { ResetButton } from './Button';
|
||||
import { CurrentVersion } from './CurrentVersion';
|
||||
import { NavigationItem } from './NavigationItem';
|
||||
import { Text15 } from './Typography';
|
||||
|
||||
export interface HeaderProps {
|
||||
currentVersion: string;
|
||||
currentVersionColor?: string;
|
||||
}
|
||||
export function Header({ currentVersion, currentVersionColor }: HeaderProps) {
|
||||
const [menuVisible, setMenuVisible] = React.useState(false);
|
||||
return (
|
||||
<Container>
|
||||
<HeaderLeft>
|
||||
<Link href={'/'}>
|
||||
<a>
|
||||
<LogoContainer>
|
||||
<LogoIcon>
|
||||
<AnimatedSvg />
|
||||
</LogoIcon>
|
||||
<Logo src={'/iconoir-logo.svg'} alt={'Iconoir Logo'} />
|
||||
</LogoContainer>
|
||||
</a>
|
||||
</Link>
|
||||
<CurrentVersion version={currentVersion} color={currentVersionColor} />
|
||||
</HeaderLeft>
|
||||
<HeaderCenter>
|
||||
<MobileMenuContainer visible={menuVisible}>
|
||||
<NavigationItem href={'/'}>Icons</NavigationItem>
|
||||
<NavigationItem href={'/docs'}>Documentation</NavigationItem>
|
||||
<NavigationItem href={'/support'} style={{ marginRight: 0 }}>
|
||||
Donate — Our Mission
|
||||
</NavigationItem>
|
||||
<BuiltWith isMobile>
|
||||
Made with <Heart width={'1em'} height={'1em'} /> by{' '}
|
||||
<a href={AUTHOR_LINKS.Luca} target={'_blank'} rel={'noreferrer'}>
|
||||
Luca
|
||||
</a>{' '}
|
||||
&{' '}
|
||||
<a href={AUTHOR_LINKS.Sam} target={'_blank'} rel={'noreferrer'}>
|
||||
Sam
|
||||
</a>
|
||||
</BuiltWith>
|
||||
</MobileMenuContainer>
|
||||
</HeaderCenter>
|
||||
<HeaderRight>
|
||||
<BuiltWith>
|
||||
Designed and built with <Heart width={'1em'} height={'1em'} /> by{' '}
|
||||
<a href={AUTHOR_LINKS.Luca} target={'_blank'} rel={'noreferrer'}>
|
||||
Luca
|
||||
</a>{' '}
|
||||
&{' '}
|
||||
<a href={AUTHOR_LINKS.Sam} target={'_blank'} rel={'noreferrer'}>
|
||||
Sam
|
||||
</a>
|
||||
</BuiltWith>
|
||||
<MobileMenuButton onClick={() => setMenuVisible((v) => !v)}>
|
||||
{menuVisible ? <Cancel /> : <Menu />}
|
||||
</MobileMenuButton>
|
||||
</HeaderRight>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
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 const LogoContainer = styled.div`
|
||||
position: relative;
|
||||
z-index: 101;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
`;
|
||||
const MobileMenuButton = styled(ResetButton)`
|
||||
z-index: 101;
|
||||
color: var(--black);
|
||||
background: transparent;
|
||||
display: inline-block;
|
||||
margin-left: auto !important;
|
||||
cursor: pointer;
|
||||
svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
transition: 0.1s;
|
||||
&:hover {
|
||||
scale: 1.1;
|
||||
transition: 0.2s;
|
||||
}
|
||||
`;
|
||||
|
||||
const Banner = styled(Text15)`
|
||||
display: none;
|
||||
${media.lg} {
|
||||
display: none;
|
||||
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 MobileMenuContainer = styled.div<{ visible?: boolean }>`
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const MobileMenuContainer = styled.div<{ $visible?: boolean }>`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
|
@ -97,7 +82,9 @@ 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;
|
||||
|
|
@ -105,13 +92,14 @@ const MobileMenuContainer = styled.div<{ visible?: boolean }>`
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
${(props) => (props.visible ? '&' : '&.noop')} {
|
||||
pointer-events: all;
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
${(props) =>
|
||||
props.$visible
|
||||
&& css`
|
||||
pointer-events: all;
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
`}
|
||||
${media.lg} {
|
||||
margin-left: auto;
|
||||
background: none;
|
||||
padding-top: 0;
|
||||
box-shadow: none;
|
||||
|
|
@ -127,11 +115,16 @@ 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%;
|
||||
|
|
@ -140,59 +133,144 @@ 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 !important;
|
||||
margin-right: 16px;
|
||||
z-index: 101;
|
||||
`;
|
||||
export const LogoIcon = styled.div`
|
||||
color: var(--black);
|
||||
margin-right: 4px !important;
|
||||
margin-right: 4px;
|
||||
svg {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
`;
|
||||
const BuiltWith = styled(NavigationItemContainer)<{ isMobile?: boolean }>`
|
||||
display: ${(props) => (props.isMobile ? 'flex' : 'none')};
|
||||
${media.lg} {
|
||||
display: ${(props) => (props.isMobile ? 'none' : 'flex')};
|
||||
}
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--black-60);
|
||||
border-bottom: none !important;
|
||||
svg {
|
||||
fill: var(--black);
|
||||
margin: 0 0.22em;
|
||||
}
|
||||
> * {
|
||||
margin: 0 0.22em;
|
||||
}
|
||||
a {
|
||||
color: var(--black);
|
||||
font-weight: 700;
|
||||
}
|
||||
> :last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
${media.lg} {
|
||||
justify-content: flex-start;
|
||||
|
||||
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;
|
||||
a {
|
||||
font-weight: normal;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: unset;
|
||||
text-decoration: unset;
|
||||
}
|
||||
svg,
|
||||
span {
|
||||
color: var(--black);
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
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,46 +1,136 @@
|
|||
import React from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { media } from './responsive';
|
||||
import { media } from '../lib/responsive';
|
||||
|
||||
const HeaderContainer = styled.div`
|
||||
position: relative;
|
||||
width: fit-content;
|
||||
margin: auto;
|
||||
`;
|
||||
|
||||
const FloatingIcon = styled.div`
|
||||
position: absolute;
|
||||
display: none;
|
||||
background-repeat: no-repeat;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
${media.md} {
|
||||
display: flex;
|
||||
}
|
||||
`;
|
||||
|
||||
const FloatingIconCellar = styled(FloatingIcon)`
|
||||
-webkit-transform: rotate(6deg);
|
||||
-moz-transform: rotate(6deg);
|
||||
top: -120px;
|
||||
right: 0px;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
background-image: url(/cellar.gif);
|
||||
background-size: 70%;
|
||||
${media.lg} {
|
||||
}
|
||||
`;
|
||||
|
||||
const FloatingIconPay = styled(FloatingIcon)`
|
||||
-webkit-transform: rotate(18deg);
|
||||
-moz-transform: rotate(18deg);
|
||||
top: -50px;
|
||||
right: -100px;
|
||||
width: 130px;
|
||||
height: 130px;
|
||||
background-image: url(/pay-bitcoin.gif);
|
||||
background-size: 70%;
|
||||
${media.lg} {
|
||||
}
|
||||
`;
|
||||
|
||||
const FloatingFaceID = styled(FloatingIcon)`
|
||||
-webkit-transform: rotate(6deg);
|
||||
-moz-transform: rotate(6deg);
|
||||
top: -130px;
|
||||
right: 380px;
|
||||
width: 110px;
|
||||
height: 110px;
|
||||
background-image: url(/face-id.gif);
|
||||
background-size: 70%;
|
||||
${media.lg} {
|
||||
}
|
||||
`;
|
||||
|
||||
const FloatingCommand = styled(FloatingIcon)`
|
||||
-webkit-transform: rotate(-7deg);
|
||||
-moz-transform: rotate(-7deg);
|
||||
top: -94px;
|
||||
left: 150px;
|
||||
width: 110px;
|
||||
height: 110px;
|
||||
background-image: url(/command.gif);
|
||||
background-size: 70%;
|
||||
${media.lg} {
|
||||
}
|
||||
`;
|
||||
|
||||
const FloatingFill = styled(FloatingIcon)`
|
||||
-webkit-transform: rotate(-14deg);
|
||||
-moz-transform: rotate(-14deg);
|
||||
top: -64px;
|
||||
left: -75px;
|
||||
width: 110px;
|
||||
height: 110px;
|
||||
background-image: url(/fill.gif);
|
||||
background-size: 70%;
|
||||
${media.lg} {
|
||||
}
|
||||
`;
|
||||
|
||||
export interface HeaderBackgroundProps {
|
||||
children: React.ReactElement;
|
||||
src: string;
|
||||
}
|
||||
export function HeaderBackground({ children, src }: HeaderBackgroundProps) {
|
||||
|
||||
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>
|
||||
<ImageContainer>
|
||||
<BackgroundImage src={src} />
|
||||
</ImageContainer>
|
||||
<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;
|
||||
`;
|
||||
const ImageContainer = styled.div`
|
||||
position: absolute;
|
||||
top: -100px;
|
||||
bottom: -100px;
|
||||
left: -30px;
|
||||
right: -30px;
|
||||
${media.lg} {
|
||||
left: -100px;
|
||||
right: -100px;
|
||||
}
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
const BackgroundImage = styled.img`
|
||||
width: 90%;
|
||||
max-width: calc(min(1100px, 90vw));
|
||||
max-height: 60%;
|
||||
${media.md} {
|
||||
max-height: 100%;
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
16
iconoir.com/components/HeaderSecondary.tsx
Normal file
16
iconoir.com/components/HeaderSecondary.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const HeaderContainer = styled.div`
|
||||
position: relative;
|
||||
width: fit-content;
|
||||
margin: auto;
|
||||
`;
|
||||
|
||||
export interface HeaderSecondaryProps {
|
||||
children: React.ReactElement;
|
||||
}
|
||||
|
||||
export function HeaderSecondary({ children }: HeaderSecondaryProps) {
|
||||
return <HeaderContainer>{children}</HeaderContainer>;
|
||||
}
|
||||
63
iconoir.com/components/Hono.tsx
Normal file
63
iconoir.com/components/Hono.tsx
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
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,31 +1,152 @@
|
|||
import React from 'react';
|
||||
import { DEFAULT_CUSTOMIZATIONS, Icon as IconType } from './IconList';
|
||||
import styled from 'styled-components';
|
||||
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 { showNotification } from '../helpers/showNotification';
|
||||
import { DEFAULT_CUSTOMIZATIONS } 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)
|
||||
);
|
||||
}
|
||||
|
||||
const Overlay = styled.div`
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
border: solid 2px var(--g0);
|
||||
background: var(--white);
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
`;
|
||||
|
||||
const CornerBR = styled(Overlay)`
|
||||
bottom: -6px;
|
||||
right: -6px;
|
||||
z-index: 999;
|
||||
`;
|
||||
|
||||
const CornerTR = styled(Overlay)`
|
||||
top: -6px;
|
||||
right: -6px;
|
||||
`;
|
||||
|
||||
const CornerBL = styled(Overlay)`
|
||||
bottom: -6px;
|
||||
left: -6px;
|
||||
`;
|
||||
|
||||
const CornerTL = styled(Overlay)`
|
||||
top: -6px;
|
||||
left: -6px;
|
||||
`;
|
||||
|
||||
const HoverContainer = styled.div`
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: stretch;
|
||||
flex-direction: column;
|
||||
|
||||
transform: translateZ(0px); // Safari Fix
|
||||
transition: opacity 0.1s linear;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
`;
|
||||
|
||||
const HoverButton = styled(ResetButton)`
|
||||
&&& {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--white);
|
||||
border-radius: 0px;
|
||||
transition: background 0.1s linear;
|
||||
color: var(--g0);
|
||||
font-size: 14px;
|
||||
line-height: 23px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
&:hover,
|
||||
&:active {
|
||||
background: var(--g0);
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const BorderContainer = styled.div<{ $iconWidth: number }>`
|
||||
width: ${(props) => props.$iconWidth}px;
|
||||
box-sizing: border-box;
|
||||
padding-bottom: 100%;
|
||||
position: relative;
|
||||
border: solid 1px var(--g6);
|
||||
border-radius: 12px;
|
||||
margin-bottom: 10px;
|
||||
@media (hover: hover) {
|
||||
&:hover ${HoverContainer} {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
border: solid 2px var(--g0);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const IconContainer = styled.div`
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const IconTag = styled.div`
|
||||
background-color: var(--g6);
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
border-radius: 5px 10px;
|
||||
padding: 3px 6px;
|
||||
letter-spacing: 0.3px;
|
||||
font-weight: 600;
|
||||
font-size: 11px;
|
||||
color: var(--g0);
|
||||
`;
|
||||
|
||||
const Subtitle = styled.div<{ $iconWidth: number }>`
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 14.74px;
|
||||
color: var(--black-60);
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: ${(props) => props.$iconWidth}px;
|
||||
`;
|
||||
|
||||
export interface IconProps {
|
||||
iconWidth: number;
|
||||
icon: IconType;
|
||||
}
|
||||
|
||||
export function Icon({ iconWidth, icon }: IconProps) {
|
||||
const IconComponent = (AllIcons as any)[icon.iconComponentName];
|
||||
const iconContainerRef = React.useRef<HTMLDivElement>(null);
|
||||
|
|
@ -33,34 +154,37 @@ export function Icon({ iconWidth, icon }: IconProps) {
|
|||
const htmlContentsRef = React.useRef<string>('');
|
||||
const iconContext = React.useContext(AllIcons.IconoirContext);
|
||||
const [supportsClipboard, setSupportsClipboard] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
setSupportsClipboard(
|
||||
typeof window !== 'undefined' &&
|
||||
typeof window?.navigator?.clipboard?.writeText !== 'undefined'
|
||||
typeof window !== 'undefined'
|
||||
&& typeof window?.navigator?.clipboard?.writeText !== 'undefined',
|
||||
);
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (iconContainerRef.current) {
|
||||
htmlContentsRef.current = bakeSvg(
|
||||
iconContainerRef.current.innerHTML,
|
||||
(iconContainerRef.current.firstChild as SVGElement).outerHTML,
|
||||
iconContext.color || DEFAULT_CUSTOMIZATIONS.hexColor,
|
||||
iconContext.strokeWidth || DEFAULT_CUSTOMIZATIONS.strokeWidth
|
||||
iconContext.strokeWidth || DEFAULT_CUSTOMIZATIONS.strokeWidth,
|
||||
);
|
||||
}
|
||||
}, [iconContext, supportsClipboard]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const element =
|
||||
downloadRef.current ||
|
||||
(iconContainerRef.current as unknown as HTMLAnchorElement);
|
||||
const element = downloadRef.current || (iconContainerRef.current as unknown as HTMLAnchorElement);
|
||||
|
||||
if (element) {
|
||||
element.href = `data:image/svg+xml;base64,${btoa(
|
||||
htmlContentsRef.current
|
||||
htmlContentsRef.current,
|
||||
)}`;
|
||||
}
|
||||
}, [iconContext, supportsClipboard]);
|
||||
|
||||
return (
|
||||
<div className={'icon-container'}>
|
||||
<BorderContainer iconWidth={iconWidth}>
|
||||
<div className="icon-container">
|
||||
<BorderContainer $iconWidth={iconWidth}>
|
||||
<IconContainer
|
||||
ref={iconContainerRef}
|
||||
{...((supportsClipboard
|
||||
|
|
@ -73,103 +197,48 @@ export function Icon({ iconWidth, icon }: IconProps) {
|
|||
}) as any)}
|
||||
>
|
||||
<IconComponent />
|
||||
|
||||
{icon.filename.includes('-solid') ? <IconTag>SOLID</IconTag> : ''}
|
||||
</IconContainer>
|
||||
{supportsClipboard ? (
|
||||
<HoverContainer>
|
||||
<HoverButton
|
||||
onClick={() => {
|
||||
if (htmlContentsRef.current) {
|
||||
navigator.clipboard
|
||||
.writeText(htmlContentsRef.current)
|
||||
.then(() => {
|
||||
showNotification('SVG code copied!');
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Copy SVG
|
||||
</HoverButton>
|
||||
<HoverButton
|
||||
as={'a'}
|
||||
ref={downloadRef}
|
||||
href={'#'}
|
||||
rel={'noreferrer'}
|
||||
download={`${icon.filename}.svg`}
|
||||
>
|
||||
Download
|
||||
</HoverButton>
|
||||
</HoverContainer>
|
||||
) : null}
|
||||
{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>{icon.filename}</Subtitle>
|
||||
<Subtitle $iconWidth={iconWidth} title={icon.filename}>
|
||||
{icon.filename}
|
||||
</Subtitle>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const HoverContainer = styled.div<{ supportsCopy?: boolean }>`
|
||||
position: absolute;
|
||||
display: ${(props) => (props.supportsCopy ? 'block' : 'none')};
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: stretch;
|
||||
flex-direction: column;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
transform: translateZ(0px); // Safari Fix
|
||||
transition: opacity 0.1s linear;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
`;
|
||||
const HoverButton = styled(ResetButton)`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--light-gray);
|
||||
border-radius: 0 !important;
|
||||
transition: background 0.1s linear;
|
||||
color: var(--black);
|
||||
font-size: 14px;
|
||||
line-height: 23px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
&:hover,
|
||||
&:active {
|
||||
background: var(--gray);
|
||||
}
|
||||
`;
|
||||
const BorderContainer = styled.div<{ iconWidth: number }>`
|
||||
width: ${(props) => props.iconWidth}px;
|
||||
box-sizing: border-box;
|
||||
padding-bottom: 100%;
|
||||
position: relative;
|
||||
border: solid 1px var(--light-gray);
|
||||
border-radius: 12px;
|
||||
margin-bottom: 10px;
|
||||
@media (hover: hover) {
|
||||
&:hover ${HoverContainer} {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
`;
|
||||
const IconContainer = styled.div`
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
const Subtitle = styled.div`
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 14.74px;
|
||||
color: var(--black-40);
|
||||
text-align: center;
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,17 +1,19 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
VariableSizeList as List,
|
||||
import type {
|
||||
ListChildComponentProps,
|
||||
areEqual,
|
||||
} from 'react-window';
|
||||
import { chunk } from 'lodash';
|
||||
import React from 'react';
|
||||
import {
|
||||
areEqual,
|
||||
VariableSizeList as List,
|
||||
} from 'react-window';
|
||||
import styled from 'styled-components';
|
||||
import useResizeObserver from 'use-resize-observer';
|
||||
import { ICON_SPACE, ICON_WIDTH } from './constants';
|
||||
import { ICON_SPACE, ICON_WIDTH } from '../lib/constants';
|
||||
import { CategoryRow } from './CategoryRow';
|
||||
import { IconListEmpty } from './IconListEmpty';
|
||||
import { IconsRow } from './IconsRow';
|
||||
import { ReactWindowScroller } from './ReactWindowScroller';
|
||||
import styled from 'styled-components';
|
||||
import { IconListEmpty } from './IconListEmpty';
|
||||
|
||||
export interface IconListFilters {
|
||||
search?: string;
|
||||
|
|
@ -40,15 +42,23 @@ function normalizeString(s: string) {
|
|||
|
||||
function filterIcons(allIcons: Icon[], filters: IconListFilters): Icon[] {
|
||||
if (filters.search) {
|
||||
return allIcons.filter((icon) => {
|
||||
const normalSearch = normalizeString(filters.search!);
|
||||
return (
|
||||
normalizeString(icon.filename).includes(normalSearch) ||
|
||||
normalizeString(icon.category).includes(normalSearch) ||
|
||||
icon.tags.some((tag) => normalizeString(tag).includes(normalSearch))
|
||||
);
|
||||
});
|
||||
} else return allIcons;
|
||||
const normalSearch = normalizeString(filters.search!);
|
||||
let result = allIcons;
|
||||
|
||||
for (const term of normalSearch.split(' ')) {
|
||||
result = result.filter((icon) => {
|
||||
return (
|
||||
normalizeString(icon.filename).includes(term)
|
||||
|| normalizeString(icon.category).includes(term)
|
||||
|| icon.tags.some((tag) => normalizeString(tag).includes(term))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
} else {
|
||||
return allIcons;
|
||||
}
|
||||
}
|
||||
|
||||
interface IconCategoryRow {
|
||||
|
|
@ -59,28 +69,34 @@ 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 });
|
||||
}
|
||||
|
|
@ -93,6 +109,7 @@ 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;
|
||||
|
|
@ -105,29 +122,67 @@ interface IconListContextValue {
|
|||
iconWidth: number;
|
||||
iconsPerRow: number;
|
||||
}
|
||||
export const IconListContext = React.createContext<
|
||||
IconListContextValue | undefined
|
||||
>(undefined);
|
||||
|
||||
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 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>();
|
||||
const listRef = React.useRef<List<IconRow[]> | null>(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);
|
||||
|
|
@ -136,14 +191,17 @@ 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}
|
||||
|
|
@ -167,34 +225,3 @@ export function IconList({ filters, allIcons }: IconListProps) {
|
|||
|
||||
return <Container ref={ref}>{children}</Container>;
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
width: 100%;
|
||||
margin-top: -${HEADER_TOP_PADDING}px;
|
||||
> :first-child {
|
||||
overflow: visible;
|
||||
> :first-child {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const Row = React.memo(
|
||||
({ data, index, style }: ListChildComponentProps<IconRow[]>) => {
|
||||
const { iconWidth } = React.useContext(IconListContext)!;
|
||||
const row = data[index];
|
||||
if (isCategoryRow(row)) {
|
||||
return (
|
||||
<CategoryRow
|
||||
category={row.category}
|
||||
numIcons={row.numIcons}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return <IconsRow icons={row.icons} style={style} iconWidth={iconWidth} />;
|
||||
}
|
||||
},
|
||||
areEqual
|
||||
);
|
||||
Row.displayName = 'Row';
|
||||
|
|
|
|||
|
|
@ -1,32 +1,8 @@
|
|||
import { SpockHandGesture } from 'iconoir-react';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { SUGGEST_ICON } from './constants';
|
||||
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} target={'_blank'} rel={'noreferrer'}>
|
||||
suggestion on GitHub.
|
||||
</a>
|
||||
</Text18>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
margin-top: 90px;
|
||||
display: flex;
|
||||
|
|
@ -34,6 +10,7 @@ const Container = styled.div`
|
|||
flex-direction: column;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const IconContainer = styled.div`
|
||||
svg {
|
||||
width: 60px;
|
||||
|
|
@ -42,8 +19,37 @@ 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,14 +1,22 @@
|
|||
import React from 'react';
|
||||
import { ICON_SPACE } from './constants';
|
||||
import { Icon } from './IconList';
|
||||
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;
|
||||
}
|
||||
`;
|
||||
|
||||
export interface IconsRowProps {
|
||||
icons: Icon[];
|
||||
style?: any;
|
||||
iconWidth: number;
|
||||
}
|
||||
|
||||
export function IconsRow({ icons, style, iconWidth }: IconsRowProps) {
|
||||
return (
|
||||
<RowContainer style={style}>
|
||||
|
|
@ -18,11 +26,3 @@ 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,38 +19,77 @@ const ResetInput = styled.input`
|
|||
}
|
||||
`;
|
||||
|
||||
export const Input = styled(ResetInput)`
|
||||
min-height: 35px;
|
||||
background: var(--super-light-gray);
|
||||
border: 1px solid var(--gray);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
padding: 6px;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
line-height: 21px;
|
||||
font-weight: 500;
|
||||
color: var(--black);
|
||||
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: 75px;
|
||||
font-size: 16px;
|
||||
line-height: 26px;
|
||||
border-radius: 12px;
|
||||
padding: 0 23px;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
&&& {
|
||||
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);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const ColorInput = styled(Input)`
|
||||
export const ColorButton = styled.div`
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='51px' height='51px' stroke-width='2.3' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg' color='%23000000'%3E%3Cpath d='M7 13.161l5.464-5.464a1 1 0 011.415 0l2.12 2.12a1 1 0 010 1.415l-1.928 1.929m-7.071 0l-2.172 2.172a.999.999 0 00-.218.327l-1.028 2.496c-.508 1.233.725 2.466 1.958 1.959l2.497-1.028c.122-.05.233-.125.326-.218l5.708-5.708m-7.071 0h7.071M13.878 3.454l2.121 2.121m4.243 4.243l-2.121-2.121m-2.122-2.122l1.414-1.414a1 1 0 011.415 0l.707.707a1 1 0 010 1.414L18.12 7.697m-2.122-2.122l2.122 2.122' stroke='%23000000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3C/path%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100%;
|
||||
right: 20px;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
&:hover {
|
||||
transition: 0.2s;
|
||||
scale: 1.2;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ColorInput = styled.input`
|
||||
padding: 0px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
&::-webkit-color-swatch,
|
||||
&::-moz-color-swatch {
|
||||
border: none;
|
||||
}
|
||||
background-color: var(--gray-200);
|
||||
opacity: 0;
|
||||
transition: 0.2s;
|
||||
&:hover + ${ColorButton} {
|
||||
transition: 0.2s;
|
||||
scale: 1.3;
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,20 @@
|
|||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { media } from '../lib/responsive';
|
||||
import { GA } from './GA';
|
||||
|
||||
export interface LayoutProps {}
|
||||
export function Layout({ children }: React.PropsWithChildren<LayoutProps>) {
|
||||
const Container = styled.div`
|
||||
padding: 50px 30px;
|
||||
${media.lg} {
|
||||
padding: 30px 50px 50px 50px;
|
||||
}
|
||||
`;
|
||||
|
||||
export function Layout({ children }: React.PropsWithChildren) {
|
||||
return (
|
||||
<div>
|
||||
<Container>
|
||||
<GA />
|
||||
{children}
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import React from 'react';
|
||||
import { MDXRemote as CoreMDXRemote, MDXRemoteProps } from 'next-mdx-remote';
|
||||
import { SuggestLibrary } from './SuggestLibrary';
|
||||
import { Pre, Body, H1, H2, H3, Code } from './Typography';
|
||||
import type { MDXRemoteProps } from 'next-mdx-remote';
|
||||
import { MDXRemote as CoreMDXRemote } from 'next-mdx-remote';
|
||||
import { Table } from './Table';
|
||||
import { Body, Code, H1, H2, H3, InlineCode, Li, Pre } from './Typography';
|
||||
|
||||
export function MDXRemote(props: MDXRemoteProps) {
|
||||
return (
|
||||
|
|
@ -17,7 +16,8 @@ export function MDXRemote(props: MDXRemoteProps) {
|
|||
h2: H2,
|
||||
h3: H3,
|
||||
table: Table,
|
||||
SuggestLibrary,
|
||||
code: InlineCode,
|
||||
li: Li,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,70 +2,111 @@ import Link from 'next/link';
|
|||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { media } from './responsive';
|
||||
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, children, style }: NavigationItemProps) {
|
||||
|
||||
export function NavigationItem({
|
||||
href,
|
||||
activeMatch,
|
||||
children,
|
||||
style,
|
||||
}: NavigationItemProps) {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<Link href={href} passHref>
|
||||
<Link href={href} passHref legacyBehavior>
|
||||
<NavigationItemContainer
|
||||
as={'a'}
|
||||
isActive={
|
||||
href.slice(1)
|
||||
? router.asPath.slice(1).startsWith(href.slice(1))
|
||||
: router.asPath === href
|
||||
}
|
||||
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
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</NavigationItemContainer>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export const NavigationItemContainer = styled(Text15)<{ isActive?: boolean }>`
|
||||
font-weight: 700;
|
||||
font-size: 18px;
|
||||
line-height: 28px;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
padding: 25px;
|
||||
color: var(--black);
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
&:not(:last-child) {
|
||||
border-bottom: solid 1px var(--light-gray);
|
||||
}
|
||||
${media.lg} {
|
||||
font-size: 15px;
|
||||
line-height: 20px;
|
||||
font-weight: 500;
|
||||
padding: 0;
|
||||
color: var(--black-60);
|
||||
width: auto;
|
||||
border-bottom: none !important;
|
||||
${(props) => (props.isActive ? '&' : '&.noop')} {
|
||||
color: var(--white);
|
||||
&::before {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
content: '';
|
||||
display: block;
|
||||
top: -18px;
|
||||
bottom: -18px;
|
||||
left: -24px;
|
||||
right: -24px;
|
||||
background: var(--black);
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,102 +1,9 @@
|
|||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { media } from '../lib/responsive';
|
||||
import { PraiseItem } from './PraiseItem';
|
||||
import { media } from './responsive';
|
||||
|
||||
const NUM_PRAISE_ITEMS = 3;
|
||||
export function Praise() {
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
const indicatorContainerRef = React.useRef<HTMLDivElement>(null);
|
||||
React.useEffect(() => {
|
||||
if (containerRef.current) {
|
||||
const handle = () => {
|
||||
if (indicatorContainerRef.current && containerRef.current) {
|
||||
const currentScrollLeft = containerRef.current.scrollLeft;
|
||||
const totalScroll = containerRef.current.scrollWidth;
|
||||
const interval = totalScroll / NUM_PRAISE_ITEMS;
|
||||
const currentIndex =
|
||||
currentScrollLeft >=
|
||||
containerRef.current.scrollWidth - window.innerWidth - 100
|
||||
? indicatorContainerRef.current.children.length - 1
|
||||
: Math.round(currentScrollLeft / interval);
|
||||
for (
|
||||
let i = 0;
|
||||
i < indicatorContainerRef.current.children.length;
|
||||
i++
|
||||
) {
|
||||
const child = indicatorContainerRef.current.children[i];
|
||||
if (currentIndex === i) {
|
||||
child.classList.add('active');
|
||||
} else {
|
||||
child.classList.remove('active');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const element = containerRef.current;
|
||||
element.addEventListener('scroll', handle);
|
||||
return () => {
|
||||
element.removeEventListener('scroll', handle);
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
return (
|
||||
<>
|
||||
<Container ref={containerRef}>
|
||||
<PraiseItem
|
||||
name={'Riccardo Suardi'}
|
||||
position={'Nibol CEO'}
|
||||
description={
|
||||
<>
|
||||
In Nibol we decided to use Iconoir to speed up the design process.
|
||||
We want to focus on the product and let Iconoir help us with the
|
||||
design.
|
||||
</>
|
||||
}
|
||||
imageUrl={'./riccardo-suardi.png'}
|
||||
logoUrl={'./nibol-logo.svg'}
|
||||
logoLink={'https://www.nibol.com/'}
|
||||
logoAlt={'Nibol Logo'}
|
||||
/>
|
||||
<PraiseItem
|
||||
name={'Fabrizio Rinaldi'}
|
||||
position={'Mailbrew and Typefully founder'}
|
||||
description={
|
||||
<>
|
||||
There's no shortage of icon packs, and yet I always find myself
|
||||
browsing iconoir. I love the style and attention to detail, and
|
||||
how easy it is to grab the perfect icons for my projects.
|
||||
</>
|
||||
}
|
||||
imageUrl={'./fabrizio-rinaldi.png'}
|
||||
logoUrl={'./typefully-logo.png'}
|
||||
logoLink={'https://typefully.com/'}
|
||||
logoAlt={'Typefully Logo'}
|
||||
/>
|
||||
<PraiseItem
|
||||
name={'Chris Messina'}
|
||||
position={'Entrepreneur and # inventor'}
|
||||
description={
|
||||
<>
|
||||
It's the tiny details that determine the degree of delight your
|
||||
customers experience from your product. Adopting Iconoir icons
|
||||
will easily boost your app's delight by a factor of 10!
|
||||
</>
|
||||
}
|
||||
imageUrl={'./chris-messina.png'}
|
||||
logoUrl={'./twitter-logo.png'}
|
||||
logoLink={'https://twitter.com/chrismessina'}
|
||||
logoAlt={'Twitter Logo'}
|
||||
/>
|
||||
</Container>
|
||||
<IndicatorContainer ref={indicatorContainerRef}>
|
||||
<Indicator className={'active'} />
|
||||
<Indicator />
|
||||
<Indicator />
|
||||
</IndicatorContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
max-width: 100%;
|
||||
|
|
@ -131,6 +38,7 @@ const Container = styled.div`
|
|||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const Indicator = styled.div`
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
|
|
@ -141,6 +49,7 @@ const Indicator = styled.div`
|
|||
background: var(--black);
|
||||
}
|
||||
`;
|
||||
|
||||
const IndicatorContainer = styled.div`
|
||||
margin: 40px auto 0 auto;
|
||||
display: flex;
|
||||
|
|
@ -153,3 +62,102 @@ 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,42 @@
|
|||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { media } from './responsive';
|
||||
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;
|
||||
|
|
@ -12,6 +46,7 @@ export interface PraiseItemProps {
|
|||
logoAlt: string;
|
||||
imageUrl: string;
|
||||
}
|
||||
|
||||
export function PraiseItem({
|
||||
name,
|
||||
position,
|
||||
|
|
@ -28,38 +63,10 @@ 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