mirror of
https://github.com/iconoir-icons/iconoir
synced 2026-03-14 22:15:43 +01:00
Compare commits
148 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 |
336 changed files with 15906 additions and 27175 deletions
|
|
@ -1,2 +0,0 @@
|
|||
dist/
|
||||
/examples/react-native/**/*.js
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
es2022: true,
|
||||
node: true,
|
||||
},
|
||||
extends: ['eslint:recommended', 'plugin:prettier/recommended'],
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
},
|
||||
rules: {
|
||||
'padding-line-between-statements': [
|
||||
'error',
|
||||
{
|
||||
blankLine: 'always',
|
||||
prev: [
|
||||
'block',
|
||||
'block-like',
|
||||
'class',
|
||||
'export',
|
||||
'import',
|
||||
'multiline-block-like',
|
||||
'multiline-expression',
|
||||
],
|
||||
next: '*',
|
||||
},
|
||||
{
|
||||
blankLine: 'always',
|
||||
prev: ['const', 'let'],
|
||||
next: ['block', 'block-like', 'class', 'export', 'import'],
|
||||
},
|
||||
{
|
||||
blankLine: 'always',
|
||||
prev: '*',
|
||||
next: ['multiline-block-like', 'multiline-expression', 'return'],
|
||||
},
|
||||
{
|
||||
blankLine: 'any',
|
||||
prev: ['export', 'import'],
|
||||
next: ['export', 'import'],
|
||||
},
|
||||
],
|
||||
|
||||
'prettier/prettier': ['error'],
|
||||
},
|
||||
};
|
||||
0
FUNDING.yml → .github/FUNDING.yml
vendored
0
FUNDING.yml → .github/FUNDING.yml
vendored
14
.github/ISSUE_TEMPLATE/bug_report.md
vendored
14
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
|
@ -13,21 +13,21 @@ Before reporting an issue, please search to see if someone has filed a similar i
|
|||
|
||||
## Prerequisites
|
||||
|
||||
* Version:
|
||||
* Are you running from source/main:
|
||||
* Are you using a released build:
|
||||
* Operating system:
|
||||
- Version:
|
||||
- Are you running from source/main:
|
||||
- Are you using a released build:
|
||||
- Operating system:
|
||||
|
||||
## Step to reproduce
|
||||
|
||||
*(Type here)*
|
||||
_(Type here)_
|
||||
|
||||
### Actual behavior
|
||||
|
||||
## Any message or error
|
||||
|
||||
*(Type here)*
|
||||
_(Type here)_
|
||||
|
||||
## Additional info or screenshots
|
||||
|
||||
* Screenshots
|
||||
- Screenshots
|
||||
|
|
|
|||
6
.github/ISSUE_TEMPLATE/icon_request.md
vendored
6
.github/ISSUE_TEMPLATE/icon_request.md
vendored
|
|
@ -13,6 +13,6 @@ Before creating an icon request, please search to see if someone has requested t
|
|||
|
||||
## Icon Request
|
||||
|
||||
* Icon name:
|
||||
* Use case:
|
||||
* Screenshots of similar icons:
|
||||
- Icon name:
|
||||
- Use case:
|
||||
- Screenshots of similar icons:
|
||||
|
|
|
|||
6
.github/actions/setup/action.yml
vendored
6
.github/actions/setup/action.yml
vendored
|
|
@ -12,11 +12,11 @@ runs:
|
|||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 22
|
||||
registry-url: ${{ inputs.node-registry }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache-dir
|
||||
|
|
@ -24,7 +24,7 @@ runs:
|
|||
run: echo "dir=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache-dir.outputs.dir }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
|
|
|
|||
2
.github/workflows/build.yaml
vendored
2
.github/workflows/build.yaml
vendored
|
|
@ -22,6 +22,6 @@ jobs:
|
|||
run: pnpm run build css
|
||||
|
||||
- name: Commit changes
|
||||
uses: stefanzweifel/git-auto-commit-action@v5
|
||||
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 }}
|
||||
13
.github/workflows/release.yaml
vendored
13
.github/workflows/release.yaml
vendored
|
|
@ -41,7 +41,7 @@ jobs:
|
|||
TAG_NAME: ${{ github.ref_name }}
|
||||
|
||||
- name: Commit release
|
||||
uses: stefanzweifel/git-auto-commit-action@v5
|
||||
uses: stefanzweifel/git-auto-commit-action@v6
|
||||
with:
|
||||
commit_message: Release Version ${{ github.ref_name }}
|
||||
branch: main
|
||||
|
|
@ -62,14 +62,3 @@ jobs:
|
|||
with:
|
||||
credentialJson: ${{ secrets.PUB_CREDENTIAL_JSON }}
|
||||
relativePath: ./packages/iconoir-flutter
|
||||
|
||||
- name: Trigger Website Workflow
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
github.rest.actions.createWorkflowDispatch({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
workflow_id: 'website.yaml',
|
||||
ref: 'main',
|
||||
})
|
||||
|
|
|
|||
26
.github/workflows/website.yaml
vendored
26
.github/workflows/website.yaml
vendored
|
|
@ -1,17 +1,23 @@
|
|||
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: {}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
|
|
@ -27,27 +33,27 @@ jobs:
|
|||
run: pnpm run build react
|
||||
|
||||
- name: Build website
|
||||
run: ./node_modules/.bin/next build
|
||||
run: pnpm run build
|
||||
working-directory: iconoir.com
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup GitHub Pages
|
||||
uses: actions/configure-pages@v3
|
||||
uses: actions/configure-pages@v5
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v2
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: './iconoir.com/out'
|
||||
path: ./iconoir.com/out
|
||||
|
||||
deploy:
|
||||
name: Deploy
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v2
|
||||
uses: actions/deploy-pages@v4
|
||||
|
|
|
|||
1
.node-version
Normal file
1
.node-version
Normal file
|
|
@ -0,0 +1 @@
|
|||
22
|
||||
|
|
@ -1 +0,0 @@
|
|||
dist/
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"singleQuote": true,
|
||||
"quoteProps": "consistent"
|
||||
}
|
||||
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
|
||||
|
|
|
|||
12
README.md
12
README.md
|
|
@ -2,13 +2,12 @@
|
|||
|
||||
[](https://github.com/iconoir-icons/iconoir/releases)
|
||||
[](https://github.com/iconoir-icons/iconoir)
|
||||
[](https://www.npmjs.com/package/iconoir-react)
|
||||
[](https://github.com/iconoir-icons/iconoir/blob/main/LICENSE)
|
||||
[](https://discord.gg/txXcKCAmKW)
|
||||
[](https://discord.gg/txXcKCAmKW)
|
||||
|
||||
## What is Iconoir?
|
||||
|
||||
Iconoir is an open-source library with 1300+ unique SVG icons, designed on a 24x24 pixels grid. No premium icons, no email sign-up, no newsletters.
|
||||
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>
|
||||
|
||||
|
|
@ -65,10 +64,7 @@ You can switch between icons from the right sidebar in the editor.
|
|||
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:
|
||||
|
|
@ -121,4 +117,4 @@ struct ContentView: View {
|
|||
|
||||
## License
|
||||
|
||||
[MIT](./LICENSE)
|
||||
MIT License.
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import { Listr } from 'listr2';
|
||||
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));
|
||||
|
||||
|
|
@ -42,7 +44,7 @@ const tasks = new Listr(
|
|||
{
|
||||
title: 'Fetching icons',
|
||||
task: async (ctx) => {
|
||||
ctx.icons = {};
|
||||
ctx.tasks = { global: { defaultVariant }, icons: {} };
|
||||
|
||||
const iconsVariantsDirs = Object.fromEntries(
|
||||
iconsVariants.map((variant) => [
|
||||
|
|
@ -71,31 +73,24 @@ const tasks = new Listr(
|
|||
};
|
||||
});
|
||||
|
||||
ctx.icons[variant] = icons;
|
||||
ctx.tasks.icons[variant] = icons;
|
||||
}
|
||||
|
||||
ctx.global = { defaultVariant };
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Building targets',
|
||||
task: (_, task) =>
|
||||
task: (ctx, task) =>
|
||||
task.newListr(
|
||||
Object.entries(targets).map(([targetName, targetConfig]) => ({
|
||||
title: targetConfig.title,
|
||||
enabled: () =>
|
||||
cliTargets.length === 0 || cliTargets.includes(targetName),
|
||||
task: async (ctx) => {
|
||||
const { default: task } = await import(
|
||||
`./targets/${targetConfig.target || targetName}/index.js`
|
||||
);
|
||||
|
||||
enabled: () => ctx.cliTargets.length === 0 || ctx.cliTargets.includes(targetName),
|
||||
task: (ctx) => {
|
||||
targetConfig.path = path.join(
|
||||
rootDir,
|
||||
...targetConfig.path.split(path.posix.sep),
|
||||
);
|
||||
|
||||
return task(ctx, targetConfig);
|
||||
return ctx.pool.run({ targetName, config: ctx.tasks, targetConfig });
|
||||
},
|
||||
})),
|
||||
{ concurrent: true, exitOnError: false },
|
||||
|
|
@ -130,4 +125,15 @@ for (const arg of process.argv.slice(2)) {
|
|||
}
|
||||
}
|
||||
|
||||
await tasks.run();
|
||||
const pool = new Tinypool({
|
||||
filename: new URL('./worker.js', import.meta.url).href,
|
||||
minThreads: 0,
|
||||
resourceLimits: {
|
||||
// Vue target (Vite/Rollup) takes up a lot of memory
|
||||
maxOldGenerationSizeMb: 8192,
|
||||
},
|
||||
});
|
||||
|
||||
await tasks.run({ cliTargets, pool });
|
||||
|
||||
await pool.destroy();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import path from 'node:path';
|
||||
|
||||
export function generateImport(name, from) {
|
||||
if (Array.isArray(name)) name = `{${name.toString()}}`;
|
||||
if (Array.isArray(name))
|
||||
name = `{${name.toString()}}`;
|
||||
|
||||
return `import ${name} from "${from}";`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,15 @@
|
|||
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);
|
||||
|
|
@ -8,7 +17,8 @@ export function getDts(path, content, options) {
|
|||
const _readFile = host.readFile;
|
||||
|
||||
host.readFile = (filename) => {
|
||||
if (filename === path) return content;
|
||||
if (normalize(filename) === path)
|
||||
return content;
|
||||
|
||||
return _readFile(filename);
|
||||
};
|
||||
|
|
@ -16,7 +26,8 @@ export function getDts(path, content, options) {
|
|||
const dtsFilename = path.replace(/\.(m|c)?(ts|js)x?$/, '.d.$1ts');
|
||||
|
||||
host.writeFile = (filename, contents) => {
|
||||
if (filename === dtsFilename) output = contents;
|
||||
if (normalize(filename) === dtsFilename)
|
||||
output = contents;
|
||||
};
|
||||
|
||||
const program = ts.createProgram([path], options, host);
|
||||
|
|
@ -32,6 +43,7 @@ export function getDts(path, content, options) {
|
|||
diagnostic.file,
|
||||
diagnostic.start,
|
||||
);
|
||||
|
||||
const message = ts.flattenDiagnosticMessageText(
|
||||
diagnostic.messageText,
|
||||
'\n',
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import fs from 'node:fs/promises';
|
||||
import { EOL } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
|
|
@ -18,10 +19,9 @@ export default async (ctx, target) => {
|
|||
const variantCssContent = [header];
|
||||
|
||||
const cssTarget = (icon, suffixed) => {
|
||||
const iconName =
|
||||
suffixed && variant !== ctx.global.defaultVariant
|
||||
? icon.nameVariant
|
||||
: icon.name;
|
||||
const iconName = suffixed && variant !== ctx.global.defaultVariant
|
||||
? icon.nameVariant
|
||||
: icon.name;
|
||||
|
||||
return `.iconoir-${iconName}::before`;
|
||||
};
|
||||
|
|
@ -30,9 +30,9 @@ export default async (ctx, target) => {
|
|||
const fileContent = await fs.readFile(icon.path, 'utf8');
|
||||
|
||||
const transformedContent = fileContent
|
||||
.replace(/\n/g, '')
|
||||
.replace(/(width|height)="[0-9]+px"/g, '')
|
||||
.replace(/[ ]+/g, ' ');
|
||||
.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}');}`;
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export default async (ctx, target) => {
|
|||
|
||||
const outDir = path.join(target.path, 'lib');
|
||||
|
||||
const entryContent = ['library iconoir_flutter;'];
|
||||
const entryContent = ['library;'];
|
||||
|
||||
for (const [variant, icons] of Object.entries(ctx.icons)) {
|
||||
const variantOutDir = path.join(outDir, variant);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
const template = (name, svg) => `
|
||||
import 'package:flutter/widgets.dart' as widgets;
|
||||
function template(name, svg) {
|
||||
return `import 'package:flutter/widgets.dart' as widgets;
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
class ${name} extends widgets.StatelessWidget {
|
||||
|
|
@ -7,19 +7,20 @@ class ${name} extends widgets.StatelessWidget {
|
|||
final double? width;
|
||||
final double? height;
|
||||
|
||||
const ${name}({widgets.Key? key, this.color, this.width, this.height})
|
||||
: super(key: key);
|
||||
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,
|
||||
);
|
||||
colorFilter: color != null
|
||||
? widgets.ColorFilter.mode(color!, widgets.BlendMode.srcIn)
|
||||
: null,
|
||||
width: width,
|
||||
height: height,
|
||||
);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
export default template;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import * as svgr from '@svgr/core';
|
||||
import * as esbuild from 'esbuild';
|
||||
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,
|
||||
|
|
@ -36,9 +36,7 @@ const jsTargets = [
|
|||
/** @type {import('esbuild').TransformOptions} */
|
||||
const defaultEsbuildOptions = { target: 'es6', minify: true };
|
||||
|
||||
/** @type {import('typescript').CompilerOptions} */
|
||||
const defaultTsOptions = {
|
||||
jsx: 'react',
|
||||
declaration: true,
|
||||
emitDeclarationOnly: true,
|
||||
target: 'es6',
|
||||
|
|
@ -80,6 +78,7 @@ export default async (ctx, target) => {
|
|||
jsTarget.path,
|
||||
'IconoirContext.tsx',
|
||||
);
|
||||
|
||||
const iconoirContextDtsPath = path.join(
|
||||
jsTarget.path,
|
||||
`IconoirContext.${jsTarget.dtsExt}`,
|
||||
|
|
@ -90,6 +89,7 @@ export default async (ctx, target) => {
|
|||
iconoirContextDtsPath,
|
||||
iconoirContext,
|
||||
jsTarget.module,
|
||||
target.native,
|
||||
);
|
||||
|
||||
for (const variant of Object.keys(ctx.icons)) {
|
||||
|
|
@ -106,10 +106,9 @@ export default async (ctx, target) => {
|
|||
const variantIndex = prepareIndex(jsTarget, variant);
|
||||
|
||||
for (const icon of icons) {
|
||||
const mainIndexComponentName =
|
||||
variant === ctx.global.defaultVariant
|
||||
? icon.pascalName
|
||||
: icon.pascalNameVariant;
|
||||
const mainIndexComponentName = variant === ctx.global.defaultVariant
|
||||
? icon.pascalName
|
||||
: icon.pascalNameVariant;
|
||||
|
||||
const jsPath = path.join(
|
||||
jsTarget.path,
|
||||
|
|
@ -152,6 +151,7 @@ export default async (ctx, target) => {
|
|||
dtsPath,
|
||||
reactComponent,
|
||||
jsTarget.module,
|
||||
target.native,
|
||||
);
|
||||
|
||||
promises.push(iconDts);
|
||||
|
|
@ -174,16 +174,20 @@ export default async (ctx, target) => {
|
|||
async function getReactComponent(iconPath, native, template) {
|
||||
const iconContent = await fs.readFile(iconPath, 'utf8');
|
||||
|
||||
const options = native ? nativeSvgrOptions : svgrOptions;
|
||||
options.template = template;
|
||||
const options = {
|
||||
...(native ? nativeSvgrOptions : svgrOptions),
|
||||
template,
|
||||
};
|
||||
|
||||
return svgr.transform(iconContent, options);
|
||||
}
|
||||
|
||||
async function generateDts(inputPath, outputPath, input, module) {
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
const template = (native) => {
|
||||
function template(native) {
|
||||
const useClientDirective = native ? '' : '"use client";';
|
||||
|
||||
const imports = [
|
||||
|
|
@ -11,8 +11,8 @@ ${useClientDirective}
|
|||
${imports}
|
||||
|
||||
type IconoirContextValue = Partial<${
|
||||
native ? 'SvgProps' : 'React.SVGProps<SVGSVGElement>'
|
||||
}>;
|
||||
native ? 'SvgProps' : 'React.SVGProps<SVGSVGElement>'
|
||||
}>;
|
||||
|
||||
export const IconoirContext = React.createContext<IconoirContextValue>({});
|
||||
|
||||
|
|
@ -29,7 +29,7 @@ export function IconoirProvider({ iconProps, children }: IconoirProviderProps) {
|
|||
);
|
||||
}
|
||||
`;
|
||||
};
|
||||
}
|
||||
|
||||
export default template;
|
||||
|
||||
|
|
|
|||
|
|
@ -5,31 +5,30 @@ export function getTemplate(native, iconoirContextPath) {
|
|||
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';
|
||||
}
|
||||
if (native)
|
||||
variables.props[1].typeAnnotation.typeAnnotation.typeParameters.params[0].typeName.name = 'Svg';
|
||||
|
||||
const useClientDirective = native ? [] : '"use client"';
|
||||
|
||||
const useClientDirective = native ? '' : '"use client";';
|
||||
const iconoirContextImport = generateImport(
|
||||
['IconoirContext'],
|
||||
iconoirContextPath,
|
||||
);
|
||||
|
||||
return tpl`
|
||||
${useClientDirective}
|
||||
${variables.imports};
|
||||
${iconoirContextImport};
|
||||
|
||||
${variables.interfaces};
|
||||
${useClientDirective};
|
||||
${variables.imports};
|
||||
${iconoirContextImport}
|
||||
|
||||
${variables.interfaces};
|
||||
|
||||
const ${variables.componentName} = (${variables.props}) => {
|
||||
const context = React.useContext(IconoirContext);
|
||||
const props = { ...context, ...passedProps };
|
||||
return ${variables.jsx};
|
||||
};
|
||||
|
||||
const ${variables.componentName} = (${variables.props}) => {
|
||||
const context = React.useContext(IconoirContext);
|
||||
const props = { ...context, ...passedProps };
|
||||
return ${variables.jsx};
|
||||
};
|
||||
|
||||
${variables.exports};
|
||||
${variables.exports};
|
||||
`;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
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 fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { build } from 'vite';
|
||||
import dts from 'vite-plugin-dts';
|
||||
import { generateExport } from '../../lib/import-export.js';
|
||||
|
|
@ -44,10 +44,9 @@ export default async (ctx, target) => {
|
|||
|
||||
promises.push(generateIconFile(icon.path, vueFileName));
|
||||
|
||||
const mainIndexComponentName =
|
||||
variant === ctx.global.defaultVariant
|
||||
? icon.pascalName
|
||||
: icon.pascalNameVariant;
|
||||
const mainIndexComponentName = variant === ctx.global.defaultVariant
|
||||
? icon.pascalName
|
||||
: icon.pascalNameVariant;
|
||||
|
||||
mainIndexContent.push(
|
||||
generateExport(
|
||||
|
|
@ -85,12 +84,9 @@ export default async (ctx, target) => {
|
|||
formats: ['cjs', 'es'],
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ['vue-demi', 'vue'],
|
||||
external: ['vue'],
|
||||
},
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: ['vue-demi'],
|
||||
},
|
||||
plugins: [
|
||||
vue({
|
||||
isProduction: true,
|
||||
|
|
|
|||
|
|
@ -1,16 +1,21 @@
|
|||
const template = (svg) => `<script lang="ts">
|
||||
import { defineComponent, inject } from "vue-demi";
|
||||
import type { SVGAttributes } from "vue-demi";
|
||||
import providerKey from "../providerKey";
|
||||
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>(() => {
|
||||
const context = inject(providerKey);
|
||||
return { context };
|
||||
export default defineComponent<SVGAttributes>({
|
||||
setup() {
|
||||
const context = inject(providerKey);
|
||||
return { context };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
${svg}
|
||||
</template>`;
|
||||
</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,10 +1,11 @@
|
|||
import { updateYamlKey } from '@atomist/yaml-updater';
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import { updateYamlKey } from '@atomist/yaml-updater';
|
||||
import semver from 'semver';
|
||||
|
||||
const PACKAGE_BASE = '';
|
||||
|
||||
const newVersion = semver.valid(semver.coerce(process.env.TAG_NAME));
|
||||
console.info('New version is %s', newVersion);
|
||||
|
||||
|
|
@ -21,17 +22,13 @@ 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');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,7 @@
|
|||
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:
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
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',
|
||||
},
|
||||
});
|
||||
13
examples/next/.gitignore
vendored
13
examples/next/.gitignore
vendored
|
|
@ -3,8 +3,12 @@
|
|||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
|
@ -24,9 +28,10 @@
|
|||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
experimental: {
|
||||
optimizePackageImports: ['iconoir-react'],
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
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;
|
||||
|
|
@ -2,21 +2,21 @@
|
|||
"name": "example-next",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"dev": "next dev",
|
||||
"lint": "next lint",
|
||||
"start": "next start"
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "14.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"next": "15.4.4",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.8.9",
|
||||
"@types/react": "^18.2.33",
|
||||
"@types/react-dom": "^18.2.14",
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/react": "^19.0.1",
|
||||
"@types/react-dom": "^19.0.2",
|
||||
"iconoir-react": "workspace:*",
|
||||
"typescript": "^5.2.2"
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,26 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"incremental": true,
|
||||
"target": "ES2017",
|
||||
"jsx": "preserve",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
},
|
||||
"resolveJsonModule": true,
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"skipLibCheck": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
|
|
|
|||
1
examples/react-native/.gitignore
vendored
1
examples/react-native/.gitignore
vendored
|
|
@ -7,6 +7,7 @@ node_modules/
|
|||
.expo/
|
||||
dist/
|
||||
web-build/
|
||||
expo-env.d.ts
|
||||
|
||||
# Native
|
||||
*.orig.*
|
||||
|
|
|
|||
|
|
@ -1,13 +1,21 @@
|
|||
{
|
||||
"expo": {
|
||||
"name": "Iconoir",
|
||||
"slug": "react-native-expo",
|
||||
"slug": "example-react-native",
|
||||
"version": "1.0.0",
|
||||
"orientation": "portrait",
|
||||
"userInterfaceStyle": "light",
|
||||
"assetBundlePatterns": ["**/*"],
|
||||
"newArchEnabled": true,
|
||||
"splash": {
|
||||
"backgroundColor": "#ffffff"
|
||||
},
|
||||
"ios": {
|
||||
"supportsTablet": true
|
||||
},
|
||||
"android": {
|
||||
"adaptiveIcon": {
|
||||
"backgroundColor": "#ffffff"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
module.exports = function (api) {
|
||||
api.cache(true);
|
||||
|
||||
return {
|
||||
presets: ['babel-preset-expo'],
|
||||
};
|
||||
};
|
||||
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,
|
||||
},
|
||||
});
|
||||
17440
examples/react-native/package-lock.json
generated
17440
examples/react-native/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,25 +1,30 @@
|
|||
{
|
||||
"name": "example-react-native",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "node_modules/expo/AppEntry.js",
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"android": "expo start --android",
|
||||
"ios": "expo start --ios",
|
||||
"start": "expo start",
|
||||
"web": "expo start --web"
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/webpack-config": "^19.0.0",
|
||||
"expo": "~49.0.15",
|
||||
"expo-status-bar": "~1.6.0",
|
||||
"iconoir-react-native": "^7.0.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-native": "0.72.6",
|
||||
"react-native-svg": "^13.9.0",
|
||||
"react-native-web": "~0.19.6"
|
||||
"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.20.0"
|
||||
"@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
|
||||
}
|
||||
}
|
||||
2
examples/vue/.gitignore
vendored
2
examples/vue/.gitignore
vendored
|
|
@ -26,3 +26,5 @@ coverage
|
|||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Iconoir</title>
|
||||
</head>
|
||||
|
|
|
|||
|
|
@ -1,25 +1,27 @@
|
|||
{
|
||||
"name": "example-vue",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "run-p type-check \"build-only {@}\" --",
|
||||
"build-only": "vite build",
|
||||
"dev": "vite",
|
||||
"build": "run-p type-check \"build-only {@}\" --",
|
||||
"preview": "vite preview",
|
||||
"type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false"
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --build"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.3.7"
|
||||
"vue": "^3.5.18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconoir/vue": "workspace:*",
|
||||
"@tsconfig/node18": "^18.2.2",
|
||||
"@types/node": "^18.18.7",
|
||||
"@vitejs/plugin-vue": "^4.4.0",
|
||||
"@vue/tsconfig": "^0.4.0",
|
||||
"npm-run-all2": "^6.1.1",
|
||||
"typescript": "~5.2.2",
|
||||
"vite": "^4.5.0",
|
||||
"vue-tsc": "^1.8.22"
|
||||
"@tsconfig/node22": "^22.0.2",
|
||||
"@types/node": "^22.16.5",
|
||||
"@vitejs/plugin-vue": "^6.0.0",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"npm-run-all2": "^8.0.4",
|
||||
"typescript": "~5.8.3",
|
||||
"vite": "^7.0.6",
|
||||
"vite-plugin-vue-devtools": "^7.7.7",
|
||||
"vue-tsc": "^3.0.4"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<script setup lang="ts">
|
||||
import {
|
||||
Check,
|
||||
Iconoir,
|
||||
IconoirProvider,
|
||||
Medal1st,
|
||||
Medal1stSolid,
|
||||
IconoirProvider,
|
||||
Check,
|
||||
} from '@iconoir/vue';
|
||||
</script>
|
||||
|
||||
|
|
@ -21,8 +21,6 @@ import {
|
|||
'height': '2em',
|
||||
}"
|
||||
>
|
||||
<SomeOtherContainer>
|
||||
<Check />
|
||||
</SomeOtherContainer>
|
||||
<Check />
|
||||
</IconoirProvider>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"baseUrl": ".",
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"exclude": ["src/**/__tests__/*"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
|
|
@ -7,5 +6,6 @@
|
|||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
}
|
||||
]
|
||||
],
|
||||
"files": []
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,19 @@
|
|||
{
|
||||
"extends": "@tsconfig/node18/tsconfig.json",
|
||||
"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.*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"types": ["node"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,18 @@
|
|||
import { fileURLToPath, URL } from 'node:url'
|
||||
import { fileURLToPath, URL } from 'node:url';
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import { defineConfig } from 'vite';
|
||||
import vueDevTools from 'vite-plugin-vue-devtools';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
vueDevTools(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
}
|
||||
})
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
{
|
||||
"extends": ["next/core-web-vitals"],
|
||||
"settings": {
|
||||
"next": {
|
||||
"rootDir": "iconoir.com"
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"react/no-unescaped-entities": ["off"]
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.js"],
|
||||
"parser": "espree",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,30 +1,6 @@
|
|||
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;
|
||||
|
|
@ -54,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 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',
|
||||
|
|
@ -27,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',
|
||||
|
|
@ -43,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',
|
||||
|
|
@ -68,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',
|
||||
|
|
|
|||
|
|
@ -8,91 +8,12 @@ import {
|
|||
import { media } from '../lib/responsive';
|
||||
import { Text14 } from './Typography';
|
||||
|
||||
export function AvailableFor() {
|
||||
const { ref, width } = useResizeObserver();
|
||||
|
||||
return (
|
||||
<>
|
||||
<MobileHeader>Available For</MobileHeader>
|
||||
<AvailableForOuter>
|
||||
<AvailableForContainer $contentWidth={width || 0} ref={ref}>
|
||||
<DesktopHeader>Available for</DesktopHeader>
|
||||
<a href={LIBRARY_LINKS.React} target={'_blank'} rel={'noreferrer'}>
|
||||
<AvailableForImage
|
||||
src={'/logo-react.svg'}
|
||||
alt={'React Logo'}
|
||||
title={'React'}
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href={'https://github.com/iconoir-icons/iconoir#swift-package'}
|
||||
target={'_blank'}
|
||||
rel={'noreferrer'}
|
||||
>
|
||||
<AvailableForImage
|
||||
src={'/logo-swift.svg'}
|
||||
alt={'Swift Logo'}
|
||||
title={'Swift'}
|
||||
/>
|
||||
</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.Vue} target={'_blank'} rel={'noreferrer'}>
|
||||
<AvailableForImage
|
||||
src={'/logo-vue.svg'}
|
||||
alt={'Vue Logo'}
|
||||
title={'Vue'}
|
||||
/>
|
||||
</a>
|
||||
<a href={LIBRARY_LINKS.Framer} target={'_blank'} rel={'noreferrer'}>
|
||||
<AvailableForImage
|
||||
src={'/logo-framer.svg'}
|
||||
alt={'Framer Logo'}
|
||||
title={'Framer'}
|
||||
/>
|
||||
</a>
|
||||
<AreYouUsing>
|
||||
<a href={SUGGEST_LIBRARY_LINK} target={'_blank'} rel={'noreferrer'}>
|
||||
<Text14>More?</Text14>
|
||||
</a>
|
||||
<a href={FEEDBACK_LINK} target={'_blank'} rel={'noreferrer'}>
|
||||
<Text14>Are you using the library?</Text14>
|
||||
</a>
|
||||
</AreYouUsing>
|
||||
</AvailableForContainer>
|
||||
</AvailableForOuter>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const AreYouUsing = styled.div`
|
||||
* {
|
||||
white-space: nowrap;
|
||||
}
|
||||
`;
|
||||
|
||||
const MobileHeader = styled(Text14)`
|
||||
&&& {
|
||||
display: block;
|
||||
|
|
@ -103,6 +24,7 @@ const MobileHeader = styled(Text14)`
|
|||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const DesktopHeader = styled(Text14)`
|
||||
&&& {
|
||||
display: none;
|
||||
|
|
@ -111,6 +33,7 @@ const DesktopHeader = styled(Text14)`
|
|||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const AvailableForAnimation = keyframes`
|
||||
5% {
|
||||
transform: translateX(0);
|
||||
|
|
@ -125,6 +48,7 @@ const AvailableForAnimation = keyframes`
|
|||
transform: translateX(0);
|
||||
}
|
||||
`;
|
||||
|
||||
const AvailableForOuter = styled.div`
|
||||
max-width: 100vw;
|
||||
margin: 16px -30px 70px -30px;
|
||||
|
|
@ -135,6 +59,7 @@ const AvailableForOuter = styled.div`
|
|||
padding: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const AvailableForContainer = styled.div<{ $contentWidth: number }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -142,8 +67,8 @@ const AvailableForContainer = styled.div<{ $contentWidth: number }>`
|
|||
width: max-content;
|
||||
--content-width: ${(props) => props.$contentWidth}px;
|
||||
${(props) =>
|
||||
props.$contentWidth &&
|
||||
css`
|
||||
props.$contentWidth
|
||||
&& css`
|
||||
animation: ${AvailableForAnimation} 40s cubic-bezier(0.37, 0, 0.63, 1)
|
||||
infinite;
|
||||
`}
|
||||
|
|
@ -161,6 +86,7 @@ const AvailableForContainer = styled.div<{ $contentWidth: number }>`
|
|||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const AvailableForImage = styled.img`
|
||||
height: 40px;
|
||||
display: block;
|
||||
|
|
@ -173,3 +99,107 @@ const AvailableForImage = styled.img`
|
|||
height: 50px;
|
||||
}
|
||||
`;
|
||||
|
||||
export function AvailableFor() {
|
||||
const { ref, width } = useResizeObserver();
|
||||
|
||||
return (
|
||||
<>
|
||||
<MobileHeader>Available For</MobileHeader>
|
||||
<AvailableForOuter>
|
||||
<AvailableForContainer $contentWidth={width || 0} ref={ref}>
|
||||
<DesktopHeader>Available for</DesktopHeader>
|
||||
<a href={LIBRARY_LINKS.React} target="_blank" rel="noreferrer">
|
||||
<AvailableForImage
|
||||
src="/logo-react.svg"
|
||||
alt="React Logo"
|
||||
title="React"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/iconoir-icons/iconoir#swift-package"
|
||||
target="_blank"
|
||||
rel="nofollow noreferrer"
|
||||
>
|
||||
<AvailableForImage
|
||||
src="/logo-swift.svg"
|
||||
alt="Swift Logo"
|
||||
title="Swift"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href={LIBRARY_LINKS.Flutter}
|
||||
target="_blank"
|
||||
rel="nofollow noreferrer"
|
||||
>
|
||||
<AvailableForImage
|
||||
src="/logo-flutter.svg"
|
||||
alt="Flutter Logo"
|
||||
title="Flutter"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href={LIBRARY_LINKS.Figma}
|
||||
target="_blank"
|
||||
rel="nofollow noreferrer"
|
||||
>
|
||||
<AvailableForImage
|
||||
src="/logo-figma.svg"
|
||||
alt="Figma Logo"
|
||||
title="Figma"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href={LIBRARY_LINKS.ReactNative}
|
||||
target="_blank"
|
||||
rel="nofollow noreferrer"
|
||||
>
|
||||
<AvailableForImage
|
||||
src="/logo-react-native.svg"
|
||||
alt="React Native Logo"
|
||||
title="React Native"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href={LIBRARY_LINKS.Vue}
|
||||
target="_blank"
|
||||
rel="nofollow noreferrer"
|
||||
>
|
||||
<AvailableForImage
|
||||
src="/logo-vue.svg"
|
||||
alt="Vue Logo"
|
||||
title="Vue"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href={LIBRARY_LINKS.Framer}
|
||||
target="_blank"
|
||||
rel="nofollow noreferrer"
|
||||
>
|
||||
<AvailableForImage
|
||||
src="/logo-framer.svg"
|
||||
alt="Framer Logo"
|
||||
title="Framer"
|
||||
/>
|
||||
</a>
|
||||
<AreYouUsing>
|
||||
<a
|
||||
href={SUGGEST_LIBRARY_LINK}
|
||||
target="_blank"
|
||||
rel="nofollow noreferrer"
|
||||
>
|
||||
<Text14>More?</Text14>
|
||||
</a>
|
||||
<a
|
||||
href={FEEDBACK_LINK}
|
||||
target="_blank"
|
||||
rel="nofollow noreferrer"
|
||||
>
|
||||
<Text14>Are you using the library?</Text14>
|
||||
</a>
|
||||
</AreYouUsing>
|
||||
</AvailableForContainer>
|
||||
</AvailableForOuter>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ export const LargeButton = styled(ResetButton)`
|
|||
}
|
||||
`;
|
||||
|
||||
export const Button = styled(LargeButton)`
|
||||
const Button = styled(LargeButton)`
|
||||
&&&& {
|
||||
height: 40px;
|
||||
font-size: 13px;
|
||||
|
|
|
|||
|
|
@ -2,25 +2,6 @@ import styled from 'styled-components';
|
|||
import { media } from '../lib/responsive';
|
||||
import { Text15 } from './Typography';
|
||||
|
||||
export interface CategoryRowProps {
|
||||
category: string;
|
||||
numIcons: number;
|
||||
style?: any;
|
||||
}
|
||||
export function CategoryRow({ category, numIcons, style }: CategoryRowProps) {
|
||||
return (
|
||||
<Container style={style}>
|
||||
<InnerContainer>
|
||||
<Title>{category}</Title>
|
||||
<Text15>
|
||||
{numIcons} Icon{numIcons === 1 ? '' : 's'}
|
||||
</Text15>
|
||||
<Separator />
|
||||
</InnerContainer>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const InnerContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -32,6 +13,7 @@ const InnerContainer = styled.div`
|
|||
margin-right: 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
|
|
@ -41,6 +23,7 @@ const Container = styled.div`
|
|||
padding-bottom: 40px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Title = styled(Text15)`
|
||||
&&& {
|
||||
font-weight: 700;
|
||||
|
|
@ -50,8 +33,32 @@ const Title = styled(Text15)`
|
|||
padding: 6px 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Separator = styled.div`
|
||||
height: 1px;
|
||||
flex: 1;
|
||||
background: var(--g6);
|
||||
`;
|
||||
|
||||
export interface CategoryRowProps {
|
||||
category: string;
|
||||
numIcons: number;
|
||||
style?: any;
|
||||
}
|
||||
|
||||
export function CategoryRow({ category, numIcons, style }: CategoryRowProps) {
|
||||
return (
|
||||
<Container style={style}>
|
||||
<InnerContainer>
|
||||
<Title>{category}</Title>
|
||||
<Text15>
|
||||
{numIcons}
|
||||
{' '}
|
||||
Icon
|
||||
{numIcons === 1 ? '' : 's'}
|
||||
</Text15>
|
||||
<Separator />
|
||||
</InnerContainer>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,72 +1,15 @@
|
|||
import type { MDXRemoteSerializeResult } from 'next-mdx-remote';
|
||||
import { BoxIso } from 'iconoir-react';
|
||||
import moment from 'moment';
|
||||
import { MDXRemoteSerializeResult } from 'next-mdx-remote';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { MDXRemote } from './MDXRemote';
|
||||
import { media } from '../lib/responsive';
|
||||
import { Code, Text15, Text18 } from './Typography';
|
||||
import { CopyButton } from './Button';
|
||||
import { MDXRemote } from './MDXRemote';
|
||||
import { Code, Text15, Text18 } from './Typography';
|
||||
|
||||
const EXPAND_HEIGHT = 400;
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
margin: 40px 0;
|
||||
display: flex;
|
||||
|
|
@ -79,6 +22,7 @@ const Container = styled.div`
|
|||
margin: 24px 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const ContainerLeft = styled.div`
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
|
@ -88,25 +32,30 @@ 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;
|
||||
}
|
||||
`;
|
||||
|
||||
const ExpandContainer = styled.div`
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
right: 23px;
|
||||
`;
|
||||
|
||||
const EntryBody = styled(Code)<{ $expanded?: boolean }>`
|
||||
&&& {
|
||||
flex: 1;
|
||||
|
|
@ -135,3 +84,63 @@ const EntryBody = styled(Code)<{ $expanded?: boolean }>`
|
|||
}
|
||||
}
|
||||
`;
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,33 +2,34 @@ import Link from 'next/link';
|
|||
import styled from 'styled-components';
|
||||
import { Text13 } from './Typography';
|
||||
|
||||
const Container = styled(Text13)`
|
||||
&&& {
|
||||
color: var(--g1);
|
||||
font-weight: 700;
|
||||
background: var(--g5);
|
||||
line-height: 1;
|
||||
padding: 7px 16px;
|
||||
border-radius: 200px;
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
transition:
|
||||
color 0.1s linear,
|
||||
background 0.1s linear;
|
||||
&:hover {
|
||||
background: var(--black);
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export interface CurrentVersionProps {
|
||||
version: string;
|
||||
}
|
||||
|
||||
export function CurrentVersion({ version }: CurrentVersionProps) {
|
||||
return (
|
||||
<Link href={'/docs/changelog'} passHref legacyBehavior>
|
||||
<Container as={'a'}>{version}</Container>
|
||||
<Link href="/docs/changelog" passHref legacyBehavior>
|
||||
<Container as="a">{version}</Container>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
const Container = styled(Text13)`
|
||||
&&& {
|
||||
color: var(--g1);
|
||||
font-weight: 700;
|
||||
background: var(--g5);
|
||||
line-height: 1;
|
||||
padding: 7px 16px;
|
||||
border-radius: 200px;
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
transition:
|
||||
color 0.1s linear,
|
||||
background 0.1s linear;
|
||||
&:hover {
|
||||
background: var(--black);
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,16 +1,62 @@
|
|||
import type { IconListCustomizations } from './IconList';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { DEFAULT_CUSTOMIZATIONS, IconListCustomizations } from './IconList';
|
||||
import { media } from '../lib/responsive';
|
||||
import { DEFAULT_CUSTOMIZATIONS } from './IconList';
|
||||
import { ColorButton, ColorInput } from './Input';
|
||||
import { Slider } from './Slider';
|
||||
import { Text13, Text15 } from './Typography';
|
||||
import { media } from '../lib/responsive';
|
||||
|
||||
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,6 +64,7 @@ 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,
|
||||
);
|
||||
|
|
@ -50,7 +97,7 @@ export function CustomizationEditor({
|
|||
</Header>
|
||||
<Field>
|
||||
<Slider
|
||||
label={'Optical Size'}
|
||||
label="Optical Size"
|
||||
minValue={16}
|
||||
maxValue={64}
|
||||
value={[size]}
|
||||
|
|
@ -63,7 +110,7 @@ export function CustomizationEditor({
|
|||
</Field>
|
||||
<Field>
|
||||
<Slider
|
||||
label={'Stroke Weight'}
|
||||
label="Stroke Weight"
|
||||
minValue={0.5}
|
||||
maxValue={3}
|
||||
value={[strokeWidth]}
|
||||
|
|
@ -78,7 +125,7 @@ export function CustomizationEditor({
|
|||
<HorizontalField>
|
||||
<Text13>Color</Text13>
|
||||
<ColorInput
|
||||
type={'color'}
|
||||
type="color"
|
||||
value={color}
|
||||
onChange={(e) => {
|
||||
setColor(e.target.value);
|
||||
|
|
@ -91,44 +138,3 @@ export function CustomizationEditor({
|
|||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const CustomizationBox = styled.div`
|
||||
background-color: var(--g7);
|
||||
width: 84%;
|
||||
padding: 8%;
|
||||
border-radius: 10px;
|
||||
margin: 24px 0;
|
||||
display: none;
|
||||
${media.md} {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
const Header = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 30px;
|
||||
border-bottom: solid 1px var(--g6);
|
||||
padding-bottom: 10px;
|
||||
`;
|
||||
const Field = styled.div`
|
||||
margin-bottom: 24px;
|
||||
`;
|
||||
const HorizontalField = styled(Field)`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
const ResetButton = styled(Field)`
|
||||
&&& {
|
||||
margin: initial;
|
||||
text-decoration: underline;
|
||||
color: var(--dark-gray);
|
||||
font-size: 13px;
|
||||
|
||||
&:hover {
|
||||
color: var(--black);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,103 @@
|
|||
import type { DocumentationItem } from '../pages/docs/[...slug]';
|
||||
import { NavArrowUp } from 'iconoir-react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { DocumentationItem } from '../pages/docs/[...slug]';
|
||||
import { media } from '../lib/responsive';
|
||||
|
||||
const HeaderItemIcon = styled.div<{ $active?: boolean }>`
|
||||
font-size: 13px;
|
||||
transition: transform 0.25s linear;
|
||||
transform: rotate(${(props) => (props.$active ? 180 : 0)}deg);
|
||||
margin-right: 7px;
|
||||
position: relative;
|
||||
top: 6px;
|
||||
svg {
|
||||
display: block;
|
||||
}
|
||||
${media.lg} {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const ChildrenContainer = styled.div<{ $expanded?: boolean }>`
|
||||
display: ${(props) => (props.$expanded ? 'block' : 'none')};
|
||||
${media.lg} {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
||||
const HeaderItem = styled.div`
|
||||
padding: 10px 30px;
|
||||
font-size: 15px;
|
||||
line-height: 19px;
|
||||
color: var(--g0);
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
cursor: pointer;
|
||||
${media.lg} {
|
||||
padding: 22px 45px;
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
&:not(:first-child) {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const NavigationItem = styled.div<{ $active?: boolean }>`
|
||||
padding: 12px 12px 12px 75px;
|
||||
transition:
|
||||
background 0.1s linear,
|
||||
color 0.1s linear;
|
||||
font-size: 16px;
|
||||
line-height: 14.5px;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--g1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
span {
|
||||
font-weight: 400;
|
||||
}
|
||||
> :not(:last-child) {
|
||||
margin-right: 14px;
|
||||
}
|
||||
&:hover,
|
||||
${(props) => (props.$active ? '&' : '&.noop')} {
|
||||
color: var(--g0);
|
||||
text-decoration: underline;
|
||||
}
|
||||
${(props) => (props.$active ? 'span' : '&.noop')} {
|
||||
font-weight: 500;
|
||||
}
|
||||
${media.lg} {
|
||||
padding: 12px 12px 12px 65px;
|
||||
}
|
||||
`;
|
||||
|
||||
const NavigationItemLabel = styled.span`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
line-height: 17.6px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--g1);
|
||||
background: var(--g5);
|
||||
`;
|
||||
|
||||
export interface DocumentationNavigationProps {
|
||||
documentationItems: DocumentationItem[];
|
||||
pathPrefix?: string[];
|
||||
}
|
||||
|
||||
export function DocumentationNavigation({
|
||||
documentationItems,
|
||||
pathPrefix,
|
||||
|
|
@ -23,14 +111,15 @@ export function DocumentationNavigation({
|
|||
const normalized = activePath.replace((pathPrefix || []).join('/'), '');
|
||||
|
||||
return (
|
||||
normalized === item.path ||
|
||||
item.children?.some((child) => {
|
||||
normalized === item.path
|
||||
|| item.children?.some((child) => {
|
||||
return activePath.startsWith(
|
||||
[item.path, child.path].filter(Boolean).join('/'),
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
setExpandedTitles(expandedItems.map((item) => item.title));
|
||||
}, [activePath, pathPrefix, documentationItems]);
|
||||
|
||||
|
|
@ -81,13 +170,15 @@ export function DocumentationNavigation({
|
|||
legacyBehavior
|
||||
key={documentationItem.path}
|
||||
>
|
||||
<NavigationItem as={'a'} $active={activePath === path}>
|
||||
<NavigationItem as="a" $active={activePath === path}>
|
||||
<span>{documentationItem.title}</span>
|
||||
{documentationItem.label ? (
|
||||
<NavigationItemLabel>
|
||||
{documentationItem.label}
|
||||
</NavigationItemLabel>
|
||||
) : null}
|
||||
{documentationItem.label
|
||||
? (
|
||||
<NavigationItemLabel>
|
||||
{documentationItem.label}
|
||||
</NavigationItemLabel>
|
||||
)
|
||||
: null}
|
||||
</NavigationItem>
|
||||
</Link>
|
||||
);
|
||||
|
|
@ -96,87 +187,3 @@ export function DocumentationNavigation({
|
|||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const HeaderItemIcon = styled.div<{ $active?: boolean }>`
|
||||
font-size: 13px;
|
||||
transition: transform 0.25s linear;
|
||||
transform: rotate(${(props) => (props.$active ? 180 : 0)}deg);
|
||||
margin-right: 7px;
|
||||
position: relative;
|
||||
top: 6px;
|
||||
svg {
|
||||
display: block;
|
||||
}
|
||||
${media.lg} {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
const ChildrenContainer = styled.div<{ $expanded?: boolean }>`
|
||||
display: ${(props) => (props.$expanded ? 'block' : 'none')};
|
||||
${media.lg} {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
const HeaderItem = styled.div`
|
||||
padding: 10px 30px;
|
||||
font-size: 15px;
|
||||
line-height: 19px;
|
||||
color: var(--g0);
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
cursor: pointer;
|
||||
${media.lg} {
|
||||
padding: 22px 45px;
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
&:not(:first-child) {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
const NavigationItem = styled.div<{ $active?: boolean }>`
|
||||
padding: 12px 12px 12px 75px;
|
||||
transition:
|
||||
background 0.1s linear,
|
||||
color 0.1s linear;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
line-height: 14.5px;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--g1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
span {
|
||||
font-weight: 500;
|
||||
}
|
||||
> :not(:last-child) {
|
||||
margin-right: 14px;
|
||||
}
|
||||
&:hover,
|
||||
${(props) => (props.$active ? '&' : '&.noop')} {
|
||||
color: var(--g0);
|
||||
text-decoration: underline;
|
||||
}
|
||||
${(props) => (props.$active ? 'span' : '&.noop')} {
|
||||
font-weight: 700;
|
||||
}
|
||||
${media.lg} {
|
||||
padding: 12px 12px 12px 65px;
|
||||
}
|
||||
`;
|
||||
const NavigationItemLabel = styled.span`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
line-height: 17.6px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--g1);
|
||||
background: var(--g5);
|
||||
`;
|
||||
|
|
|
|||
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,17 +1,64 @@
|
|||
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 { Sponsor } from './Sponsor';
|
||||
import { media } from '../lib/responsive';
|
||||
import { CustomizationEditor } from './CustomizationEditor';
|
||||
import { FiltersEditor } from './FiltersEditor';
|
||||
import { Icon, IconList, IconListFilters } from './IconList';
|
||||
import { media } from '../lib/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();
|
||||
|
|
@ -39,8 +86,7 @@ export function Explore({ allIcons }: ExploreProps) {
|
|||
<FilterContainer>
|
||||
<FiltersEditor filters={filters} onChange={setFilters} />
|
||||
</FilterContainer>
|
||||
<Sponsor />
|
||||
<Ad />
|
||||
<Streamline />
|
||||
<CustomizationEditor
|
||||
customizations={customizations}
|
||||
onChange={setCustomizations}
|
||||
|
|
@ -49,46 +95,3 @@ export function Explore({ allIcons }: ExploreProps) {
|
|||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
${media.md} {
|
||||
align-items: flex-start;
|
||||
flex-direction: row;
|
||||
}
|
||||
`;
|
||||
const Left = styled.div`
|
||||
flex: 1;
|
||||
min-height: calc(100vh - 100px);
|
||||
background: white;
|
||||
${media.md} {
|
||||
background: none;
|
||||
}
|
||||
`;
|
||||
const Right = styled.div`
|
||||
position: sticky;
|
||||
top: 50px;
|
||||
width: 275px;
|
||||
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;
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import type { IconListFilters } from './IconList';
|
||||
import React from 'react';
|
||||
import { IconListFilters } from './IconList';
|
||||
import { LargeInput } from './Input';
|
||||
|
||||
export interface FiltersEditorProps {
|
||||
filters: IconListFilters;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
onChange: (filters: IconListFilters) => void;
|
||||
}
|
||||
|
||||
export function FiltersEditor({ filters, onChange }: FiltersEditorProps) {
|
||||
const [, startTransition] = (React as any).useTransition();
|
||||
const [search, setSearch] = React.useState(filters.search);
|
||||
|
|
@ -43,10 +43,10 @@ export function FiltersEditor({ filters, onChange }: FiltersEditorProps) {
|
|||
|
||||
return (
|
||||
<LargeInput
|
||||
placeholder={'Search...'}
|
||||
placeholder="Search..."
|
||||
value={search}
|
||||
type={'search'}
|
||||
autoCapitalize={'none'}
|
||||
type="search"
|
||||
autoCapitalize="none"
|
||||
tabIndex={1}
|
||||
onFocus={(e) => {
|
||||
if (!didScrollRef.current) {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,64 @@ import {
|
|||
import { Logo, LogoContainer, LogoIcon } from './Header';
|
||||
import { Text13, Text17 } from './Typography';
|
||||
|
||||
export interface FooterCategoryProps {
|
||||
const Container = styled.div`
|
||||
display: block;
|
||||
margin-top: 110px;
|
||||
padding-top: 30px;
|
||||
margin-top: 100px;
|
||||
padding: 84px 12%;
|
||||
background-color: var(--g7);
|
||||
align-items: center;
|
||||
> :not(:last-child) {
|
||||
margin-right: 50px;
|
||||
}
|
||||
`;
|
||||
|
||||
const FooterEnd = styled.div`
|
||||
border-top: 1px solid var(--g5);
|
||||
padding-top: 20px;
|
||||
margin-top: 74px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
const FooterCategories = styled.div`
|
||||
width: 100%;
|
||||
margin-top: 54px;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const FooterCategoryContainer = styled.div`
|
||||
width: 28%;
|
||||
margin-right: 20px;
|
||||
`;
|
||||
|
||||
const FooterCategoryTitle = styled(Text17)`
|
||||
&&& {
|
||||
margin-bottom: 24px;
|
||||
font-weight: 600;
|
||||
display: block;
|
||||
color: var(--g0);
|
||||
}
|
||||
`;
|
||||
|
||||
const FooterCategoryLinks = styled.div``;
|
||||
|
||||
const FooterCategoryLink = styled.a`
|
||||
display: block;
|
||||
font-size: 17px;
|
||||
color: var(--g1);
|
||||
width: fit-content;
|
||||
margin-bottom: 12px;
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
color: var(--g0);
|
||||
}
|
||||
`;
|
||||
|
||||
interface FooterCategoryProps {
|
||||
category: string;
|
||||
links: { name: string; url: string }[];
|
||||
}
|
||||
|
|
@ -31,17 +88,19 @@ function FooterCategory({ category, links }: FooterCategoryProps) {
|
|||
}
|
||||
|
||||
export function Footer() {
|
||||
const year = new Date().getFullYear();
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<LogoContainer>
|
||||
<LogoIcon>
|
||||
<PeaceHand />
|
||||
</LogoIcon>
|
||||
<Logo src={'/iconoir-logo.svg'} alt={'Iconoir Logo'} />
|
||||
<Logo src="/iconoir-logo.svg" alt="Iconoir Logo" />
|
||||
</LogoContainer>
|
||||
<FooterCategories>
|
||||
<FooterCategory
|
||||
category={'Project'}
|
||||
category="Project"
|
||||
links={[
|
||||
{ name: 'Our Mission', url: '/support' },
|
||||
{ name: 'Contribute', url: '/docs/contributing' },
|
||||
|
|
@ -52,7 +111,7 @@ export function Footer() {
|
|||
]}
|
||||
/>
|
||||
<FooterCategory
|
||||
category={'Support'}
|
||||
category="Support"
|
||||
links={[
|
||||
{
|
||||
name: 'License',
|
||||
|
|
@ -69,7 +128,7 @@ export function Footer() {
|
|||
]}
|
||||
/>
|
||||
<FooterCategory
|
||||
category={'Developers'}
|
||||
category="Developers"
|
||||
links={[
|
||||
{ name: 'Changelog', url: '/docs/changelog' },
|
||||
{
|
||||
|
|
@ -84,15 +143,19 @@ export function Footer() {
|
|||
</FooterCategories>
|
||||
<FooterEnd>
|
||||
<Text13 style={{ fontWeight: 400 }}>
|
||||
Parts of this content are ©2020-2023 by individual Iconoir
|
||||
contributors. Content available under a{' '}
|
||||
<a href={LICENSE_LINK} target={'_blank'} rel={'noreferrer'}>
|
||||
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={'noreferrer'}>
|
||||
<a href={PRIVACY_LINK} target="_blank" rel="nofollow noreferrer">
|
||||
Privacy
|
||||
</a>
|
||||
</Text13>
|
||||
|
|
@ -100,54 +163,3 @@ export function Footer() {
|
|||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: block;
|
||||
margin-top: 110px;
|
||||
padding-top: 30px;
|
||||
margin-top: 100px;
|
||||
padding: 84px 12%;
|
||||
background-color: var(--g7);
|
||||
align-items: center;
|
||||
> :not(:last-child) {
|
||||
margin-right: 50px;
|
||||
}
|
||||
`;
|
||||
const FooterEnd = styled.div`
|
||||
border-top: 1px solid var(--g5);
|
||||
padding-top: 20px;
|
||||
margin-top: 74px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
const FooterCategories = styled.div`
|
||||
width: 100%;
|
||||
margin-top: 54px;
|
||||
display: flex;
|
||||
`;
|
||||
const FooterCategoryContainer = styled.div`
|
||||
width: 28%;
|
||||
margin-right: 20px;
|
||||
`;
|
||||
const FooterCategoryTitle = styled(Text17)`
|
||||
&&& {
|
||||
margin-bottom: 24px;
|
||||
font-weight: 600;
|
||||
display: block;
|
||||
color: var(--g0);
|
||||
}
|
||||
`;
|
||||
const FooterCategoryLinks = styled.div``;
|
||||
const FooterCategoryLink = styled.a`
|
||||
display: block;
|
||||
font-size: 17px;
|
||||
color: var(--g1);
|
||||
width: fit-content;
|
||||
margin-bottom: 12px;
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
color: var(--g0);
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ export function GA() {
|
|||
<>
|
||||
<Script
|
||||
src="https://www.googletagmanager.com/gtag/js?id=UA-33344001-9"
|
||||
strategy={'afterInteractive'}
|
||||
strategy="afterInteractive"
|
||||
/>
|
||||
<Script id={'google-analytics'} strategy={'afterInteractive'}>
|
||||
<Script id="google-analytics" strategy="afterInteractive">
|
||||
{`
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { Menu, Xmark } from 'iconoir-react';
|
||||
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 { SHARE_LINK } from '../lib/constants';
|
||||
import { DISCORD_LINK, SHARE_LINK } from '../lib/constants';
|
||||
import { media } from '../lib/responsive';
|
||||
import { AnimatedSvg } from './AnimatedSvg';
|
||||
import { ResetButton } from './Button';
|
||||
|
|
@ -11,56 +11,18 @@ import { CurrentVersion } from './CurrentVersion';
|
|||
import { NavigationItem } from './NavigationItem';
|
||||
import { Text15 } from './Typography';
|
||||
|
||||
export interface HeaderProps {
|
||||
currentVersion: string;
|
||||
}
|
||||
export function Header({ currentVersion }: HeaderProps) {
|
||||
const [menuVisible, setMenuVisible] = React.useState(false);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<HeaderLeft>
|
||||
<Link href={'/'}>
|
||||
<LogoContainer>
|
||||
<LogoIcon>
|
||||
<AnimatedSvg />
|
||||
</LogoIcon>
|
||||
<Logo src={'/iconoir-logo.svg'} alt={'Iconoir Logo'} />
|
||||
</LogoContainer>
|
||||
</Link>
|
||||
<CurrentVersion version={currentVersion} />
|
||||
</HeaderLeft>
|
||||
<HeaderCenter>
|
||||
<MobileMenuContainer $visible={menuVisible}>
|
||||
<NavigationItem href={'/'}>Icons</NavigationItem>
|
||||
<NavigationItem href={'/docs/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'}>
|
||||
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'}>
|
||||
Share with <Heart width={'1em'} height={'1em'} /> on{' '}
|
||||
<span>X (Twitter)</span>
|
||||
</a>
|
||||
</Share>
|
||||
<MobileMenuButton onClick={() => setMenuVisible((v) => !v)}>
|
||||
{menuVisible ? <Xmark /> : <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;
|
||||
|
|
@ -74,6 +36,26 @@ export const LogoContainer = styled.div`
|
|||
}
|
||||
`;
|
||||
|
||||
const Banner = styled(Text15)`
|
||||
display: none;
|
||||
${media.lg} {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px 0;
|
||||
background: var(--g5);
|
||||
color: var(--g0);
|
||||
font-weight: 500;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
text-decoration: underline;
|
||||
top: 0;
|
||||
}
|
||||
> * {
|
||||
margin: 0 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
const MobileMenuButton = styled(ResetButton)`
|
||||
&&& {
|
||||
z-index: 101;
|
||||
|
|
@ -91,6 +73,7 @@ const MobileMenuButton = styled(ResetButton)`
|
|||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const MobileMenuContainer = styled.div<{ $visible?: boolean }>`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
|
@ -110,8 +93,8 @@ const MobileMenuContainer = styled.div<{ $visible?: boolean }>`
|
|||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
${(props) =>
|
||||
props.$visible &&
|
||||
css`
|
||||
props.$visible
|
||||
&& css`
|
||||
pointer-events: all;
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
|
|
@ -132,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%;
|
||||
|
|
@ -145,17 +133,20 @@ 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;
|
||||
}
|
||||
`;
|
||||
|
||||
const HeaderRight = styled(HeaderItem)`
|
||||
&&& {
|
||||
justify-content: flex-end;
|
||||
|
|
@ -182,8 +173,8 @@ const Share = styled(Text15)<{ $isMobile?: boolean }>`
|
|||
&&& {
|
||||
display: none;
|
||||
${(props) =>
|
||||
props.$isMobile &&
|
||||
css`
|
||||
props.$isMobile
|
||||
&& css`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 12px 0;
|
||||
|
|
@ -208,3 +199,78 @@ const Share = styled(Text15)<{ $isMobile?: boolean }>`
|
|||
}
|
||||
}
|
||||
`;
|
||||
|
||||
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,16 +1,101 @@
|
|||
import React from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { media } from '../lib/responsive';
|
||||
import { useRef, useEffect } from 'react';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export function HeaderBackground({ children }: HeaderBackgroundProps) {
|
||||
const parallaxRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!parallaxRef.current) return;
|
||||
if (!parallaxRef.current)
|
||||
return;
|
||||
|
||||
const parallaxElements = parallaxRef.current.querySelectorAll(
|
||||
'[data-parallax-factor]',
|
||||
|
|
@ -21,7 +106,7 @@ export function HeaderBackground({ children }: HeaderBackgroundProps) {
|
|||
const y = event.clientY / window.innerHeight;
|
||||
|
||||
parallaxElements.forEach((el) => {
|
||||
const factor = parseFloat(
|
||||
const factor = Number.parseFloat(
|
||||
el.getAttribute('data-parallax-factor') || '1',
|
||||
);
|
||||
|
||||
|
|
@ -49,81 +134,3 @@ export function HeaderBackground({ children }: HeaderBackgroundProps) {
|
|||
</HeaderContainer>
|
||||
);
|
||||
}
|
||||
|
||||
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} {
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export interface HeaderSecondaryProps {
|
||||
children: React.ReactElement;
|
||||
}
|
||||
export function HeaderSecondary({ children }: HeaderSecondaryProps) {
|
||||
return <HeaderContainer>{children}</HeaderContainer>;
|
||||
}
|
||||
|
||||
const HeaderContainer = styled.div`
|
||||
position: relative;
|
||||
width: fit-content;
|
||||
margin: auto;
|
||||
`;
|
||||
|
||||
export interface HeaderSecondaryProps {
|
||||
children: React.ReactElement;
|
||||
}
|
||||
|
||||
export function HeaderSecondary({ children }: HeaderSecondaryProps) {
|
||||
return <HeaderContainer>{children}</HeaderContainer>;
|
||||
}
|
||||
|
|
|
|||
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,9 +1,10 @@
|
|||
import type { Icon as IconType } from './IconList';
|
||||
import * as AllIcons from 'iconoir-react';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { showNotification } from '../lib/showNotification';
|
||||
import { ResetButton } from './Button';
|
||||
import { DEFAULT_CUSTOMIZATIONS, Icon as IconType } from './IconList';
|
||||
import { DEFAULT_CUSTOMIZATIONS } from './IconList';
|
||||
|
||||
const HEADER = '<?xml version="1.0" encoding="UTF-8"?>';
|
||||
|
||||
|
|
@ -13,8 +14,8 @@ function bakeSvg(
|
|||
strokeWidth: string | number,
|
||||
) {
|
||||
return (
|
||||
HEADER +
|
||||
svgString
|
||||
HEADER
|
||||
+ svgString
|
||||
.replace(
|
||||
/stroke="currentColor"/g,
|
||||
`stroke="currentColor" stroke-width="${strokeWidth}"`,
|
||||
|
|
@ -23,106 +24,6 @@ function bakeSvg(
|
|||
);
|
||||
}
|
||||
|
||||
export interface IconProps {
|
||||
iconWidth: number;
|
||||
icon: IconType;
|
||||
}
|
||||
export function Icon({ iconWidth, icon }: IconProps) {
|
||||
const IconComponent = (AllIcons as any)[icon.iconComponentName];
|
||||
const iconContainerRef = React.useRef<HTMLDivElement>(null);
|
||||
const downloadRef = React.useRef<HTMLAnchorElement>(null);
|
||||
const htmlContentsRef = React.useRef<string>('');
|
||||
const iconContext = React.useContext(AllIcons.IconoirContext);
|
||||
const [supportsClipboard, setSupportsClipboard] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
setSupportsClipboard(
|
||||
typeof window !== 'undefined' &&
|
||||
typeof window?.navigator?.clipboard?.writeText !== 'undefined',
|
||||
);
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (iconContainerRef.current) {
|
||||
htmlContentsRef.current = bakeSvg(
|
||||
iconContainerRef.current.innerHTML,
|
||||
iconContext.color || DEFAULT_CUSTOMIZATIONS.hexColor,
|
||||
iconContext.strokeWidth || DEFAULT_CUSTOMIZATIONS.strokeWidth,
|
||||
);
|
||||
}
|
||||
}, [iconContext, supportsClipboard]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const element =
|
||||
downloadRef.current ||
|
||||
(iconContainerRef.current as unknown as HTMLAnchorElement);
|
||||
|
||||
if (element) {
|
||||
element.href = `data:image/svg+xml;base64,${btoa(
|
||||
htmlContentsRef.current,
|
||||
)}`;
|
||||
}
|
||||
}, [iconContext, supportsClipboard]);
|
||||
|
||||
return (
|
||||
<div className={'icon-container'}>
|
||||
<BorderContainer $iconWidth={iconWidth}>
|
||||
<IconContainer
|
||||
ref={iconContainerRef}
|
||||
{...((supportsClipboard
|
||||
? {}
|
||||
: {
|
||||
as: 'a',
|
||||
href: '#',
|
||||
rel: 'noreferrer',
|
||||
download: `${icon.filename}.svg`,
|
||||
}) as any)}
|
||||
>
|
||||
<IconComponent />
|
||||
|
||||
{icon.filename.includes('-solid') ? <IconTag>SOLID</IconTag> : ''}
|
||||
</IconContainer>
|
||||
{supportsClipboard ? (
|
||||
<HoverContainer>
|
||||
<CornerBR />
|
||||
<CornerTR />
|
||||
<CornerBL />
|
||||
<CornerTL />
|
||||
<HoverButton
|
||||
onClick={() => {
|
||||
if (htmlContentsRef.current) {
|
||||
navigator.clipboard
|
||||
.writeText(htmlContentsRef.current)
|
||||
.then(() => {
|
||||
showNotification('SVG code copied!');
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Copy SVG
|
||||
</HoverButton>
|
||||
<HoverButton
|
||||
as={'a'}
|
||||
ref={downloadRef}
|
||||
href={'#'}
|
||||
rel={'noreferrer'}
|
||||
download={`${icon.filename}.svg`}
|
||||
>
|
||||
Download
|
||||
</HoverButton>
|
||||
</HoverContainer>
|
||||
) : null}
|
||||
</BorderContainer>
|
||||
<Subtitle $iconWidth={iconWidth} title={icon.filename}>
|
||||
{icon.filename}
|
||||
</Subtitle>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Overlay = styled.div`
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
|
|
@ -131,23 +32,28 @@ const Overlay = styled.div`
|
|||
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;
|
||||
|
|
@ -161,6 +67,7 @@ const HoverContainer = styled.div`
|
|||
opacity: 0;
|
||||
pointer-events: none;
|
||||
`;
|
||||
|
||||
const HoverButton = styled(ResetButton)`
|
||||
&&& {
|
||||
display: flex;
|
||||
|
|
@ -184,6 +91,7 @@ const HoverButton = styled(ResetButton)`
|
|||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const BorderContainer = styled.div<{ $iconWidth: number }>`
|
||||
width: ${(props) => props.$iconWidth}px;
|
||||
box-sizing: border-box;
|
||||
|
|
@ -200,6 +108,7 @@ const BorderContainer = styled.div<{ $iconWidth: number }>`
|
|||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const IconContainer = styled.div`
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
|
|
@ -207,6 +116,7 @@ const IconContainer = styled.div`
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const IconTag = styled.div`
|
||||
background-color: var(--g6);
|
||||
position: absolute;
|
||||
|
|
@ -219,14 +129,116 @@ const IconTag = styled.div`
|
|||
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-40);
|
||||
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);
|
||||
const downloadRef = React.useRef<HTMLAnchorElement>(null);
|
||||
const htmlContentsRef = React.useRef<string>('');
|
||||
const iconContext = React.useContext(AllIcons.IconoirContext);
|
||||
const [supportsClipboard, setSupportsClipboard] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
setSupportsClipboard(
|
||||
typeof window !== 'undefined'
|
||||
&& typeof window?.navigator?.clipboard?.writeText !== 'undefined',
|
||||
);
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (iconContainerRef.current) {
|
||||
htmlContentsRef.current = bakeSvg(
|
||||
(iconContainerRef.current.firstChild as SVGElement).outerHTML,
|
||||
iconContext.color || DEFAULT_CUSTOMIZATIONS.hexColor,
|
||||
iconContext.strokeWidth || DEFAULT_CUSTOMIZATIONS.strokeWidth,
|
||||
);
|
||||
}
|
||||
}, [iconContext, supportsClipboard]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const element = downloadRef.current || (iconContainerRef.current as unknown as HTMLAnchorElement);
|
||||
|
||||
if (element) {
|
||||
element.href = `data:image/svg+xml;base64,${btoa(
|
||||
htmlContentsRef.current,
|
||||
)}`;
|
||||
}
|
||||
}, [iconContext, supportsClipboard]);
|
||||
|
||||
return (
|
||||
<div className="icon-container">
|
||||
<BorderContainer $iconWidth={iconWidth}>
|
||||
<IconContainer
|
||||
ref={iconContainerRef}
|
||||
{...((supportsClipboard
|
||||
? {}
|
||||
: {
|
||||
as: 'a',
|
||||
href: '#',
|
||||
rel: 'noreferrer',
|
||||
download: `${icon.filename}.svg`,
|
||||
}) as any)}
|
||||
>
|
||||
<IconComponent />
|
||||
|
||||
{icon.filename.includes('-solid') ? <IconTag>SOLID</IconTag> : ''}
|
||||
</IconContainer>
|
||||
{supportsClipboard
|
||||
? (
|
||||
<HoverContainer>
|
||||
<CornerBR />
|
||||
<CornerTR />
|
||||
<CornerBL />
|
||||
<CornerTL />
|
||||
<HoverButton
|
||||
onClick={() => {
|
||||
if (htmlContentsRef.current) {
|
||||
navigator.clipboard
|
||||
.writeText(htmlContentsRef.current)
|
||||
.then(() => {
|
||||
showNotification('SVG code copied!');
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Copy SVG
|
||||
</HoverButton>
|
||||
<HoverButton
|
||||
as="a"
|
||||
ref={downloadRef}
|
||||
href="#"
|
||||
rel="noreferrer"
|
||||
download={`${icon.filename}.svg`}
|
||||
>
|
||||
Download
|
||||
</HoverButton>
|
||||
</HoverContainer>
|
||||
)
|
||||
: null}
|
||||
</BorderContainer>
|
||||
<Subtitle $iconWidth={iconWidth} title={icon.filename}>
|
||||
{icon.filename}
|
||||
</Subtitle>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
import type {
|
||||
ListChildComponentProps,
|
||||
} from 'react-window';
|
||||
import { chunk } from 'lodash';
|
||||
import React from 'react';
|
||||
import {
|
||||
areEqual,
|
||||
ListChildComponentProps,
|
||||
VariableSizeList as List,
|
||||
} from 'react-window';
|
||||
import styled from 'styled-components';
|
||||
import useResizeObserver from 'use-resize-observer';
|
||||
import { CategoryRow } from './CategoryRow';
|
||||
import { ICON_SPACE, ICON_WIDTH } from '../lib/constants';
|
||||
import { CategoryRow } from './CategoryRow';
|
||||
import { IconListEmpty } from './IconListEmpty';
|
||||
import { IconsRow } from './IconsRow';
|
||||
import { ReactWindowScroller } from './ReactWindowScroller';
|
||||
|
|
@ -46,15 +48,17 @@ function filterIcons(allIcons: Icon[], filters: IconListFilters): Icon[] {
|
|||
for (const term of normalSearch.split(' ')) {
|
||||
result = result.filter((icon) => {
|
||||
return (
|
||||
normalizeString(icon.filename).includes(term) ||
|
||||
normalizeString(icon.category).includes(term) ||
|
||||
icon.tags.some((tag) => normalizeString(tag).includes(term))
|
||||
normalizeString(icon.filename).includes(term)
|
||||
|| normalizeString(icon.category).includes(term)
|
||||
|| icon.tags.some((tag) => normalizeString(tag).includes(term))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
} else return allIcons;
|
||||
} else {
|
||||
return allIcons;
|
||||
}
|
||||
}
|
||||
|
||||
interface IconCategoryRow {
|
||||
|
|
@ -77,7 +81,8 @@ function getRowsFromIcons(
|
|||
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);
|
||||
}
|
||||
|
||||
|
|
@ -117,71 +122,8 @@ interface IconListContextValue {
|
|||
iconWidth: number;
|
||||
iconsPerRow: number;
|
||||
}
|
||||
export const IconListContext = React.createContext<
|
||||
IconListContextValue | undefined
|
||||
>(undefined);
|
||||
|
||||
export interface IconListProps {
|
||||
filters: IconListFilters;
|
||||
allIcons: Icon[];
|
||||
}
|
||||
export function IconList({ filters, allIcons }: IconListProps) {
|
||||
const filteredIcons = filterIcons(allIcons, filters);
|
||||
const { ref, width = 400 } = useResizeObserver();
|
||||
const iconsPerRow = width
|
||||
? Math.floor((width + ICON_SPACE) / (ICON_WIDTH + ICON_SPACE))
|
||||
: null;
|
||||
let children = null;
|
||||
const listRef = React.useRef<List<IconRow[]> | null>();
|
||||
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);
|
||||
}
|
||||
}, [iconWidth, height]);
|
||||
|
||||
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;
|
||||
listRef.current = c;
|
||||
}}
|
||||
itemData={iconRows}
|
||||
width={width}
|
||||
outerRef={outerRef}
|
||||
style={style}
|
||||
height={height}
|
||||
itemCount={iconRows.length}
|
||||
onScroll={onScroll}
|
||||
itemSize={(index) => getItemSize(iconRows[index], iconWidth)}
|
||||
>
|
||||
{Row}
|
||||
</List>
|
||||
)}
|
||||
</ReactWindowScroller>
|
||||
</IconListContext.Provider>
|
||||
);
|
||||
} else if (width && filters.search) {
|
||||
return <IconListEmpty searchTerm={filters.search} />;
|
||||
}
|
||||
|
||||
return <Container ref={ref}>{children}</Container>;
|
||||
}
|
||||
const IconListContext = React.createContext<IconListContextValue | undefined>(undefined);
|
||||
|
||||
const Container = styled.div`
|
||||
width: 100%;
|
||||
|
|
@ -213,4 +155,73 @@ const Row = React.memo(
|
|||
},
|
||||
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>(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);
|
||||
}
|
||||
}, [iconWidth, height]);
|
||||
|
||||
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;
|
||||
listRef.current = c;
|
||||
}}
|
||||
itemData={iconRows}
|
||||
width={width}
|
||||
outerRef={outerRef}
|
||||
style={style}
|
||||
height={height}
|
||||
itemCount={iconRows.length}
|
||||
onScroll={onScroll}
|
||||
itemSize={(index) => getItemSize(iconRows[index], iconWidth)}
|
||||
>
|
||||
{Row}
|
||||
</List>
|
||||
)}
|
||||
</ReactWindowScroller>
|
||||
</IconListContext.Provider>
|
||||
);
|
||||
} else if (width && filters.search) {
|
||||
return <IconListEmpty searchTerm={filters.search} />;
|
||||
}
|
||||
|
||||
return <Container ref={ref}>{children}</Container>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,29 +3,6 @@ import styled from 'styled-components';
|
|||
import { SUGGEST_ICON_LINK } from '../lib/constants';
|
||||
import { Text18 } from './Typography';
|
||||
|
||||
export interface IconListEmptyProps {
|
||||
searchTerm: string;
|
||||
}
|
||||
export function IconListEmpty({ searchTerm }: IconListEmptyProps) {
|
||||
return (
|
||||
<Container>
|
||||
<IconContainer>
|
||||
<SpockHandGesture />
|
||||
</IconContainer>
|
||||
<Title>
|
||||
Unfortunately there are no icons for '{searchTerm}'
|
||||
</Title>
|
||||
<Text18 style={{ color: 'var(--black-60)' }}>
|
||||
{"If you can't find the icon, you can make a"}
|
||||
<br />
|
||||
<a href={SUGGEST_ICON_LINK} target={'_blank'} rel={'noreferrer'}>
|
||||
suggestion on GitHub.
|
||||
</a>
|
||||
</Text18>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
margin-top: 90px;
|
||||
display: flex;
|
||||
|
|
@ -33,6 +10,7 @@ const Container = styled.div`
|
|||
flex-direction: column;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const IconContainer = styled.div`
|
||||
svg {
|
||||
width: 60px;
|
||||
|
|
@ -41,6 +19,7 @@ const IconContainer = styled.div`
|
|||
margin-bottom: 65px;
|
||||
color: var(--black);
|
||||
`;
|
||||
|
||||
const Title = styled(Text18)`
|
||||
&&& {
|
||||
font-weight: 700;
|
||||
|
|
@ -48,3 +27,29 @@ const Title = styled(Text18)`
|
|||
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,13 +1,22 @@
|
|||
import type { Icon } from './IconList';
|
||||
import styled from 'styled-components';
|
||||
import { ICON_SPACE } from '../lib/constants';
|
||||
import { Icon as IconC } from './Icon';
|
||||
import { Icon } from './IconList';
|
||||
|
||||
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}>
|
||||
|
|
@ -17,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,7 +19,7 @@ const ResetInput = styled.input`
|
|||
}
|
||||
`;
|
||||
|
||||
export const Input = styled(ResetInput)`
|
||||
const Input = styled(ResetInput)`
|
||||
&&& {
|
||||
min-height: 35px;
|
||||
background: var(--white);
|
||||
|
|
|
|||
|
|
@ -3,19 +3,18 @@ import styled from 'styled-components';
|
|||
import { media } from '../lib/responsive';
|
||||
import { GA } from './GA';
|
||||
|
||||
export interface LayoutProps {}
|
||||
export function Layout({ children }: React.PropsWithChildren<LayoutProps>) {
|
||||
return (
|
||||
<Container>
|
||||
<GA />
|
||||
{children}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
padding: 50px 30px;
|
||||
${media.lg} {
|
||||
padding: 30px 50px 50px 50px;
|
||||
}
|
||||
`;
|
||||
|
||||
export function Layout({ children }: React.PropsWithChildren) {
|
||||
return (
|
||||
<Container>
|
||||
<GA />
|
||||
{children}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { MDXRemote as CoreMDXRemote, MDXRemoteProps } from 'next-mdx-remote';
|
||||
import type { MDXRemoteProps } from 'next-mdx-remote';
|
||||
import { MDXRemote as CoreMDXRemote } from 'next-mdx-remote';
|
||||
import { Table } from './Table';
|
||||
import { Body, Code, InlineCode, H1, H2, H3, Pre, Li } from './Typography';
|
||||
import { Body, Code, H1, H2, H3, InlineCode, Li, Pre } from './Typography';
|
||||
|
||||
export function MDXRemote(props: MDXRemoteProps) {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -5,41 +5,7 @@ import styled from 'styled-components';
|
|||
import { media } from '../lib/responsive';
|
||||
import { Text15 } from './Typography';
|
||||
|
||||
export interface NavigationItemProps {
|
||||
href: string;
|
||||
activeMatch?: string;
|
||||
children: React.ReactElement | string;
|
||||
style?: any;
|
||||
}
|
||||
export function NavigationItem({
|
||||
href,
|
||||
activeMatch,
|
||||
children,
|
||||
style,
|
||||
}: NavigationItemProps) {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<Link href={href} passHref legacyBehavior>
|
||||
<NavigationItemContainer
|
||||
as={'a'}
|
||||
style={style}
|
||||
$text={children.toString()}
|
||||
$isActive={
|
||||
activeMatch
|
||||
? router.asPath.startsWith(activeMatch)
|
||||
: href.slice(1)
|
||||
? router.asPath.slice(1).startsWith(href.slice(1))
|
||||
: router.asPath === href
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</NavigationItemContainer>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export const NavigationItemContainer = styled(Text15)<{
|
||||
const NavigationItemContainer = styled(Text15)<{
|
||||
$text: string;
|
||||
$isActive?: boolean;
|
||||
}>`
|
||||
|
|
@ -109,3 +75,38 @@ export const NavigationItemContainer = styled(Text15)<{
|
|||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export interface NavigationItemProps {
|
||||
href: string;
|
||||
activeMatch?: string;
|
||||
children: React.ReactElement | string;
|
||||
style?: any;
|
||||
}
|
||||
|
||||
export function NavigationItem({
|
||||
href,
|
||||
activeMatch,
|
||||
children,
|
||||
style,
|
||||
}: NavigationItemProps) {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<Link href={href} passHref legacyBehavior>
|
||||
<NavigationItemContainer
|
||||
as="a"
|
||||
style={style}
|
||||
$text={children.toString()}
|
||||
$isActive={
|
||||
activeMatch
|
||||
? router.asPath.startsWith(activeMatch)
|
||||
: href.slice(1)
|
||||
? router.asPath.slice(1).startsWith(href.slice(1))
|
||||
: router.asPath === href
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</NavigationItemContainer>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,110 +1,10 @@
|
|||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { PraiseItem } from './PraiseItem';
|
||||
import { media } from '../lib/responsive';
|
||||
import { PraiseItem } from './PraiseItem';
|
||||
|
||||
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%;
|
||||
margin: 0 -30px;
|
||||
|
|
@ -138,6 +38,7 @@ const Container = styled.div`
|
|||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const Indicator = styled.div`
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
|
|
@ -148,6 +49,7 @@ const Indicator = styled.div`
|
|||
background: var(--black);
|
||||
}
|
||||
`;
|
||||
|
||||
const IndicatorContainer = styled.div`
|
||||
margin: 40px auto 0 auto;
|
||||
display: flex;
|
||||
|
|
@ -160,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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,40 @@ import styled from 'styled-components';
|
|||
import { media } from '../lib/responsive';
|
||||
import { Text14, Text18 } from './Typography';
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-direction: row;
|
||||
flex-shrink: 0;
|
||||
width: calc(100vw - 60px);
|
||||
scroll-snap-align: center;
|
||||
${media.xs} {
|
||||
width: 428px;
|
||||
}
|
||||
`;
|
||||
|
||||
const AuthorImage = styled.img`
|
||||
height: 60px;
|
||||
width: 60px;
|
||||
margin-right: 28px;
|
||||
`;
|
||||
|
||||
const Logo = styled.img`
|
||||
height: 23px;
|
||||
margin-top: 36px;
|
||||
`;
|
||||
|
||||
const Header = styled(Text18)`
|
||||
&&& {
|
||||
font-weight: 700;
|
||||
color: var(--black);
|
||||
}
|
||||
`;
|
||||
|
||||
const Body = styled(Text18)`
|
||||
margin-top: 8px;
|
||||
`;
|
||||
|
||||
export interface PraiseItemProps {
|
||||
name: string;
|
||||
position: string;
|
||||
|
|
@ -12,6 +46,7 @@ export interface PraiseItemProps {
|
|||
logoAlt: string;
|
||||
imageUrl: string;
|
||||
}
|
||||
|
||||
export function PraiseItem({
|
||||
name,
|
||||
position,
|
||||
|
|
@ -28,40 +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;
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@
|
|||
// Modified to remove scrollTo callback to support momentum scroll on iOS. We don't need it
|
||||
// in this implementation anyway.
|
||||
|
||||
import type { GridProps, ListProps } from 'react-window';
|
||||
import { throttle } from 'lodash';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { GridProps, ListProps } from 'react-window';
|
||||
|
||||
function isHtmlElement(
|
||||
element: HTMLElement | typeof window,
|
||||
|
|
@ -16,6 +16,7 @@ interface PositionKey {
|
|||
x: string;
|
||||
y: string;
|
||||
}
|
||||
|
||||
const windowScrollPositionKey: PositionKey = {
|
||||
y: 'pageYOffset',
|
||||
x: 'pageXOffset',
|
||||
|
|
@ -26,19 +27,16 @@ const documentScrollPositionKey: PositionKey = {
|
|||
x: 'scrollLeft',
|
||||
};
|
||||
|
||||
const getScrollPosition = (
|
||||
axis: keyof PositionKey,
|
||||
element?: HTMLElement | null,
|
||||
): number =>
|
||||
// @ts-ignore indexing as string
|
||||
element?.[documentScrollPositionKey[axis] as any] ||
|
||||
// @ts-ignore indexing as string
|
||||
window[windowScrollPositionKey[axis] as any] ||
|
||||
// @ts-ignore indexing as string
|
||||
document.documentElement[documentScrollPositionKey[axis] as any] ||
|
||||
// @ts-ignore indexing as string
|
||||
document.body[documentScrollPositionKey[axis] as any] ||
|
||||
0;
|
||||
function getScrollPosition(axis: keyof PositionKey, element?: HTMLElement | null): number {
|
||||
// @ts-expect-error indexing as string
|
||||
return element?.[documentScrollPositionKey[axis] as any]
|
||||
|| window[windowScrollPositionKey[axis] as any]
|
||||
// @ts-expect-error indexing as string
|
||||
|| document.documentElement[documentScrollPositionKey[axis] as any]
|
||||
// @ts-expect-error indexing as string
|
||||
|| document.body[documentScrollPositionKey[axis] as any]
|
||||
|| 0;
|
||||
}
|
||||
|
||||
interface ChildOpts<Props extends ListProps | GridProps> {
|
||||
ref: React.MutableRefObject<any>;
|
||||
|
|
@ -47,7 +45,6 @@ interface ChildOpts<Props extends ListProps | GridProps> {
|
|||
onScroll: Props['onScroll'];
|
||||
}
|
||||
interface ReactWindowScrollerProps<Props extends ListProps | GridProps> {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
children: (opts: ChildOpts<Props>) => React.ReactElement;
|
||||
throttleTime?: number;
|
||||
isGrid?: boolean;
|
||||
|
|
@ -60,29 +57,30 @@ export function ReactWindowScroller<
|
|||
throttleTime = 10,
|
||||
isGrid = false,
|
||||
}: ReactWindowScrollerProps<Props>) {
|
||||
const ref = useRef<any>();
|
||||
const outerRef = useRef<HTMLElement>();
|
||||
const targetElement =
|
||||
typeof window === 'undefined' ? (undefined as any) : window;
|
||||
const ref = useRef<any>(null);
|
||||
const outerRef = useRef<HTMLElement>(null);
|
||||
const targetElement = typeof window === 'undefined' ? (undefined as any) : window;
|
||||
|
||||
useEffect(() => {
|
||||
const handleWindowScroll = throttle(() => {
|
||||
const rect = outerRef.current?.parentElement?.getBoundingClientRect();
|
||||
const offsetTop =
|
||||
(rect?.top || 0) +
|
||||
(isHtmlElement(targetElement)
|
||||
|
||||
const offsetTop = (rect?.top || 0)
|
||||
+ (isHtmlElement(targetElement)
|
||||
? targetElement.scrollTop
|
||||
: targetElement.scrollY);
|
||||
const offsetLeft =
|
||||
(rect?.left || 0) +
|
||||
(isHtmlElement(targetElement)
|
||||
|
||||
const offsetLeft = (rect?.left || 0)
|
||||
+ (isHtmlElement(targetElement)
|
||||
? targetElement.scrollLeft
|
||||
: targetElement.scrollX);
|
||||
|
||||
const scrollTop = getScrollPosition('y') - offsetTop;
|
||||
const scrollLeft = getScrollPosition('x') - offsetLeft;
|
||||
if (isGrid)
|
||||
ref.current && ref.current!.scrollTo({ scrollLeft, scrollTop });
|
||||
if (!isGrid) ref.current && ref.current!.scrollTo(scrollTop);
|
||||
if (!isGrid)
|
||||
ref.current && ref.current!.scrollTo(scrollTop);
|
||||
}, throttleTime);
|
||||
|
||||
targetElement.addEventListener('scroll', handleWindowScroll);
|
||||
|
|
|
|||
|
|
@ -1,43 +1,9 @@
|
|||
import { ArrowRight } from 'iconoir-react';
|
||||
import styled from 'styled-components';
|
||||
import { DonateContainer, DonateHeader, DonateRight } from '../pages/support';
|
||||
import { GITHUB_TREE_PREFIX } from '../lib/constants';
|
||||
import { Text18 } from './Typography';
|
||||
import { DonateContainer, DonateHeader, DonateRight } from '../pages/support';
|
||||
import { LargeButton } from './Button';
|
||||
|
||||
export interface ReadOnGitHubProps {
|
||||
path: string;
|
||||
resource?: string;
|
||||
}
|
||||
export function ReadOnGitHub({
|
||||
path,
|
||||
resource = 'our documentation',
|
||||
}: ReadOnGitHubProps) {
|
||||
return (
|
||||
<DonateContainer style={{ marginTop: 88 }}>
|
||||
<div>
|
||||
<DonateHeader>Read it on GitHub</DonateHeader>
|
||||
<Text18>
|
||||
If you prefer, you can take a look at {resource} on our GitHub
|
||||
repository.
|
||||
</Text18>
|
||||
</div>
|
||||
<DonateRight>
|
||||
<a
|
||||
href={`${GITHUB_TREE_PREFIX}/${
|
||||
path.startsWith('/') ? path.slice(1) : path
|
||||
}`}
|
||||
target={'_blank'}
|
||||
rel={'noreferrer'}
|
||||
>
|
||||
<DonateIconButton>
|
||||
<ArrowRight />
|
||||
</DonateIconButton>
|
||||
</a>
|
||||
</DonateRight>
|
||||
</DonateContainer>
|
||||
);
|
||||
}
|
||||
import { Text18 } from './Typography';
|
||||
|
||||
export const DonateIconButton = styled(LargeButton)`
|
||||
&&& {
|
||||
|
|
@ -51,3 +17,42 @@ export const DonateIconButton = styled(LargeButton)`
|
|||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export interface ReadOnGitHubProps {
|
||||
path: string;
|
||||
resource?: string;
|
||||
}
|
||||
|
||||
export function ReadOnGitHub({
|
||||
path,
|
||||
resource = 'our documentation',
|
||||
}: ReadOnGitHubProps) {
|
||||
return (
|
||||
<DonateContainer style={{ marginTop: 88 }}>
|
||||
<div>
|
||||
<DonateHeader>Read it on GitHub</DonateHeader>
|
||||
<Text18>
|
||||
If you prefer, you can take a look at
|
||||
{' '}
|
||||
{resource}
|
||||
{' '}
|
||||
on our GitHub
|
||||
repository.
|
||||
</Text18>
|
||||
</div>
|
||||
<DonateRight>
|
||||
<a
|
||||
href={`${GITHUB_TREE_PREFIX}/${
|
||||
path.startsWith('/') ? path.slice(1) : path
|
||||
}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<DonateIconButton>
|
||||
<ArrowRight />
|
||||
</DonateIconButton>
|
||||
</a>
|
||||
</DonateRight>
|
||||
</DonateContainer>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,29 @@
|
|||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
const TITLE_SUFFIX = 'Iconoir | Free Icons';
|
||||
|
||||
export interface SEOProps {
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
export function SEO({ title }: SEOProps) {
|
||||
|
||||
export function SEO({ title, description }: SEOProps) {
|
||||
const { asPath } = useRouter();
|
||||
const pageTitle = title ? `${title} | ${TITLE_SUFFIX}` : TITLE_SUFFIX;
|
||||
const pageDescription = description;
|
||||
|
||||
const pathWithoutQuery = asPath.split(/[?#]/)[0];
|
||||
|
||||
const canonicalUrl = `https://iconoir.com${
|
||||
pathWithoutQuery !== '/' ? pathWithoutQuery : ''
|
||||
}`;
|
||||
|
||||
return (
|
||||
<Head>
|
||||
<title>{pageTitle}</title>
|
||||
<link rel="canonical" href="https://iconoir.com/" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Iconoir is the biggest open source icon library that provides a massive selection of high-quality icons, available for free download.
|
||||
No premium options or email sign-up required, free for real. Icons available in SVG,
|
||||
Font, React, React Native, and Flutter libraries, Figma and Framer."
|
||||
/>
|
||||
<link rel="canonical" href={canonicalUrl} />
|
||||
<meta name="description" content={pageDescription} />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
|
|
@ -29,12 +35,7 @@ export function SEO({ title }: SEOProps) {
|
|||
property="og:image"
|
||||
content="https://iconoir.com/iconoir-brand.png"
|
||||
/>
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Iconoir is the biggest open source icon library that provides a massive selection of high-quality icons, available for free download.
|
||||
No premium options or email sign-up required, free for real. Icons available in SVG,
|
||||
Font, React, React Native, and Flutter libraries, Figma and Framer."
|
||||
/>
|
||||
<meta property="og:description" content={pageDescription} />
|
||||
<meta property="og:image:width" content="1270" />
|
||||
<meta property="og:image:height" content="760" />
|
||||
|
||||
|
|
@ -53,10 +54,7 @@ export function SEO({ title }: SEOProps) {
|
|||
name="twitter:image:alt"
|
||||
content="The biggest open source icon library with tons of free icons."
|
||||
/>
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="The biggest open source icon library with tons of free icons."
|
||||
/>
|
||||
<meta name="twitter:description" content={pageDescription} />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
</Head>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,22 +1,77 @@
|
|||
import type { SliderState } from '@react-stately/slider';
|
||||
import type { SliderProps as ReactSliderProps } from '@react-types/slider';
|
||||
import { useFocusRing } from '@react-aria/focus';
|
||||
import { useNumberFormatter } from '@react-aria/i18n';
|
||||
import { useSlider, useSliderThumb } from '@react-aria/slider';
|
||||
import { mergeProps } from '@react-aria/utils';
|
||||
import { VisuallyHidden } from '@react-aria/visually-hidden';
|
||||
import { SliderState, useSliderState } from '@react-stately/slider';
|
||||
import { SliderProps as ReactSliderProps } from '@react-types/slider';
|
||||
import { useSliderState } from '@react-stately/slider';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Text13 } from './Typography';
|
||||
|
||||
const SliderContainer = styled.div`
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
touch-action: none;
|
||||
`;
|
||||
|
||||
const SliderHeader = styled.div`
|
||||
display: flex;
|
||||
align-self: stretch;
|
||||
`;
|
||||
|
||||
const Output = styled(Text13)`
|
||||
flex: 1 0 auto;
|
||||
text-align: end;
|
||||
margin-bottom: 6px;
|
||||
`;
|
||||
|
||||
const Track = styled.div`
|
||||
position: relative;
|
||||
height: 30px;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const TrackBackground = styled.div`
|
||||
position: absolute;
|
||||
height: 2px;
|
||||
top: 15px;
|
||||
background: var(--black);
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const ThumbContainer = styled.div`
|
||||
position: absolute;
|
||||
transform: translateX(-50%);
|
||||
`;
|
||||
|
||||
const ThumbInner = styled.div`
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: solid 2px var(--black);
|
||||
box-shadow: 0px 3px 0px 0px var(--g0);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
transition: 0.2s;
|
||||
scale: 1.2;
|
||||
}
|
||||
`;
|
||||
|
||||
export interface SliderProps extends ReactSliderProps<number[]> {
|
||||
formatOptions?: Parameters<typeof useNumberFormatter>[0];
|
||||
}
|
||||
|
||||
export function Slider(props: SliderProps) {
|
||||
let trackRef = React.useRef(null);
|
||||
let numberFormatter = useNumberFormatter(props.formatOptions);
|
||||
let state = useSliderState({ ...props, numberFormatter });
|
||||
let { groupProps, trackProps, labelProps, outputProps } = useSlider(
|
||||
const trackRef = React.useRef<HTMLDivElement>(null);
|
||||
const numberFormatter = useNumberFormatter(props.formatOptions);
|
||||
const state = useSliderState({ ...props, numberFormatter });
|
||||
|
||||
const { groupProps, trackProps, labelProps, outputProps } = useSlider(
|
||||
props,
|
||||
state,
|
||||
trackRef,
|
||||
|
|
@ -26,11 +81,11 @@ export function Slider(props: SliderProps) {
|
|||
<SliderContainer {...groupProps}>
|
||||
<SliderHeader>
|
||||
{props.label && (
|
||||
<Text13 as={'label'} {...labelProps}>
|
||||
<Text13 as="label" {...labelProps}>
|
||||
{props.label}
|
||||
</Text13>
|
||||
)}
|
||||
<Output as={'output'} {...outputProps}>
|
||||
<Output as="output" {...outputProps}>
|
||||
{state.getThumbValueLabel(0)}
|
||||
</Output>
|
||||
</SliderHeader>
|
||||
|
|
@ -44,13 +99,14 @@ export function Slider(props: SliderProps) {
|
|||
|
||||
interface ThumbProps {
|
||||
state: SliderState;
|
||||
trackRef: React.RefObject<HTMLElement>;
|
||||
trackRef: React.RefObject<HTMLElement | null>;
|
||||
index: number;
|
||||
}
|
||||
|
||||
function Thumb({ state, trackRef, index }: ThumbProps) {
|
||||
let inputRef = React.useRef(null);
|
||||
let { thumbProps, inputProps } = useSliderThumb(
|
||||
const inputRef = React.useRef(null);
|
||||
|
||||
const { thumbProps, inputProps } = useSliderThumb(
|
||||
{
|
||||
index,
|
||||
trackRef,
|
||||
|
|
@ -59,7 +115,7 @@ function Thumb({ state, trackRef, index }: ThumbProps) {
|
|||
state,
|
||||
);
|
||||
|
||||
let { focusProps, isFocusVisible } = useFocusRing();
|
||||
const { focusProps, isFocusVisible } = useFocusRing();
|
||||
|
||||
return (
|
||||
<ThumbContainer
|
||||
|
|
@ -73,13 +129,13 @@ function Thumb({ state, trackRef, index }: ThumbProps) {
|
|||
backgroundColor: isFocusVisible
|
||||
? 'var(--accent)'
|
||||
: state.isThumbDragging(index)
|
||||
? 'var(--g6)'
|
||||
: 'var(--white)',
|
||||
? 'var(--g6)'
|
||||
: 'var(--white)',
|
||||
scale: isFocusVisible
|
||||
? '1.0'
|
||||
: state.isThumbDragging(index)
|
||||
? '1.3'
|
||||
: '1.0',
|
||||
? '1.3'
|
||||
: '1.0',
|
||||
}}
|
||||
>
|
||||
<VisuallyHidden>
|
||||
|
|
@ -89,49 +145,3 @@ function Thumb({ state, trackRef, index }: ThumbProps) {
|
|||
</ThumbContainer>
|
||||
);
|
||||
}
|
||||
|
||||
const SliderContainer = styled.div`
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
touch-action: none;
|
||||
`;
|
||||
const SliderHeader = styled.div`
|
||||
display: flex;
|
||||
align-self: stretch;
|
||||
`;
|
||||
const Output = styled(Text13)`
|
||||
flex: 1 0 auto;
|
||||
text-align: end;
|
||||
margin-bottom: 6px;
|
||||
`;
|
||||
const Track = styled.div`
|
||||
position: relative;
|
||||
height: 30px;
|
||||
width: 100%;
|
||||
`;
|
||||
const TrackBackground = styled.div`
|
||||
position: absolute;
|
||||
height: 2px;
|
||||
top: 15px;
|
||||
background: var(--black);
|
||||
width: 100%;
|
||||
`;
|
||||
const ThumbContainer = styled.div`
|
||||
position: absolute;
|
||||
transform: translateX(-50%);
|
||||
`;
|
||||
const ThumbInner = styled.div`
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: solid 2px var(--black);
|
||||
box-shadow: 0px 3px 0px 0px var(--g0);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
transition: 0.2s;
|
||||
scale: 1.2;
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -2,35 +2,14 @@ import React from 'react';
|
|||
import styled from 'styled-components';
|
||||
import { media } from '../lib/responsive';
|
||||
|
||||
export function Sponsor() {
|
||||
return (
|
||||
<SponsorContainer>
|
||||
<SponsorText>
|
||||
<SponsorLeft>
|
||||
<SponsorLogo />
|
||||
</SponsorLeft>
|
||||
<SponsorRight>
|
||||
<SponsorTitle>Get 3 months free of Framer with Iconoir.</SponsorTitle>
|
||||
<SponsorDescr>
|
||||
Click the link and use the code “pro-yearly-partner”.
|
||||
</SponsorDescr>
|
||||
</SponsorRight>
|
||||
</SponsorText>
|
||||
<a href="https://www.framer.com?via=iconoir">
|
||||
<SponsorCTA>Get the offer</SponsorCTA>
|
||||
</a>
|
||||
</SponsorContainer>
|
||||
);
|
||||
}
|
||||
|
||||
const SponsorContainer = styled.div`
|
||||
border: 1px solid var(--g7);
|
||||
background-color: var(--g7);
|
||||
border-radius: 10px;
|
||||
width: 96%;
|
||||
width: 88%;
|
||||
font-size: 14px;
|
||||
color: var(--g1);
|
||||
padding: 2%;
|
||||
padding: 6%;
|
||||
margin-top: 24px;
|
||||
& > a {
|
||||
text-decoration: none;
|
||||
|
|
@ -111,3 +90,24 @@ const SponsorCTA = styled.div`
|
|||
color: var(--white);
|
||||
}
|
||||
`;
|
||||
|
||||
export function Sponsor() {
|
||||
return (
|
||||
<SponsorContainer>
|
||||
<SponsorText>
|
||||
<SponsorLeft>
|
||||
<SponsorLogo />
|
||||
</SponsorLeft>
|
||||
<SponsorRight>
|
||||
<SponsorTitle>Get 3 months free of Framer with Iconoir.</SponsorTitle>
|
||||
<SponsorDescr>
|
||||
Click the link and use the code “pro-yearly-partner”.
|
||||
</SponsorDescr>
|
||||
</SponsorRight>
|
||||
</SponsorText>
|
||||
<a href="https://www.framer.com?via=iconoir">
|
||||
<SponsorCTA>Get the offer</SponsorCTA>
|
||||
</a>
|
||||
</SponsorContainer>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,19 +2,6 @@ import styled from 'styled-components';
|
|||
import { media } from '../lib/responsive';
|
||||
import { Text15 } from './Typography';
|
||||
|
||||
export interface StatProps {
|
||||
value: string;
|
||||
description: string;
|
||||
}
|
||||
export function Stat({ value, description }: StatProps) {
|
||||
return (
|
||||
<StatContainer>
|
||||
<StatText>{value}</StatText>
|
||||
<Text15>{description}</Text15>
|
||||
</StatContainer>
|
||||
);
|
||||
}
|
||||
|
||||
const StatText = styled.div`
|
||||
font-size: 38px;
|
||||
font-weight: 700;
|
||||
|
|
@ -27,6 +14,7 @@ const StatText = styled.div`
|
|||
text-stroke: 1.5px;
|
||||
}
|
||||
`;
|
||||
|
||||
const StatContainer = styled.div`
|
||||
text-align: center;
|
||||
width: 45%;
|
||||
|
|
@ -52,3 +40,17 @@ export const StatsContainer = styled.div`
|
|||
margin: 60px auto;
|
||||
}
|
||||
`;
|
||||
|
||||
export interface StatProps {
|
||||
value: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export function Stat({ value, description }: StatProps) {
|
||||
return (
|
||||
<StatContainer>
|
||||
<StatText>{value}</StatText>
|
||||
<Text15>{description}</Text15>
|
||||
</StatContainer>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
112
iconoir.com/components/Streamline.tsx
Normal file
112
iconoir.com/components/Streamline.tsx
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
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``;
|
||||
|
||||
const PromoLogo = styled.div`
|
||||
@keyframes my-animation {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
45% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
55% {
|
||||
transform: scale(1);
|
||||
}
|
||||
90% {
|
||||
transform: scale(1);
|
||||
}
|
||||
95% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
background-image: url('./streamline-ad-logo.png');
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
background-size: 100% 100%;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
margin: 14px auto;
|
||||
|
||||
animation-name: my-animation;
|
||||
animation-duration: 5s;
|
||||
animation-timing-function: ease;
|
||||
animation-iteration-count: infinite;
|
||||
animation-delay: 5s;
|
||||
`;
|
||||
|
||||
const PromoImage = styled.img`
|
||||
width: 70%;
|
||||
`;
|
||||
|
||||
const PromoInfo = styled.div``;
|
||||
|
||||
const PromoTitle = styled.h2`
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin: 0 auto;
|
||||
`;
|
||||
|
||||
const PromoSub = styled.h2`
|
||||
font-size: 14px;
|
||||
margin: 0 auto 30px auto;
|
||||
`;
|
||||
|
||||
const PromoDescription = styled.p`
|
||||
border: 1px solid var(--g6);
|
||||
margin: 6%;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
padding: 10px 0;
|
||||
font-weight: 600;
|
||||
`;
|
||||
|
||||
export function Streamline() {
|
||||
return (
|
||||
<a
|
||||
rel="sponsored"
|
||||
href="https://bit.ly/3SNgpKo"
|
||||
style={{ textDecoration: 'none' }}
|
||||
>
|
||||
<PromoContainer>
|
||||
<PromoContent>
|
||||
<PromoInfo>
|
||||
<PromoLogo />
|
||||
<PromoTitle>Expand Your Icon Collection</PromoTitle>
|
||||
<PromoSub>with Streamline</PromoSub>
|
||||
<PromoImage src="./streamline-ad.png" />
|
||||
<PromoDescription>170,000 Vector Icons</PromoDescription>
|
||||
<SponsorLabel>Our sponsor</SponsorLabel>
|
||||
</PromoInfo>
|
||||
</PromoContent>
|
||||
</PromoContainer>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { ArrowRight } from 'iconoir-react';
|
||||
import { DonateContainer, DonateHeader, DonateRight } from '../pages/support';
|
||||
import { SUGGEST_LIBRARY_LINK } from '../lib/constants';
|
||||
import { DonateContainer, DonateHeader, DonateRight } from '../pages/support';
|
||||
import { DonateIconButton } from './ReadOnGitHub';
|
||||
import { Text18 } from './Typography';
|
||||
|
||||
|
|
@ -16,7 +16,7 @@ export function SuggestLibrary() {
|
|||
</Text18>
|
||||
</div>
|
||||
<DonateRight>
|
||||
<a href={SUGGEST_LIBRARY_LINK} target={'_blank'} rel={'noreferrer'}>
|
||||
<a href={SUGGEST_LIBRARY_LINK} target="_blank" rel="noreferrer">
|
||||
<DonateIconButton>
|
||||
<ArrowRight />
|
||||
</DonateIconButton>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { media } from '../lib/responsive';
|
||||
import { showNotification } from '../lib/showNotification';
|
||||
import { CopyButton } from './Button';
|
||||
import { media } from '../lib/responsive';
|
||||
|
||||
export const Text15 = styled.div`
|
||||
font-size: 15px;
|
||||
|
|
@ -124,14 +124,6 @@ export const Li = styled.li`
|
|||
margin: 4px 0;
|
||||
`;
|
||||
|
||||
export const CodeElement = styled.code`
|
||||
&&& {
|
||||
display: inline-block;
|
||||
padding: 0 4px;
|
||||
color: var(--g0);
|
||||
}
|
||||
`;
|
||||
|
||||
const PreContainer = styled(Code)`
|
||||
&&& {
|
||||
position: relative;
|
||||
|
|
@ -147,6 +139,7 @@ const PreContainer = styled(Code)`
|
|||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const CopyContainer = styled.div`
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
|
|
@ -159,34 +152,36 @@ export function Pre({ children, ...props }: React.PropsWithChildren<any>) {
|
|||
|
||||
React.useEffect(() => {
|
||||
setSupportsClipboard(
|
||||
typeof window !== 'undefined' &&
|
||||
typeof window?.navigator?.clipboard?.writeText !== 'undefined',
|
||||
typeof window !== 'undefined'
|
||||
&& typeof window?.navigator?.clipboard?.writeText !== 'undefined',
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<PreContainer {...props}>
|
||||
<pre ref={containerRef}>{children}</pre>
|
||||
{supportsClipboard ? (
|
||||
<CopyContainer>
|
||||
<CopyButton
|
||||
onClick={() => {
|
||||
if (containerRef.current) {
|
||||
navigator.clipboard
|
||||
.writeText(containerRef.current.innerText)
|
||||
.then(() => {
|
||||
showNotification('Code copied!');
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Copy
|
||||
</CopyButton>
|
||||
</CopyContainer>
|
||||
) : null}
|
||||
{supportsClipboard
|
||||
? (
|
||||
<CopyContainer>
|
||||
<CopyButton
|
||||
onClick={() => {
|
||||
if (containerRef.current?.textContent) {
|
||||
navigator.clipboard
|
||||
.writeText(containerRef.current.textContent)
|
||||
.then(() => {
|
||||
showNotification('Code copied!');
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Copy
|
||||
</CopyButton>
|
||||
</CopyContainer>
|
||||
)
|
||||
: null}
|
||||
</PreContainer>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import type { IconListCustomizations } from './IconList';
|
||||
import React from 'react';
|
||||
import { DEFAULT_CUSTOMIZATIONS, IconListCustomizations } from './IconList';
|
||||
import { DEFAULT_CUSTOMIZATIONS } from './IconList';
|
||||
|
||||
const CUSTOMIZATIONS_KEY = 'iconoir-customize';
|
||||
|
||||
export function useCustomizationPersistence(): [
|
||||
IconListCustomizations,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
(customizations: IconListCustomizations) => void,
|
||||
] {
|
||||
const [customizations, _setCustomizations] = React.useState(
|
||||
|
|
|
|||
|
|
@ -227,6 +227,7 @@ filename,category,tags
|
|||
"chat-plus-in","Communication",
|
||||
"check","Actions",
|
||||
"check-circle","Actions",
|
||||
"check-square","Actions",
|
||||
"chocolate","Food",
|
||||
"chromecast","Devices",
|
||||
"chromecast-active","Devices",
|
||||
|
|
@ -505,7 +506,7 @@ filename,category,tags
|
|||
"fish","Animals",
|
||||
"fishing","Activities",
|
||||
"flare","Shapes",
|
||||
"flash","Photos and Videos",
|
||||
"flash","Photos and Videos","trigger,ray,bolt,lightning",
|
||||
"flash-off","Photos and Videos",
|
||||
"flask","Science",
|
||||
"flip","Design Tools",
|
||||
|
|
@ -544,6 +545,7 @@ filename,category,tags
|
|||
"gas-tank-droplet","Transport","fuel",
|
||||
"gif-format","Photos and Videos",
|
||||
"gift","Other",
|
||||
"git","Git","git",
|
||||
"git-branch","Git","git, github",
|
||||
"git-cherry-pick-commit","Git",
|
||||
"git-commit","Git","git, github",
|
||||
|
|
@ -1330,6 +1332,7 @@ filename,category,tags
|
|||
"wind","Weather","weather,air,fresh",
|
||||
"window-check","System","browser,os",
|
||||
"window-lock","System","browser,os",
|
||||
"window-tabs","System","browser,os,tab,navigation",
|
||||
"window-no-access","System","browser,os",
|
||||
"window-xmark","System","browser,os,error,issue",
|
||||
"windows","System",
|
||||
|
|
@ -1356,3 +1359,26 @@ filename,category,tags
|
|||
"z-square","Typography","coordinate,axis",
|
||||
"zoom-in","Organization",
|
||||
"zoom-out","Organization",
|
||||
"asterisk","Typography","",
|
||||
"dns","Connectivity","",
|
||||
"hashtag","Social","",
|
||||
"peerlist","Social","",
|
||||
"polar-sh","Development","",
|
||||
"x","Social","twitter",
|
||||
"ice-cream","Food","gelato",
|
||||
"mastodon","Social","",
|
||||
"meter-arrow-down-right","Maps","",
|
||||
"rhombus-arrow-right","Maps","",
|
||||
"u-turn-arrow-left","Maps","",
|
||||
"u-turn-arrow-right","Maps","",
|
||||
"calendar-arrow-down","System","",
|
||||
"calendar-arrow-up","System","",
|
||||
"calendar-check","System","",
|
||||
"calendar-rotate","System","",
|
||||
"calendar-xmark","System","",
|
||||
"cube-dots","3D Editor","",
|
||||
"cube-scan","3D Editor","",
|
||||
"droplet-snow-flake-in","Science","defrost",
|
||||
"dots-grid-3x3","Navigation","",
|
||||
"bug","Development","malware",
|
||||
"whatsapp","Social","chat,text,texting,chatting,bubble"
|
||||
|
Can't render this file because it has a wrong number of fields in line 2.
|
|
|
@ -3,14 +3,11 @@ export const REPO = {
|
|||
repo: 'iconoir',
|
||||
} as const;
|
||||
|
||||
export const GITHUB_LINK =
|
||||
`https://github.com/${REPO.owner}/${REPO.repo}` as const;
|
||||
export const GITHUB_LINK = `https://github.com/${REPO.owner}/${REPO.repo}` as const;
|
||||
export const ISSUE_LINK = `${GITHUB_LINK}/issues/new/choose` as const;
|
||||
export const SUGGEST_ICON_LINK =
|
||||
`${GITHUB_LINK}/issues/new?assignees=lucaburgio&labels=icon+request&template=icon_request.md&title=%5BICON%5D` as const;
|
||||
export const SUGGEST_ICON_LINK = `${GITHUB_LINK}/issues/new?assignees=lucaburgio&labels=icon+request&template=icon_request.md&title=%5BICON%5D` as const;
|
||||
export const LICENSE_LINK = `${GITHUB_LINK}/blob/main/LICENSE` as const;
|
||||
export const SUGGEST_LIBRARY_LINK =
|
||||
`${GITHUB_LINK}/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=%5BFEAT%5D` as const;
|
||||
export const SUGGEST_LIBRARY_LINK = `${GITHUB_LINK}/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=%5BFEAT%5D` as const;
|
||||
|
||||
export const GITHUB_TREE_PREFIX = `${GITHUB_LINK}/tree/main` as const;
|
||||
export const LIBRARY_LINKS = {
|
||||
|
|
@ -22,14 +19,11 @@ export const LIBRARY_LINKS = {
|
|||
Figma: 'https://www.figma.com/community/file/983248991460488027/Iconoir-Pack',
|
||||
} as const;
|
||||
|
||||
export const SHARE_LINK =
|
||||
'https://twitter.com/intent/tweet?text=Your%20new%20free%20icons%20library.%20No%20premium%20options%20or%20signups%20by%20%40burgioluca%20&url=https%3A%2F%2Ficonoir.com' as const;
|
||||
export const SUPPORT_LINK =
|
||||
'https://opencollective.com/iconoir/donate?interval=month&amount=10' as const;
|
||||
export const DISCORD_LINK = 'https://discord.gg/c3uzjx6k' as const;
|
||||
export const SHARE_LINK = 'https://twitter.com/intent/tweet?text=Your%20new%20free%20icons%20library.%20No%20premium%20options%20or%20signups%20by%20%40burgioluca%20&url=https%3A%2F%2Ficonoir.com' as const;
|
||||
export const SUPPORT_LINK = 'https://opencollective.com/iconoir/donate?interval=month&amount=10' as const;
|
||||
export const DISCORD_LINK = 'https://discord.gg/txXcKCAmKW' as const;
|
||||
export const FEEDBACK_LINK = 'https://forms.gle/3HvwVYow7D6T8zad7' as const;
|
||||
export const PRIVACY_LINK =
|
||||
'https://www.freeprivacypolicy.com/live/ba00d743-a0cd-44f8-8cb5-6f58911db0fb' as const;
|
||||
export const PRIVACY_LINK = 'https://www.freeprivacypolicy.com/live/ba00d743-a0cd-44f8-8cb5-6f58911db0fb' as const;
|
||||
|
||||
export const AUTHOR_LINKS = {
|
||||
Luca: 'https://twitter.com/burgioluca',
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import fs from 'fs';
|
||||
import fs from 'node:fs';
|
||||
|
||||
export function getHeaderProps() {
|
||||
const packageJson = JSON.parse(fs.readFileSync('../package.json').toString());
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { Icon } from '../components/IconList';
|
||||
import csv from 'csvtojson';
|
||||
import * as AllIcons from 'iconoir-react';
|
||||
import { kebabCase, pascalCase } from 'scule';
|
||||
import { Icon } from '../components/IconList';
|
||||
|
||||
const ICONS_PATH = 'icons.csv';
|
||||
const TAG_SEPARATOR = '|';
|
||||
|
|
@ -19,18 +19,19 @@ export async function getAllIcons(): Promise<Icon[]> {
|
|||
(icon) => icon === iconComponentName || icon === iconComponentSolidName,
|
||||
);
|
||||
|
||||
if (iconComponents.length === 0)
|
||||
if (iconComponents.length === 0) {
|
||||
throw new Error(
|
||||
`Couldn't find icons for ${row.filename} (${iconComponentName}) in 'iconoir-react'.`,
|
||||
);
|
||||
}
|
||||
|
||||
for (const iconComponent of iconComponents) {
|
||||
icons.push({
|
||||
filename: kebabCase(iconComponent),
|
||||
category: row.category,
|
||||
tags:
|
||||
row.tags?.split(TAG_SEPARATOR).map((item: string) => item.trim()) ||
|
||||
[],
|
||||
row.tags?.split(TAG_SEPARATOR).map((item: string) => item.trim())
|
||||
|| [],
|
||||
iconComponentName: iconComponent,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
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