Compare commits

..

No commits in common. "main" and "v7.1.0" have entirely different histories.

324 changed files with 27420 additions and 15582 deletions

3
.eslintignore Normal file
View file

@ -0,0 +1,3 @@
dist/
/examples/react-native/**/*.js
/iconoir.com/out/

46
.eslintrc.cjs Normal file
View file

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

View file

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

View file

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

View file

@ -12,11 +12,11 @@ runs:
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 22
node-version: 20
registry-url: ${{ inputs.node-registry }}
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v2
- 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@v4
uses: actions/cache@v3
with:
path: ${{ steps.pnpm-cache-dir.outputs.dir }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}

View file

@ -22,6 +22,6 @@ jobs:
run: pnpm run build css
- name: Commit changes
uses: stefanzweifel/git-auto-commit-action@v6
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: Update build artifacts

View file

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

View file

@ -41,7 +41,7 @@ jobs:
TAG_NAME: ${{ github.ref_name }}
- name: Commit release
uses: stefanzweifel/git-auto-commit-action@v6
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: Release Version ${{ github.ref_name }}
branch: main

View file

@ -1,5 +1,14 @@
name: Website
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: true
on:
workflow_dispatch:
workflow_run:
@ -8,16 +17,6 @@ on:
types:
- completed
permissions:
actions: read
contents: read
pages: write
id-token: write
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: true
jobs:
build:
name: Build
@ -33,27 +32,27 @@ jobs:
run: pnpm run build react
- name: Build website
run: pnpm run build
run: ./node_modules/.bin/next build
working-directory: iconoir.com
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Setup GitHub Pages
uses: actions/configure-pages@v5
uses: actions/configure-pages@v3
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
uses: actions/upload-pages-artifact@v2
with:
path: ./iconoir.com/out
path: './iconoir.com/out'
deploy:
name: Deploy
needs: build
runs-on: ubuntu-latest
needs: build
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
uses: actions/deploy-pages@v2

View file

@ -1 +0,0 @@
22

1
.prettierignore Normal file
View file

@ -0,0 +1 @@
dist/

4
.prettierrc.json Normal file
View file

@ -0,0 +1,4 @@
{
"singleQuote": true,
"quoteProps": "consistent"
}

View file

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

51
.vscode/settings.json vendored
View file

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

View file

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

View file

@ -2,12 +2,13 @@
[![Version](https://img.shields.io/github/v/release/iconoir-icons/iconoir?style=flat-square)](https://github.com/iconoir-icons/iconoir/releases)
[![Project Stars](https://img.shields.io/github/stars/iconoir-icons/iconoir?style=flat-square)](https://github.com/iconoir-icons/iconoir)
[![React Library](https://img.shields.io/npm/dm/iconoir-react?color=98E8F3&label=react&style=flat-square)](https://www.npmjs.com/package/iconoir-react)
[![License](https://img.shields.io/github/license/iconoir-icons/iconoir?style=flat-square)](https://github.com/iconoir-icons/iconoir/blob/main/LICENSE)
[![Discord](https://img.shields.io/discord/998909400234348615?color=5865f2&label=discord&style=flat-square)](https://discord.gg/txXcKCAmKW)
[![Discord](https://img.shields.io/discord/998909400234348615?color=5865f2&label=Discord&style=flat-square)](https://discord.gg/txXcKCAmKW)
## What is Iconoir?
Iconoir is an open-source library with 1600+ unique SVG icons, designed on a 24x24 pixels grid.
Iconoir is an open-source library with 1300+ unique SVG icons, designed on a 24x24 pixels grid. No premium icons, no email sign-up, no newsletters.
<a href="https://iconoir.com"><strong>Browse at iconoir.com &rarr;</strong></a>
@ -64,7 +65,10 @@ 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:
@ -117,4 +121,4 @@ struct ContentView: View {
## License
MIT License.
[MIT](./LICENSE)

View file

@ -1,10 +1,8 @@
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));
@ -44,7 +42,7 @@ const tasks = new Listr(
{
title: 'Fetching icons',
task: async (ctx) => {
ctx.tasks = { global: { defaultVariant }, icons: {} };
ctx.icons = {};
const iconsVariantsDirs = Object.fromEntries(
iconsVariants.map((variant) => [
@ -73,24 +71,31 @@ const tasks = new Listr(
};
});
ctx.tasks.icons[variant] = icons;
ctx.icons[variant] = icons;
}
ctx.global = { defaultVariant };
},
},
{
title: 'Building targets',
task: (ctx, task) =>
task: (_, task) =>
task.newListr(
Object.entries(targets).map(([targetName, targetConfig]) => ({
title: targetConfig.title,
enabled: () => ctx.cliTargets.length === 0 || ctx.cliTargets.includes(targetName),
task: (ctx) => {
enabled: () =>
cliTargets.length === 0 || cliTargets.includes(targetName),
task: async (ctx) => {
const { default: task } = await import(
`./targets/${targetConfig.target || targetName}/index.js`
);
targetConfig.path = path.join(
rootDir,
...targetConfig.path.split(path.posix.sep),
);
return ctx.pool.run({ targetName, config: ctx.tasks, targetConfig });
return task(ctx, targetConfig);
},
})),
{ concurrent: true, exitOnError: false },
@ -125,15 +130,4 @@ for (const arg of process.argv.slice(2)) {
}
}
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();
await tasks.run();

View file

@ -1,8 +1,7 @@
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}";`;
}

View file

@ -1,15 +1,6 @@
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);
@ -17,8 +8,7 @@ export function getDts(path, content, options) {
const _readFile = host.readFile;
host.readFile = (filename) => {
if (normalize(filename) === path)
return content;
if (filename === path) return content;
return _readFile(filename);
};
@ -26,8 +16,7 @@ export function getDts(path, content, options) {
const dtsFilename = path.replace(/\.(m|c)?(ts|js)x?$/, '.d.$1ts');
host.writeFile = (filename, contents) => {
if (normalize(filename) === dtsFilename)
output = contents;
if (filename === dtsFilename) output = contents;
};
const program = ts.createProgram([path], options, host);
@ -43,7 +32,6 @@ export function getDts(path, content, options) {
diagnostic.file,
diagnostic.start,
);
const message = ts.flattenDiagnosticMessageText(
diagnostic.messageText,
'\n',

View file

@ -1,5 +1,4 @@
import fs from 'node:fs/promises';
import { EOL } from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
@ -19,9 +18,10 @@ 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
.replaceAll(EOL, '')
.replace(/(width|height)="\d+px"/g, '')
.replace(/ +/g, ' ');
.replace(/\n/g, '')
.replace(/(width|height)="[0-9]+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}');}`;

View file

@ -7,7 +7,7 @@ export default async (ctx, target) => {
const outDir = path.join(target.path, 'lib');
const entryContent = ['library;'];
const entryContent = ['library iconoir_flutter;'];
for (const [variant, icons] of Object.entries(ctx.icons)) {
const variantOutDir = path.join(outDir, variant);

View file

@ -1,5 +1,5 @@
function template(name, svg) {
return `import 'package:flutter/widgets.dart' as widgets;
const template = (name, svg) => `
import 'package:flutter/widgets.dart' as widgets;
import 'package:flutter_svg/flutter_svg.dart';
class ${name} extends widgets.StatelessWidget {
@ -7,20 +7,19 @@ class ${name} extends widgets.StatelessWidget {
final double? width;
final double? height;
const ${name}({super.key, this.color, this.width, this.height});
const ${name}({widgets.Key? key, this.color, this.width, this.height})
: super(key: key);
@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;

View file

@ -1,7 +1,7 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import * as svgr from '@svgr/core';
import * as esbuild from 'esbuild';
import fs from 'node:fs/promises';
import path from 'node:path';
import {
generateExport,
generateImport,
@ -36,7 +36,9 @@ 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',
@ -78,7 +80,6 @@ export default async (ctx, target) => {
jsTarget.path,
'IconoirContext.tsx',
);
const iconoirContextDtsPath = path.join(
jsTarget.path,
`IconoirContext.${jsTarget.dtsExt}`,
@ -89,7 +90,6 @@ export default async (ctx, target) => {
iconoirContextDtsPath,
iconoirContext,
jsTarget.module,
target.native,
);
for (const variant of Object.keys(ctx.icons)) {
@ -106,9 +106,10 @@ 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,
@ -151,7 +152,6 @@ export default async (ctx, target) => {
dtsPath,
reactComponent,
jsTarget.module,
target.native,
);
promises.push(iconDts);
@ -174,20 +174,16 @@ export default async (ctx, target) => {
async function getReactComponent(iconPath, native, template) {
const iconContent = await fs.readFile(iconPath, 'utf8');
const options = {
...(native ? nativeSvgrOptions : svgrOptions),
template,
};
const options = native ? nativeSvgrOptions : svgrOptions;
options.template = template;
return svgr.transform(iconContent, options);
}
async function generateDts(inputPath, outputPath, input, module, native) {
async function generateDts(inputPath, outputPath, input, module) {
const dts = getDts(inputPath, await input, {
...defaultTsOptions,
jsx: native ? 'react-native' : 'react',
module,
...(module === 'esnext' && { moduleResolution: 'bundler' }),
});
return fs.writeFile(outputPath, dts);

View file

@ -1,4 +1,4 @@
function template(native) {
const 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;

View file

@ -5,30 +5,31 @@ 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';
const useClientDirective = native ? [] : '"use client"';
if (native) {
variables.props[1].typeAnnotation.typeAnnotation.typeParameters.params[0].typeName.name =
'Svg';
}
const useClientDirective = native ? '' : '"use client";';
const iconoirContextImport = generateImport(
['IconoirContext'],
iconoirContextPath,
);
return tpl`
${useClientDirective};
${variables.imports};
${iconoirContextImport}
${variables.interfaces};
const ${variables.componentName} = (${variables.props}) => {
const context = React.useContext(IconoirContext);
const props = { ...context, ...passedProps };
return ${variables.jsx};
};
${useClientDirective}
${variables.imports};
${iconoirContextImport};
${variables.interfaces};
${variables.exports};
const ${variables.componentName} = (${variables.props}) => {
const context = React.useContext(IconoirContext);
const props = { ...context, ...passedProps };
return ${variables.jsx};
};
${variables.exports};
`;
};
}

View file

@ -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,9 +44,10 @@ 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(
@ -84,9 +85,12 @@ export default async (ctx, target) => {
formats: ['cjs', 'es'],
},
rollupOptions: {
external: ['vue'],
external: ['vue-demi', 'vue'],
},
},
optimizeDeps: {
exclude: ['vue-demi'],
},
plugins: [
vue({
isProduction: true,

View file

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

View file

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

View file

@ -1,11 +1,10 @@
/* eslint-disable no-console */
import { updateYamlKey } from '@atomist/yaml-updater';
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);
@ -22,13 +21,17 @@ 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');
}

View file

@ -3,7 +3,10 @@
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

View file

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

View file

@ -3,12 +3,8 @@
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
.pnp.js
.yarn/install-state.gz
# testing
/coverage
@ -28,10 +24,9 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# local env files
.env*.local
# vercel
.vercel

View file

@ -0,0 +1,8 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
optimizePackageImports: ['iconoir-react'],
},
};
module.exports = nextConfig;

View file

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

View file

@ -2,21 +2,21 @@
"name": "example-next",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint"
"dev": "next dev",
"lint": "next lint",
"start": "next start"
},
"dependencies": {
"next": "15.4.4",
"react": "^19.0.0",
"react-dom": "^19.0.0"
"next": "14.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/node": "^22.10.2",
"@types/react": "^19.0.1",
"@types/react-dom": "^19.0.2",
"@types/node": "^20.8.9",
"@types/react": "^18.2.33",
"@types/react-dom": "^18.2.14",
"iconoir-react": "workspace:*",
"typescript": "^5.7.2"
"typescript": "^5.2.2"
}
}

View file

@ -1,26 +1,26 @@
{
"compilerOptions": {
"incremental": true,
"target": "ES2017",
"jsx": "preserve",
"target": "es5",
"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,
"skipLibCheck": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
]
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]

View file

@ -7,7 +7,6 @@ node_modules/
.expo/
dist/
web-build/
expo-env.d.ts
# Native
*.orig.*

View file

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

View file

@ -0,0 +1,7 @@
module.exports = function (api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
};
};

View file

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

View file

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

17440
examples/react-native/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

@ -26,5 +26,3 @@ coverage
*.njsproj
*.sln
*.sw?
*.tsbuildinfo

View file

@ -2,6 +2,7 @@
<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>

View file

@ -1,27 +1,25 @@
{
"name": "example-vue",
"type": "module",
"private": true,
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build"
"dev": "vite",
"preview": "vite preview",
"type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false"
},
"dependencies": {
"vue": "^3.5.18"
"vue": "^3.3.7"
},
"devDependencies": {
"@iconoir/vue": "workspace:*",
"@tsconfig/node22": "^22.0.2",
"@types/node": "^22.16.5",
"@vitejs/plugin-vue": "^6.0.0",
"@vue/tsconfig": "^0.7.0",
"npm-run-all2": "^8.0.4",
"typescript": "~5.8.3",
"vite": "^7.0.6",
"vite-plugin-vue-devtools": "^7.7.7",
"vue-tsc": "^3.0.4"
"@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"
}
}

View file

@ -1,10 +1,10 @@
<script setup lang="ts">
import {
Check,
Iconoir,
IconoirProvider,
Medal1st,
Medal1stSolid,
IconoirProvider,
Check,
} from '@iconoir/vue';
</script>
@ -21,6 +21,8 @@ import {
'height': '2em',
}"
>
<Check />
<SomeOtherContainer>
<Check />
</SomeOtherContainer>
</IconoirProvider>
</template>

View file

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

View file

@ -1,4 +1,5 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
@ -6,6 +7,5 @@
{
"path": "./tsconfig.app.json"
}
],
"files": []
]
}

View file

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

View file

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

View file

@ -0,0 +1,20 @@
{
"extends": ["next/core-web-vitals"],
"settings": {
"next": {
"rootDir": "iconoir.com"
}
},
"rules": {
"react/no-unescaped-entities": ["off"]
},
"overrides": [
{
"files": ["*.js"],
"parser": "espree",
"parserOptions": {
"ecmaVersion": "latest"
}
}
]
}

View file

@ -1,6 +1,30 @@
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;
@ -30,24 +54,3 @@ const AdContainer = styled.div`
}
}
`;
export function Ad() {
const containerRef = React.useRef<HTMLDivElement>(null);
const addedScript = React.useRef(false);
React.useEffect(() => {
const container = containerRef.current;
if (container && !addedScript.current) {
addedScript.current = true;
const script = document.createElement('script');
script.async = true;
script.type = 'text/javascript';
script.src = '//cdn.carbonads.com/carbon.js?serve=CESDK5QJ&placement=iconoircom';
script.id = '_carbonads_js';
container.appendChild(script);
}
}, []);
return <AdContainer ref={containerRef} />;
}

View file

@ -1,6 +1,7 @@
import anime from 'animejs';
import React from 'react';
// eslint-disable-next-line no-unused-vars
type SetInstances = (instances: anime.AnimeInstance[]) => void;
function playWithLines1(setInstances: SetInstances): anime.AnimeInstance[] {
@ -10,7 +11,7 @@ function playWithLines1(setInstances: SetInstances): anime.AnimeInstance[] {
strokeDashoffset: [anime.setDashoffset, 0],
easing: 'easeInOutSine',
duration: 1500,
delay(_el, i) {
delay: function (el, i) {
return i * 250;
},
direction: 'alternate',
@ -26,7 +27,7 @@ function playWithLines2(setInstances: SetInstances): anime.AnimeInstance[] {
strokeDashoffset: [anime.setDashoffset, 0],
easing: 'easeInOutSine',
duration: 1500,
delay(_el, i) {
delay: function (el, i) {
return i * 250;
},
direction: 'alternate',
@ -42,7 +43,7 @@ function playWithLines3(setInstances: SetInstances): anime.AnimeInstance[] {
strokeDashoffset: [anime.setDashoffset, 0],
easing: 'easeInOutSine',
duration: 1500,
delay(_el, i) {
delay: function (el, i) {
return i * 250;
},
direction: 'alternate',
@ -67,7 +68,7 @@ function playWithLines4(setInstances: SetInstances): anime.AnimeInstance[] {
strokeDashoffset: [anime.setDashoffset, 0],
easing: 'easeInOutSine',
duration: 1500,
delay(_el, i) {
delay: function (el, i) {
return i * 250;
},
direction: 'alternate',

View file

@ -8,12 +8,91 @@ 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;
@ -24,7 +103,6 @@ const MobileHeader = styled(Text14)`
}
}
`;
const DesktopHeader = styled(Text14)`
&&& {
display: none;
@ -33,7 +111,6 @@ const DesktopHeader = styled(Text14)`
}
}
`;
const AvailableForAnimation = keyframes`
5% {
transform: translateX(0);
@ -48,7 +125,6 @@ const AvailableForAnimation = keyframes`
transform: translateX(0);
}
`;
const AvailableForOuter = styled.div`
max-width: 100vw;
margin: 16px -30px 70px -30px;
@ -59,7 +135,6 @@ const AvailableForOuter = styled.div`
padding: 0;
}
`;
const AvailableForContainer = styled.div<{ $contentWidth: number }>`
display: flex;
align-items: center;
@ -67,8 +142,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;
`}
@ -86,7 +161,6 @@ const AvailableForContainer = styled.div<{ $contentWidth: number }>`
}
}
`;
const AvailableForImage = styled.img`
height: 40px;
display: block;
@ -99,107 +173,3 @@ const AvailableForImage = styled.img`
height: 50px;
}
`;
export function AvailableFor() {
const { ref, width } = useResizeObserver();
return (
<>
<MobileHeader>Available For</MobileHeader>
<AvailableForOuter>
<AvailableForContainer $contentWidth={width || 0} ref={ref}>
<DesktopHeader>Available for</DesktopHeader>
<a href={LIBRARY_LINKS.React} target="_blank" rel="noreferrer">
<AvailableForImage
src="/logo-react.svg"
alt="React Logo"
title="React"
/>
</a>
<a
href="https://github.com/iconoir-icons/iconoir#swift-package"
target="_blank"
rel="nofollow noreferrer"
>
<AvailableForImage
src="/logo-swift.svg"
alt="Swift Logo"
title="Swift"
/>
</a>
<a
href={LIBRARY_LINKS.Flutter}
target="_blank"
rel="nofollow noreferrer"
>
<AvailableForImage
src="/logo-flutter.svg"
alt="Flutter Logo"
title="Flutter"
/>
</a>
<a
href={LIBRARY_LINKS.Figma}
target="_blank"
rel="nofollow noreferrer"
>
<AvailableForImage
src="/logo-figma.svg"
alt="Figma Logo"
title="Figma"
/>
</a>
<a
href={LIBRARY_LINKS.ReactNative}
target="_blank"
rel="nofollow noreferrer"
>
<AvailableForImage
src="/logo-react-native.svg"
alt="React Native Logo"
title="React Native"
/>
</a>
<a
href={LIBRARY_LINKS.Vue}
target="_blank"
rel="nofollow noreferrer"
>
<AvailableForImage
src="/logo-vue.svg"
alt="Vue Logo"
title="Vue"
/>
</a>
<a
href={LIBRARY_LINKS.Framer}
target="_blank"
rel="nofollow noreferrer"
>
<AvailableForImage
src="/logo-framer.svg"
alt="Framer Logo"
title="Framer"
/>
</a>
<AreYouUsing>
<a
href={SUGGEST_LIBRARY_LINK}
target="_blank"
rel="nofollow noreferrer"
>
<Text14>More?</Text14>
</a>
<a
href={FEEDBACK_LINK}
target="_blank"
rel="nofollow noreferrer"
>
<Text14>Are you using the library?</Text14>
</a>
</AreYouUsing>
</AvailableForContainer>
</AvailableForOuter>
</>
);
}

View file

@ -50,7 +50,7 @@ export const LargeButton = styled(ResetButton)`
}
`;
const Button = styled(LargeButton)`
export const Button = styled(LargeButton)`
&&&& {
height: 40px;
font-size: 13px;

View file

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

View file

@ -1,15 +1,72 @@
import type { MDXRemoteSerializeResult } from 'next-mdx-remote';
import { BoxIso } from 'iconoir-react';
import moment from 'moment';
import { MDXRemoteSerializeResult } from 'next-mdx-remote';
import React from 'react';
import styled from 'styled-components';
import { media } from '../lib/responsive';
import { CopyButton } from './Button';
import { MDXRemote } from './MDXRemote';
import { media } from '../lib/responsive';
import { Code, Text15, Text18 } from './Typography';
import { CopyButton } from './Button';
const EXPAND_HEIGHT = 400;
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;
@ -22,7 +79,6 @@ const Container = styled.div`
margin: 24px 0;
}
`;
const ContainerLeft = styled.div`
display: flex;
align-items: flex-start;
@ -32,30 +88,25 @@ 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;
@ -84,63 +135,3 @@ 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>
);
}

View file

@ -2,34 +2,33 @@ 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);
}
}
`;

View file

@ -1,62 +1,16 @@
import type { IconListCustomizations } from './IconList';
import React from 'react';
import styled from 'styled-components';
import { media } from '../lib/responsive';
import { DEFAULT_CUSTOMIZATIONS } from './IconList';
import { DEFAULT_CUSTOMIZATIONS, IconListCustomizations } from './IconList';
import { ColorButton, ColorInput } from './Input';
import { Slider } from './Slider';
import { Text13, Text15 } from './Typography';
const CustomizationBox = styled.div`
background-color: var(--g7);
width: 84%;
padding: 8%;
border-radius: 10px;
margin: 24px 0;
display: none;
${media.md} {
display: block;
}
`;
const Header = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 30px;
border-bottom: solid 1px var(--g6);
padding-bottom: 10px;
`;
const Field = styled.div`
margin-bottom: 18px;
`;
const HorizontalField = styled(Field)`
display: flex;
align-items: center;
justify-content: space-between;
`;
const ResetButton = styled(Field)`
&&& {
margin: initial;
text-decoration: underline;
color: var(--dark-gray);
font-size: 13px;
&:hover {
color: var(--black);
cursor: pointer;
}
}
`;
import { media } from '../lib/responsive';
export interface CustomizationEditorProps {
customizations: IconListCustomizations;
// eslint-disable-next-line no-unused-vars
onChange: (customizations: IconListCustomizations) => void;
}
export function CustomizationEditor({
customizations,
onChange,
@ -64,7 +18,6 @@ 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,
);
@ -97,7 +50,7 @@ export function CustomizationEditor({
</Header>
<Field>
<Slider
label="Optical Size"
label={'Optical Size'}
minValue={16}
maxValue={64}
value={[size]}
@ -110,7 +63,7 @@ export function CustomizationEditor({
</Field>
<Field>
<Slider
label="Stroke Weight"
label={'Stroke Weight'}
minValue={0.5}
maxValue={3}
value={[strokeWidth]}
@ -125,7 +78,7 @@ export function CustomizationEditor({
<HorizontalField>
<Text13>Color</Text13>
<ColorInput
type="color"
type={'color'}
value={color}
onChange={(e) => {
setColor(e.target.value);
@ -138,3 +91,44 @@ 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;
}
}
`;

View file

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

View file

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

View file

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

View file

@ -1,12 +1,12 @@
import type { IconListFilters } from './IconList';
import React from 'react';
import { IconListFilters } from './IconList';
import { LargeInput } from './Input';
export interface FiltersEditorProps {
filters: IconListFilters;
// eslint-disable-next-line no-unused-vars
onChange: (filters: IconListFilters) => void;
}
export function FiltersEditor({ filters, onChange }: FiltersEditorProps) {
const [, startTransition] = (React as any).useTransition();
const [search, setSearch] = React.useState(filters.search);
@ -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) {

View file

@ -10,64 +10,7 @@ import {
import { Logo, LogoContainer, LogoIcon } from './Header';
import { Text13, Text17 } from './Typography';
const Container = styled.div`
display: block;
margin-top: 110px;
padding-top: 30px;
margin-top: 100px;
padding: 84px 12%;
background-color: var(--g7);
align-items: center;
> :not(:last-child) {
margin-right: 50px;
}
`;
const FooterEnd = styled.div`
border-top: 1px solid var(--g5);
padding-top: 20px;
margin-top: 74px;
width: 100%;
display: flex;
justify-content: space-between;
`;
const FooterCategories = styled.div`
width: 100%;
margin-top: 54px;
display: flex;
`;
const FooterCategoryContainer = styled.div`
width: 28%;
margin-right: 20px;
`;
const FooterCategoryTitle = styled(Text17)`
&&& {
margin-bottom: 24px;
font-weight: 600;
display: block;
color: var(--g0);
}
`;
const FooterCategoryLinks = styled.div``;
const FooterCategoryLink = styled.a`
display: block;
font-size: 17px;
color: var(--g1);
width: fit-content;
margin-bottom: 12px;
text-decoration: none;
&:hover {
text-decoration: underline;
color: var(--g0);
}
`;
interface FooterCategoryProps {
export interface FooterCategoryProps {
category: string;
links: { name: string; url: string }[];
}
@ -88,19 +31,17 @@ 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' },
@ -111,7 +52,7 @@ export function Footer() {
]}
/>
<FooterCategory
category="Support"
category={'Support'}
links={[
{
name: 'License',
@ -128,7 +69,7 @@ export function Footer() {
]}
/>
<FooterCategory
category="Developers"
category={'Developers'}
links={[
{ name: 'Changelog', url: '/docs/changelog' },
{
@ -143,19 +84,15 @@ export function Footer() {
</FooterCategories>
<FooterEnd>
<Text13 style={{ fontWeight: 400 }}>
Parts of this content are &copy;2020-
{year}
{' '}
by individual Iconoir
contributors. Content available under a
{' '}
<a href={LICENSE_LINK} target="_blank" rel="nofollow noreferrer">
Parts of this content are &copy;2020-2023 by individual Iconoir
contributors. Content available under a{' '}
<a href={LICENSE_LINK} target={'_blank'} rel={'noreferrer'}>
MIT License
</a>
.
</Text13>
<Text13 style={{ fontWeight: 400 }}>
<a href={PRIVACY_LINK} target="_blank" rel="nofollow noreferrer">
<a href={PRIVACY_LINK} target={'_blank'} rel={'noreferrer'}>
Privacy
</a>
</Text13>
@ -163,3 +100,54 @@ 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);
}
`;

View file

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

View file

@ -1,9 +1,9 @@
import { Discord, Menu, Sparks, Xmark } from 'iconoir-react';
import { Menu, Xmark } from 'iconoir-react';
import { Heart } from 'iconoir-react/solid';
import Link from 'next/link';
import React from 'react';
import styled, { css } from 'styled-components';
import { DISCORD_LINK, SHARE_LINK } from '../lib/constants';
import { SHARE_LINK } from '../lib/constants';
import { media } from '../lib/responsive';
import { AnimatedSvg } from './AnimatedSvg';
import { ResetButton } from './Button';
@ -11,18 +11,56 @@ import { CurrentVersion } from './CurrentVersion';
import { NavigationItem } from './NavigationItem';
import { Text15 } from './Typography';
const StyledDiscord = styled(Discord)<{ $isMobile?: boolean }>`
display: none;
${media.lg} {
display: flex;
margin: 0 0 0 16px;
&:hover {
scale: 1.1;
transition: 0.2s;
color: #7289da;
}
}
`;
export interface HeaderProps {
currentVersion: string;
}
export function Header({ currentVersion }: HeaderProps) {
const [menuVisible, setMenuVisible] = React.useState(false);
return (
<Container>
<HeaderLeft>
<Link href={'/'}>
<LogoContainer>
<LogoIcon>
<AnimatedSvg />
</LogoIcon>
<Logo src={'/iconoir-logo.svg'} alt={'Iconoir Logo'} />
</LogoContainer>
</Link>
<CurrentVersion version={currentVersion} />
</HeaderLeft>
<HeaderCenter>
<MobileMenuContainer $visible={menuVisible}>
<NavigationItem href={'/'}>Icons</NavigationItem>
<NavigationItem href={'/docs/introduction'} activeMatch={'/docs'}>
Documentation
</NavigationItem>
<NavigationItem href={'/support'} style={{ marginRight: 0 }}>
Donate &mdash; Our Mission
</NavigationItem>
<Share $isMobile>
<a href={SHARE_LINK} target={'_blank'} rel={'noreferrer'}>
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>
);
}
export const LogoContainer = styled.div`
position: relative;
@ -36,26 +74,6 @@ 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;
@ -73,7 +91,6 @@ const MobileMenuButton = styled(ResetButton)`
}
}
`;
const MobileMenuContainer = styled.div<{ $visible?: boolean }>`
position: absolute;
top: 0;
@ -93,8 +110,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;
@ -115,16 +132,11 @@ const MobileMenuContainer = styled.div<{ $visible?: boolean }>`
}
}
`;
const Container = styled.div`
display: flex;
align-items: center;
justify-content: center;
${media.lg} {
margin-top: 40px;
}
`;
const HeaderItem = styled.div`
flex: 1;
width: 33%;
@ -133,20 +145,17 @@ 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;
@ -173,8 +182,8 @@ const Share = styled(Text15)<{ $isMobile?: boolean }>`
&&& {
display: none;
${(props) =>
props.$isMobile
&& css`
props.$isMobile &&
css`
display: flex;
justify-content: center;
padding: 12px 0;
@ -199,78 +208,3 @@ 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 &mdash; Our Mission
</NavigationItem>
<Share $isMobile>
<a href={SHARE_LINK} target="_blank" rel="noreferrer nofollow">
Share with
{' '}
<Heart width="1em" height="1em" />
{' '}
on
{' '}
<span>X (Twitter)</span>
</a>
</Share>
</MobileMenuContainer>
</HeaderCenter>
<HeaderRight>
<Share>
<a href={SHARE_LINK} target="_blank" rel="noreferrer nofollow">
Share with
{' '}
<Heart width="1em" height="1em" />
{' '}
on
{' '}
<span>X (Twitter)</span>
</a>
</Share>
<a href={DISCORD_LINK} rel="nofollow noreferrer">
<StyledDiscord $isMobile />
</a>
<MobileMenuButton onClick={() => setMenuVisible((v) => !v)}>
{menuVisible ? <Xmark /> : <Menu />}
</MobileMenuButton>
</HeaderRight>
</Container>
);
}

View file

@ -1,101 +1,16 @@
import React, { useEffect, useRef } from 'react';
import React from 'react';
import styled from 'styled-components';
import { media } from '../lib/responsive';
const HeaderContainer = styled.div`
position: relative;
width: fit-content;
margin: auto;
`;
const FloatingIcon = styled.div`
position: absolute;
display: none;
background-repeat: no-repeat;
z-index: -1;
pointer-events: none;
align-items: center;
justify-content: center;
${media.md} {
display: flex;
}
`;
const FloatingIconCellar = styled(FloatingIcon)`
-webkit-transform: rotate(6deg);
-moz-transform: rotate(6deg);
top: -120px;
right: 0px;
width: 200px;
height: 200px;
background-image: url(/cellar.gif);
background-size: 70%;
${media.lg} {
}
`;
const FloatingIconPay = styled(FloatingIcon)`
-webkit-transform: rotate(18deg);
-moz-transform: rotate(18deg);
top: -50px;
right: -100px;
width: 130px;
height: 130px;
background-image: url(/pay-bitcoin.gif);
background-size: 70%;
${media.lg} {
}
`;
const FloatingFaceID = styled(FloatingIcon)`
-webkit-transform: rotate(6deg);
-moz-transform: rotate(6deg);
top: -130px;
right: 380px;
width: 110px;
height: 110px;
background-image: url(/face-id.gif);
background-size: 70%;
${media.lg} {
}
`;
const FloatingCommand = styled(FloatingIcon)`
-webkit-transform: rotate(-7deg);
-moz-transform: rotate(-7deg);
top: -94px;
left: 150px;
width: 110px;
height: 110px;
background-image: url(/command.gif);
background-size: 70%;
${media.lg} {
}
`;
const FloatingFill = styled(FloatingIcon)`
-webkit-transform: rotate(-14deg);
-moz-transform: rotate(-14deg);
top: -64px;
left: -75px;
width: 110px;
height: 110px;
background-image: url(/fill.gif);
background-size: 70%;
${media.lg} {
}
`;
import { useRef, useEffect } from 'react';
export interface HeaderBackgroundProps {
children: React.ReactElement;
}
export function HeaderBackground({ children }: HeaderBackgroundProps) {
const parallaxRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!parallaxRef.current)
return;
if (!parallaxRef.current) return;
const parallaxElements = parallaxRef.current.querySelectorAll(
'[data-parallax-factor]',
@ -106,7 +21,7 @@ export function HeaderBackground({ children }: HeaderBackgroundProps) {
const y = event.clientY / window.innerHeight;
parallaxElements.forEach((el) => {
const factor = Number.parseFloat(
const factor = parseFloat(
el.getAttribute('data-parallax-factor') || '1',
);
@ -134,3 +49,81 @@ 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} {
}
`;

View file

@ -1,16 +1,15 @@
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>;
}

View file

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

View file

@ -1,10 +1,9 @@
import type { Icon as IconType } from './IconList';
import * as AllIcons from 'iconoir-react';
import React from 'react';
import styled from 'styled-components';
import { showNotification } from '../lib/showNotification';
import { ResetButton } from './Button';
import { DEFAULT_CUSTOMIZATIONS } from './IconList';
import { DEFAULT_CUSTOMIZATIONS, Icon as IconType } from './IconList';
const HEADER = '<?xml version="1.0" encoding="UTF-8"?>';
@ -14,8 +13,8 @@ function bakeSvg(
strokeWidth: string | number,
) {
return (
HEADER
+ svgString
HEADER +
svgString
.replace(
/stroke="currentColor"/g,
`stroke="currentColor" stroke-width="${strokeWidth}"`,
@ -24,6 +23,106 @@ 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%;
@ -32,28 +131,23 @@ 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;
@ -67,7 +161,6 @@ const HoverContainer = styled.div`
opacity: 0;
pointer-events: none;
`;
const HoverButton = styled(ResetButton)`
&&& {
display: flex;
@ -91,7 +184,6 @@ const HoverButton = styled(ResetButton)`
}
}
`;
const BorderContainer = styled.div<{ $iconWidth: number }>`
width: ${(props) => props.$iconWidth}px;
box-sizing: border-box;
@ -108,7 +200,6 @@ const BorderContainer = styled.div<{ $iconWidth: number }>`
}
}
`;
const IconContainer = styled.div`
position: absolute;
inset: 0;
@ -116,7 +207,6 @@ const IconContainer = styled.div`
align-items: center;
justify-content: center;
`;
const IconTag = styled.div`
background-color: var(--g6);
position: absolute;
@ -129,116 +219,14 @@ 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-60);
color: var(--black-40);
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>
);
}

View file

@ -1,16 +1,14 @@
import type {
ListChildComponentProps,
} from 'react-window';
import { chunk } from 'lodash';
import React from 'react';
import {
areEqual,
ListChildComponentProps,
VariableSizeList as List,
} from 'react-window';
import styled from 'styled-components';
import useResizeObserver from 'use-resize-observer';
import { ICON_SPACE, ICON_WIDTH } from '../lib/constants';
import { CategoryRow } from './CategoryRow';
import { ICON_SPACE, ICON_WIDTH } from '../lib/constants';
import { IconListEmpty } from './IconListEmpty';
import { IconsRow } from './IconsRow';
import { ReactWindowScroller } from './ReactWindowScroller';
@ -48,17 +46,15 @@ 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 {
@ -81,8 +77,7 @@ 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);
}
@ -122,8 +117,71 @@ interface IconListContextValue {
iconWidth: number;
iconsPerRow: number;
}
export const IconListContext = React.createContext<
IconListContextValue | undefined
>(undefined);
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 Container = styled.div`
width: 100%;
@ -155,73 +213,4 @@ 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>;
}

View file

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

View file

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

View file

@ -19,7 +19,7 @@ const ResetInput = styled.input`
}
`;
const Input = styled(ResetInput)`
export const Input = styled(ResetInput)`
&&& {
min-height: 35px;
background: var(--white);

View file

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

View file

@ -1,7 +1,6 @@
import type { MDXRemoteProps } from 'next-mdx-remote';
import { MDXRemote as CoreMDXRemote } from 'next-mdx-remote';
import { MDXRemote as CoreMDXRemote, MDXRemoteProps } from 'next-mdx-remote';
import { Table } from './Table';
import { Body, Code, H1, H2, H3, InlineCode, Li, Pre } from './Typography';
import { Body, Code, InlineCode, H1, H2, H3, Pre, Li } from './Typography';
export function MDXRemote(props: MDXRemoteProps) {
return (

View file

@ -5,7 +5,41 @@ import styled from 'styled-components';
import { media } from '../lib/responsive';
import { Text15 } from './Typography';
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>
);
}
export const NavigationItemContainer = styled(Text15)<{
$text: string;
$isActive?: boolean;
}>`
@ -75,38 +109,3 @@ 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>
);
}

View file

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

View file

@ -3,40 +3,6 @@ import styled from 'styled-components';
import { media } from '../lib/responsive';
import { Text14, Text18 } from './Typography';
const Container = styled.div`
display: flex;
align-items: flex-start;
flex-direction: row;
flex-shrink: 0;
width: calc(100vw - 60px);
scroll-snap-align: center;
${media.xs} {
width: 428px;
}
`;
const AuthorImage = styled.img`
height: 60px;
width: 60px;
margin-right: 28px;
`;
const Logo = styled.img`
height: 23px;
margin-top: 36px;
`;
const Header = styled(Text18)`
&&& {
font-weight: 700;
color: var(--black);
}
`;
const Body = styled(Text18)`
margin-top: 8px;
`;
export interface PraiseItemProps {
name: string;
position: string;
@ -46,7 +12,6 @@ export interface PraiseItemProps {
logoAlt: string;
imageUrl: string;
}
export function PraiseItem({
name,
position,
@ -63,10 +28,40 @@ 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;
`;

View file

@ -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,7 +16,6 @@ interface PositionKey {
x: string;
y: string;
}
const windowScrollPositionKey: PositionKey = {
y: 'pageYOffset',
x: 'pageXOffset',
@ -27,16 +26,19 @@ const documentScrollPositionKey: PositionKey = {
x: 'scrollLeft',
};
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;
}
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;
interface ChildOpts<Props extends ListProps | GridProps> {
ref: React.MutableRefObject<any>;
@ -45,6 +47,7 @@ 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;
@ -57,30 +60,29 @@ export function ReactWindowScroller<
throttleTime = 10,
isGrid = false,
}: ReactWindowScrollerProps<Props>) {
const ref = useRef<any>(null);
const outerRef = useRef<HTMLElement>(null);
const targetElement = typeof window === 'undefined' ? (undefined as any) : window;
const ref = useRef<any>();
const outerRef = useRef<HTMLElement>();
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);

View file

@ -1,9 +1,43 @@
import { ArrowRight } from 'iconoir-react';
import styled from 'styled-components';
import { GITHUB_TREE_PREFIX } from '../lib/constants';
import { DonateContainer, DonateHeader, DonateRight } from '../pages/support';
import { LargeButton } from './Button';
import { GITHUB_TREE_PREFIX } from '../lib/constants';
import { Text18 } from './Typography';
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>
);
}
export const DonateIconButton = styled(LargeButton)`
&&& {
@ -17,42 +51,3 @@ 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>
);
}

View file

@ -1,5 +1,4 @@
import Head from 'next/head';
import { useRouter } from 'next/router';
const TITLE_SUFFIX = 'Iconoir | Free Icons';
@ -7,22 +6,14 @@ export interface SEOProps {
title?: string;
description?: string;
}
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={canonicalUrl} />
<link rel="canonical" href="https://iconoir.com/" />
<meta name="description" content={pageDescription} />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />

View file

@ -1,77 +1,22 @@
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 { useSliderState } from '@react-stately/slider';
import { SliderState, useSliderState } from '@react-stately/slider';
import { SliderProps as ReactSliderProps } from '@react-types/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) {
const trackRef = React.useRef<HTMLDivElement>(null);
const numberFormatter = useNumberFormatter(props.formatOptions);
const state = useSliderState({ ...props, numberFormatter });
const { groupProps, trackProps, labelProps, outputProps } = useSlider(
let trackRef = React.useRef(null);
let numberFormatter = useNumberFormatter(props.formatOptions);
let state = useSliderState({ ...props, numberFormatter });
let { groupProps, trackProps, labelProps, outputProps } = useSlider(
props,
state,
trackRef,
@ -81,11 +26,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>
@ -99,14 +44,13 @@ export function Slider(props: SliderProps) {
interface ThumbProps {
state: SliderState;
trackRef: React.RefObject<HTMLElement | null>;
trackRef: React.RefObject<HTMLElement>;
index: number;
}
function Thumb({ state, trackRef, index }: ThumbProps) {
const inputRef = React.useRef(null);
const { thumbProps, inputProps } = useSliderThumb(
let inputRef = React.useRef(null);
let { thumbProps, inputProps } = useSliderThumb(
{
index,
trackRef,
@ -115,7 +59,7 @@ function Thumb({ state, trackRef, index }: ThumbProps) {
state,
);
const { focusProps, isFocusVisible } = useFocusRing();
let { focusProps, isFocusVisible } = useFocusRing();
return (
<ThumbContainer
@ -145,3 +89,49 @@ 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;
}
`;

View file

@ -2,14 +2,35 @@ 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: 88%;
width: 96%;
font-size: 14px;
color: var(--g1);
padding: 6%;
padding: 2%;
margin-top: 24px;
& > a {
text-decoration: none;
@ -90,24 +111,3 @@ 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>
);
}

View file

@ -2,6 +2,19 @@ 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;
@ -14,7 +27,6 @@ const StatText = styled.div`
text-stroke: 1.5px;
}
`;
const StatContainer = styled.div`
text-align: center;
width: 45%;
@ -40,17 +52,3 @@ 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>
);
}

View file

@ -1,6 +1,27 @@
import React from 'react';
import styled from 'styled-components';
export function Streamline() {
return (
<a 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>
);
}
// Styled components go here
const PromoContainer = styled.div`
border-radius: 12px;
border: 1px solid var(--g6);
@ -11,7 +32,6 @@ const PromoContainer = styled.div`
background-color: var(--g7);
}
`;
const SponsorLabel = styled.div`
font-size: 12px;
font-weight: 400;
@ -19,7 +39,9 @@ const SponsorLabel = styled.div`
margin: 10px 0;
`;
const PromoContent = styled.div``;
const PromoContent = styled.div`
// Your content styles here, similar to SponsorText
`;
const PromoLogo = styled.div`
@keyframes my-animation {
@ -66,7 +88,9 @@ const PromoImage = styled.img`
width: 70%;
`;
const PromoInfo = styled.div``;
const PromoInfo = styled.div`
// Styles for the text container, similar to SponsorRight
`;
const PromoTitle = styled.h2`
font-size: 16px;
@ -87,26 +111,3 @@ const PromoDescription = styled.p`
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>
);
}

View file

@ -1,6 +1,6 @@
import { ArrowRight } from 'iconoir-react';
import { SUGGEST_LIBRARY_LINK } from '../lib/constants';
import { DonateContainer, DonateHeader, DonateRight } from '../pages/support';
import { SUGGEST_LIBRARY_LINK } from '../lib/constants';
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>

View file

@ -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,6 +124,14 @@ 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;
@ -139,7 +147,6 @@ const PreContainer = styled(Code)`
}
}
`;
const CopyContainer = styled.div`
position: absolute;
top: 16px;
@ -152,36 +159,34 @@ 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?.textContent) {
navigator.clipboard
.writeText(containerRef.current.textContent)
.then(() => {
showNotification('Code copied!');
})
.catch((err) => {
console.error(err);
});
}
}}
>
Copy
</CopyButton>
</CopyContainer>
)
: null}
{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}
</PreContainer>
);
}

View file

@ -1,11 +1,11 @@
import type { IconListCustomizations } from './IconList';
import React from 'react';
import { DEFAULT_CUSTOMIZATIONS } from './IconList';
import { DEFAULT_CUSTOMIZATIONS, IconListCustomizations } 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(

View file

@ -227,7 +227,6 @@ filename,category,tags
"chat-plus-in","Communication",
"check","Actions",
"check-circle","Actions",
"check-square","Actions",
"chocolate","Food",
"chromecast","Devices",
"chromecast-active","Devices",
@ -506,7 +505,7 @@ filename,category,tags
"fish","Animals",
"fishing","Activities",
"flare","Shapes",
"flash","Photos and Videos","trigger,ray,bolt,lightning",
"flash","Photos and Videos",
"flash-off","Photos and Videos",
"flask","Science",
"flip","Design Tools",
@ -545,7 +544,6 @@ 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",
@ -1332,7 +1330,6 @@ 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",
@ -1364,21 +1361,4 @@ filename,category,tags
"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"
"x","Social","twitter",
Can't render this file because it has a wrong number of fields in line 2.

View file

@ -3,11 +3,14 @@ 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 = {
@ -19,11 +22,14 @@ 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/txXcKCAmKW' 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 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',

View file

@ -1,4 +1,4 @@
import fs from 'node:fs';
import fs from 'fs';
export function getHeaderProps() {
const packageJson = JSON.parse(fs.readFileSync('../package.json').toString());

View file

@ -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,19 +19,18 @@ 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