Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d9418a25d | ||
|
|
309f440724 | ||
|
|
05eacffce6 | ||
|
|
41991a82aa | ||
|
|
3cb1625e46 | ||
|
|
ec6d30d82b | ||
|
|
b25472d265 | ||
|
|
b8718e70f6 | ||
|
|
aace268b9b | ||
|
|
e8a2c60b75 | ||
|
|
5a92b9ba26 | ||
|
|
66d8f9fc39 | ||
|
|
e042fec3b5 | ||
|
|
c870b3c54a | ||
|
|
f53e5bfff0 | ||
|
|
f06ac67e33 | ||
|
|
b0b1a4f8ab | ||
|
|
3a37bfa5cd | ||
|
|
16fb55f52c | ||
|
|
951813ce76 | ||
|
|
ba6751ac45 | ||
|
|
99c1037550 | ||
|
|
e32a4a2c10 | ||
|
|
5d9cc1580a | ||
|
|
d9f72b2525 | ||
|
|
0d0408c43b | ||
|
|
55d30bda04 | ||
|
|
f90f10a24a | ||
|
|
f3e36fb0d7 | ||
|
|
4faa47c0b0 | ||
|
|
e593a44ad2 | ||
|
|
dba8fb7932 | ||
|
|
a87cecebe3 | ||
|
|
3b9ea6d087 | ||
|
|
076b52527f | ||
|
|
58ef6830c6 | ||
|
|
f032bed22e | ||
|
|
266fd1c717 | ||
|
|
357a50175e | ||
|
|
a50c54cfff | ||
|
|
bd95db1af8 | ||
|
|
6b474e16d4 | ||
|
|
566032a95d | ||
|
|
042bf77119 | ||
|
|
5b8975c9a3 | ||
|
|
7eb8afe2f8 | ||
|
|
08b2fd2455 |
@@ -13,7 +13,7 @@ The `contributors` property is a required array of GitHub usernames for the peop
|
||||
|
||||
The `tags` property is an array of strings that describe the icon and can be used for searching.
|
||||
Validate the tags against the `icon.schema.json` to ensure they are correctly formatted and adhere to the defined structure.
|
||||
Provide tag suggestions based on the name of the icon and the use cases provided in the PR description. Use the existing tags in the repository as a reference for consistency and to avoid duplicates. Don't suggest words like: 'icon' and preferably use single words. Tags should always be in lowercase, and may also contain spaces (e.g. `magnifying glass`). The name of icon should not be included in the tags, as it is already specified.
|
||||
Provide tag suggestions based on the name of the icon and the use cases provided in the PR description. Use the existing tags in the repository as a reference for consistency and to avoid duplicates. Don't suggest words like: 'icon' and preferably use single words. Tags should always be in lowercase, and may also contain spaces (e.g. `magnifying glass`). Don't include the full icon name, its space-separated form, or any individual part of its kebab-case name because those terms are already searchable. For example, don't suggest `mail-search`, `mail search`, `mail`, or `search` for the `mail-search` icon.
|
||||
|
||||
## Categories
|
||||
|
||||
|
||||
@@ -298,11 +298,11 @@ Link text should describe the destination or concept.
|
||||
|
||||
**Do:**
|
||||
|
||||
> See the [Icon Design Guide](./design-principles) for exact dimensions and spacing requirements.
|
||||
> See the [Icon Design Specification](./specification) for exact dimensions and spacing requirements.
|
||||
|
||||
**Don't:**
|
||||
|
||||
> For more information, [click here](./design-principles).
|
||||
> For more information, [click here](./specification).
|
||||
|
||||
Avoid generic link text such as:
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
name: Docs Checks
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- docs/**
|
||||
- .github/workflows/docs.yml
|
||||
- package.json
|
||||
- pnpm-workspace.yaml
|
||||
- pnpm-lock.yaml
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
# Release metadata is generated by diffing every release tag
|
||||
fetch-depth: 0
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
cache: 'pnpm'
|
||||
node-version-file: 'package.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build docs
|
||||
run: pnpm --filter @lucide/docs build
|
||||
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
# Release metadata is generated by diffing every release tag
|
||||
fetch-depth: 0
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
cache: 'pnpm'
|
||||
node-version-file: 'package.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Generate docs data
|
||||
run: pnpm --filter @lucide/docs run "/^prebuild:.*/"
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm --filter @lucide/docs typecheck
|
||||
@@ -46,3 +46,19 @@ jobs:
|
||||
|
||||
- name: Test
|
||||
run: pnpm --filter @lucide/astro test
|
||||
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version-file: 'package.json'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm --filter @lucide/astro typecheck
|
||||
|
||||
@@ -37,3 +37,19 @@ jobs:
|
||||
with:
|
||||
name: lucide-font
|
||||
path: lucide-font
|
||||
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
cache: 'pnpm'
|
||||
node-version-file: 'package.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm --filter build-font typecheck
|
||||
|
||||
@@ -29,3 +29,19 @@ jobs:
|
||||
|
||||
- name: Build
|
||||
run: pnpm --filter @lucide/lab build
|
||||
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
cache: 'pnpm'
|
||||
node-version-file: 'package.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm --filter @lucide/lab typecheck
|
||||
|
||||
@@ -31,3 +31,19 @@ jobs:
|
||||
|
||||
- name: Test
|
||||
run: pnpm --filter lucide-preact test
|
||||
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
cache: 'pnpm'
|
||||
node-version-file: 'package.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm --filter lucide-preact typecheck
|
||||
|
||||
@@ -47,3 +47,19 @@ jobs:
|
||||
|
||||
- name: Test
|
||||
run: pnpm --filter lucide-react-native test
|
||||
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
cache: 'pnpm'
|
||||
node-version-file: 'package.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm --filter lucide-react-native typecheck
|
||||
|
||||
@@ -70,3 +70,19 @@ jobs:
|
||||
|
||||
- name: Run integration checks
|
||||
run: pnpm test:integrations:react
|
||||
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
cache: 'pnpm'
|
||||
node-version-file: 'package.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm --filter lucide-react typecheck
|
||||
|
||||
@@ -27,4 +27,20 @@ jobs:
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Test
|
||||
run: pnpm --filter lucide-react test
|
||||
run: pnpm --filter @lucide/shared test
|
||||
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
cache: 'pnpm'
|
||||
node-version-file: 'package.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm --filter @lucide/shared typecheck
|
||||
|
||||
@@ -47,3 +47,19 @@ jobs:
|
||||
|
||||
- name: Test
|
||||
run: pnpm --filter lucide-solid test
|
||||
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
cache: 'pnpm'
|
||||
node-version-file: 'package.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm --filter lucide-solid typecheck
|
||||
|
||||
@@ -26,3 +26,19 @@ jobs:
|
||||
|
||||
- name: Build
|
||||
run: pnpm --filter lucide-static build
|
||||
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
cache: 'pnpm'
|
||||
node-version-file: 'package.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm --filter lucide-static typecheck
|
||||
|
||||
@@ -61,5 +61,5 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Test
|
||||
- name: Typecheck
|
||||
run: pnpm --filter @lucide/svelte typecheck
|
||||
|
||||
@@ -10,6 +10,9 @@ on:
|
||||
- packages/shared/**
|
||||
- tools/build-icons/**
|
||||
- tools/rollup-plugins/**
|
||||
- integrations/lucide-vue/**
|
||||
- package.json
|
||||
- pnpm-workspace.yaml
|
||||
- pnpm-lock.yaml
|
||||
|
||||
permissions:
|
||||
@@ -47,3 +50,38 @@ jobs:
|
||||
|
||||
- name: Test
|
||||
run: pnpm --filter @lucide/vue test
|
||||
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
cache: 'pnpm'
|
||||
node-version-file: 'package.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Test
|
||||
run: pnpm --filter @lucide/vue typecheck
|
||||
|
||||
integration:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
cache: 'pnpm'
|
||||
node-version-file: 'package.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Install Chromium
|
||||
run: pnpm --filter @lucide/integration-vue-vite exec playwright install --with-deps chromium
|
||||
|
||||
- name: Run integration checks
|
||||
run: pnpm test:integrations:vue
|
||||
|
||||
@@ -46,3 +46,19 @@ jobs:
|
||||
|
||||
- name: Test
|
||||
run: pnpm --filter lucide test
|
||||
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
cache: 'pnpm'
|
||||
node-version-file: 'package.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm --filter lucide typecheck
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
.DS_Store
|
||||
.idea
|
||||
.next
|
||||
.astro
|
||||
.obsidian
|
||||
.now
|
||||
.cache
|
||||
@@ -18,6 +19,8 @@ stats
|
||||
integrations/**/.react-router
|
||||
integrations/**/.vitest-attachments
|
||||
integrations/**/__screenshots__
|
||||
integrations/**/.nuxt
|
||||
integrations/**/.output
|
||||
outlined
|
||||
lucide-font
|
||||
packages/**/src/icons/*.js
|
||||
|
||||
@@ -25,6 +25,8 @@ integrations/**/next-env.d.ts
|
||||
integrations/**/routeTree.gen.ts
|
||||
integrations/**/.vitest-attachments
|
||||
integrations/**/__screenshots__
|
||||
integrations/**/.nuxt
|
||||
integrations/**/.output
|
||||
|
||||
# lucide-svelte
|
||||
packages/svelte/src/icons/*.svelte
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { eventHandler, setResponseHeader } from 'h3';
|
||||
import iconNodes from '../../data/iconNodes/index.ts';
|
||||
import { IconNodeWithKeys } from '../../theme/types';
|
||||
import iconMetaData from '../../data/iconMetaData';
|
||||
import releaseMeta from '../../data/releaseMetaData.json';
|
||||
import categories from '../../data/categoriesData.json';
|
||||
|
||||
const dataResponse = {
|
||||
icons: Object.entries(iconNodes).reduce((acc, [name, iconNode]) => {
|
||||
const newIconNode = (iconNode as IconNodeWithKeys).map(([name, { key, ...attrs }]) => {
|
||||
const newIconNode = iconNode.map(([name, { key, ...attrs }]) => {
|
||||
return [name, attrs];
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { eventHandler, getQuery, setResponseHeader } from 'h3';
|
||||
import iconNodes from '../../data/iconNodes';
|
||||
import { IconNodeWithKeys } from '../../theme/types';
|
||||
|
||||
export default eventHandler((event) => {
|
||||
const query = getQuery(event);
|
||||
@@ -19,7 +18,7 @@ export default eventHandler((event) => {
|
||||
return [name, iconNode];
|
||||
}
|
||||
|
||||
const newIconNode = (iconNode as IconNodeWithKeys).map(([name, { key, ...attrs }]) => {
|
||||
const newIconNode = iconNode.map(([name, { key, ...attrs }]) => {
|
||||
return [name, attrs];
|
||||
});
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ export default defineConfig({
|
||||
{
|
||||
text: 'Resources',
|
||||
items: [
|
||||
...resourcesSidebar[0].items,
|
||||
...resourcesSidebar[0].items.map(({ text, link }) => ({ text, link })),
|
||||
{ text: 'How to use icons', link: '/how-to/' },
|
||||
{ text: 'Contributing icons', link: '/contribute/icons/' },
|
||||
],
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { SVGProps } from 'react';
|
||||
import { getCommands } from './utils';
|
||||
|
||||
export type Point = { x: number; y: number };
|
||||
@@ -15,8 +14,8 @@ export type Path = {
|
||||
};
|
||||
|
||||
export type PathProps<
|
||||
RequiredProps extends keyof SVGProps<SVGPathElement | SVGRectElement | SVGCircleElement>,
|
||||
NeverProps extends keyof SVGProps<SVGPathElement | SVGRectElement | SVGCircleElement>,
|
||||
RequiredProps extends keyof React.SVGProps<SVGPathElement | SVGRectElement | SVGCircleElement>,
|
||||
NeverProps extends keyof React.SVGProps<SVGPathElement | SVGRectElement | SVGCircleElement>,
|
||||
> = Required<Pick<React.SVGProps<SVGElement & SVGRectElement & SVGCircleElement>, RequiredProps>> &
|
||||
Omit<
|
||||
React.SVGProps<SVGPathElement & SVGRectElement & SVGCircleElement>,
|
||||
|
||||
@@ -53,10 +53,7 @@ export const getCommands = (src: string) =>
|
||||
getNodes(src)
|
||||
.map(convertToPathNode)
|
||||
.flatMap(({ d, name }, idx) =>
|
||||
new SVGPathData(d)
|
||||
.toAbs()
|
||||
// @ts-expect-error `commands` is typed as a union of command types svgson does not narrow
|
||||
.commands.map((c, cIdx) => ({ ...c, id: idx, idx: cIdx, name })),
|
||||
new SVGPathData(d).toAbs().commands.map((c, cIdx) => ({ ...c, id: idx, idx: cIdx, name })),
|
||||
);
|
||||
|
||||
const getPaths = (src: string) => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Category, IconEntity } from '../theme/types';
|
||||
|
||||
const directory = path.join(process.cwd(), '../categories');
|
||||
|
||||
export function getAllCategoryFiles(): Category[] {
|
||||
export function getAllCategoryFiles(): Omit<Category, 'iconCount'>[] {
|
||||
const fileNames = fs.readdirSync(directory).filter((file) => path.extname(file) === '.json');
|
||||
|
||||
return fileNames.map((fileName) => {
|
||||
@@ -21,9 +21,9 @@ export function getAllCategoryFiles(): Category[] {
|
||||
}
|
||||
|
||||
export function mapCategoryIconCount(
|
||||
categories: Category[],
|
||||
categories: Omit<Category, 'iconCount'>[],
|
||||
icons: { categories: IconEntity['categories'] }[],
|
||||
) {
|
||||
): Category[] {
|
||||
return categories.map((category) => ({
|
||||
...category,
|
||||
iconCount: icons.reduce(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createLucideIcon } from 'lucide-react/src/lucide-react';
|
||||
import React from 'react';
|
||||
import createLucideIcon from 'lucide-react/src/createLucideIcon';
|
||||
import { type LucideProps, type IconNode } from 'lucide-react/src/types';
|
||||
import { IconEntity } from '../theme/types';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
|
||||
@@ -20,6 +20,8 @@ export async function getData(name: string) {
|
||||
const iconNode = iconNodes[name];
|
||||
|
||||
const releaseData = releaseMeta?.[name] ?? {
|
||||
fromFork: false,
|
||||
awaitingRelease: false,
|
||||
createdRelease: {
|
||||
version: '0.0.0',
|
||||
date: DATE_OF_FORK,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, isAbsolute, resolve } from 'node:path';
|
||||
import type MarkdownIt from 'markdown-it';
|
||||
import type { RenderRule } from 'markdown-it/lib/renderer.mjs';
|
||||
import type { MarkdownIt, RenderRule } from './types';
|
||||
import container from 'markdown-it-container';
|
||||
|
||||
type ContainerArgs = [typeof container, string, { render: RenderRule }];
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import type MarkdownIt from 'markdown-it';
|
||||
import type { RenderRule } from 'markdown-it/lib/renderer.mjs';
|
||||
import type Token from 'markdown-it/lib/token.mjs';
|
||||
import type { MarkdownIt, RenderRule, Token } from './types';
|
||||
import container from 'markdown-it-container';
|
||||
import sandpackTheme from '../theme/sandpackTheme.json';
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* />
|
||||
*/
|
||||
|
||||
import type MarkdownIt from 'markdown-it';
|
||||
import type { MarkdownIt } from './types';
|
||||
|
||||
type SnackParams = Record<string, string>;
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { MarkdownRenderer } from 'vitepress';
|
||||
|
||||
// VitePress 1.x renders with markdown-it 14, so derive the plugin types from its renderer
|
||||
// instead of the markdown-it 15 package installed in docs.
|
||||
export type MarkdownIt = MarkdownRenderer;
|
||||
export type RenderRule = NonNullable<MarkdownIt['renderer']['rules'][string]>;
|
||||
export type Token = Parameters<RenderRule>[0][number];
|
||||
@@ -60,6 +60,11 @@ export const resourcesSidebar = [
|
||||
link: '/contribute/icons/design-principles',
|
||||
desc: 'Learn the visual principles that shape Lucide icons with side-by-side examples.',
|
||||
},
|
||||
{
|
||||
text: 'Design specification',
|
||||
link: '/contribute/icons/specification',
|
||||
desc: 'Review the technical requirements every Lucide icon must meet.',
|
||||
},
|
||||
{
|
||||
text: 'Naming conventions',
|
||||
link: '/contribute/icons/naming-conventions',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useLocalStorage } from '@vueuse/core';
|
||||
import IconButton from './base/IconButton.vue';
|
||||
import Button from './base/Button.vue';
|
||||
import { x } from '../../data/iconNodes';
|
||||
import Icon from '@lucide/vue/src/Icon';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
@@ -65,16 +65,17 @@ function hidePopup() {
|
||||
}"
|
||||
class="ask-overlay"
|
||||
>
|
||||
<IconButton
|
||||
<Button
|
||||
@click="hidePopup"
|
||||
class="hide-button"
|
||||
>
|
||||
<Icon
|
||||
:iconNode="x"
|
||||
name="x"
|
||||
:size="20"
|
||||
absoluteStrokeWidth
|
||||
/>
|
||||
</IconButton>
|
||||
</Button>
|
||||
|
||||
<h2 class="title">
|
||||
We value your feedback!
|
||||
|
||||
@@ -1,7 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
href?: string;
|
||||
}>()
|
||||
|
||||
const isExternal = computed(() => {
|
||||
return props.href?.startsWith('http') ?? false
|
||||
})
|
||||
|
||||
const component = computed(() => {
|
||||
return props.href ? 'a' : 'button'
|
||||
})
|
||||
|
||||
const target = computed(() => {
|
||||
return isExternal.value ? '_blank' : undefined
|
||||
})
|
||||
|
||||
const rel = computed(() => {
|
||||
return isExternal.value ? 'noreferrer noopener' : undefined
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button v-bind="$attrs" class="icon-button">
|
||||
<component
|
||||
:is="component"
|
||||
v-bind="$attrs"
|
||||
:href="href"
|
||||
:target="target"
|
||||
:rel="rel"
|
||||
class="icon-button"
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@@ -16,12 +47,21 @@
|
||||
transition: color 0.25s, border-color 0.25s, background-color 0.25s;
|
||||
border-radius: 6px;
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
/* width: 56px;
|
||||
height: 56px; */
|
||||
font-size: 24px;
|
||||
gap: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
transition: color 0.1s, border-color 0.1s, background-color 0.1s;
|
||||
}
|
||||
|
||||
.icon-button span {
|
||||
padding: 0 4px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.icon-button:hover {
|
||||
border-color: var(--vp-button-alt-hover-border);
|
||||
color: var(--vp-button-alt-hover-text);
|
||||
@@ -14,7 +14,7 @@ export interface InputProps {
|
||||
import { ref, onMounted, nextTick, watch } from 'vue';
|
||||
import Icon from '@lucide/vue/src/Icon';
|
||||
import { x } from '../../../data/iconNodes';
|
||||
import IconButton from './IconButton.vue';
|
||||
import Button from './Button.vue';
|
||||
|
||||
const props = withDefaults(defineProps<InputProps>(), {
|
||||
type: 'text',
|
||||
@@ -68,9 +68,9 @@ defineExpose({
|
||||
ref="input"
|
||||
:value="modelValue"
|
||||
v-bind="$attrs"
|
||||
@input="$emit('update:modelValue', $event.target.value)"
|
||||
@input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
<IconButton
|
||||
<Button
|
||||
@click="onClear"
|
||||
v-if="type === 'search' && modelValue"
|
||||
class="clear-button"
|
||||
@@ -79,9 +79,10 @@ defineExpose({
|
||||
>
|
||||
<Icon
|
||||
:iconNode="x"
|
||||
name="x"
|
||||
:size="20"
|
||||
/>
|
||||
</IconButton>
|
||||
</Button>
|
||||
<kbd
|
||||
v-if="shortcut"
|
||||
class="shortcut"
|
||||
|
||||
@@ -46,6 +46,7 @@ const value = computed({
|
||||
<template #icon>
|
||||
<Icon
|
||||
:iconNode="search"
|
||||
name="search"
|
||||
class="search-icon"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -9,7 +9,8 @@ import RocketDuotoneIcon from '../home/RocketDuotoneIcon.ts';
|
||||
<p>
|
||||
<Icon
|
||||
:iconNode="RocketDuotoneIcon"
|
||||
size="18"
|
||||
name="rocket-duotone"
|
||||
:size="18"
|
||||
class="rocket-icon"
|
||||
/>
|
||||
<VPLink
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
<script setup lang="ts">
|
||||
import { TabGroup, TabList, Tab, TabPanels, TabPanel } from '@headlessui/vue'
|
||||
|
||||
const props = defineProps<{
|
||||
tabs: string[];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TabGroup>
|
||||
<TabList class="tab-list">
|
||||
<template v-for="(tab, index) in props.tabs" :key="index">
|
||||
<Tab :id="`tab-${index}`" class="tab">{{ tab }}</Tab>
|
||||
</template>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<template v-for="(_, name) in $slots">
|
||||
<TabPanel>
|
||||
<slot :name="name" />
|
||||
</TabPanel>
|
||||
</template>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.tab-list {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 32px;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tab {
|
||||
display: block;
|
||||
line-height: 32px;
|
||||
font-size: 16px;
|
||||
padding: 10px 12px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-2);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
transition: color 0.5s;
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
|
||||
.tab[aria-selected="true"] {
|
||||
color: var(--vp-c-text-1);
|
||||
transition: color 0.25s;
|
||||
border-bottom: 2px solid var(--vp-c-brand);
|
||||
}
|
||||
|
||||
@media (min-width: 860px) {
|
||||
.tab-list {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.tab {
|
||||
font-size: 18px;
|
||||
padding: 10px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.tab {
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1152px) {
|
||||
.tab {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -34,7 +34,7 @@ const percentage = computed<string>(() => `${((Number(props.modelValue) - props.
|
||||
type="range"
|
||||
v-bind="$attrs"
|
||||
v-bind:value="modelValue"
|
||||
v-on:input="$emit('update:modelValue', Number($event.target.value))"
|
||||
v-on:input="$emit('update:modelValue', Number(($event.target as HTMLInputElement).value))"
|
||||
:min="min"
|
||||
:max="max"
|
||||
:step="step"
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { rotateCw } from '../../../data/iconNodes';
|
||||
import Icon from '@lucide/vue/src/Icon';
|
||||
import IconButton from './IconButton.vue';
|
||||
import Button from './Button.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<IconButton class="reset-button">
|
||||
<Button class="reset-button">
|
||||
<Icon
|
||||
:size="20"
|
||||
:iconNode="rotateCw"
|
||||
name="rotate-cw"
|
||||
/>
|
||||
</IconButton>
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -27,6 +27,7 @@ const selected = defineModel<{ name: string; value: string; icon?: string; iconD
|
||||
<slot name="end-icon">
|
||||
<Icon
|
||||
:iconNode="chevronsUpDown"
|
||||
name="chevrons-up-down"
|
||||
class="chevron-icon"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
@@ -57,6 +58,7 @@ const selected = defineModel<{ name: string; value: string; icon?: string; iconD
|
||||
>
|
||||
<Icon
|
||||
:iconNode="check"
|
||||
name="check"
|
||||
class="check"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { type FunctionalComponent } from 'vue';
|
||||
import { type LucideProps } from '@lucide/vue';
|
||||
import { type LucideProps } from '@lucide/vue/src/types';
|
||||
import { Sandpack } from 'sandpack-vue3';
|
||||
|
||||
defineProps<FunctionalComponent<LucideProps>>();
|
||||
|
||||
@@ -10,7 +10,14 @@ import { useLocalStorage } from '@vueuse/core';
|
||||
const { page } = useData();
|
||||
const router = useRouter();
|
||||
|
||||
const frameworks = [
|
||||
interface Framework {
|
||||
name: string;
|
||||
icon: string;
|
||||
iconDark?: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const frameworks: Framework[] = [
|
||||
{ name: 'Vanilla', icon: '/framework-logos/js.svg', value: '/guide/lucide/' },
|
||||
{ name: 'React', icon: '/framework-logos/react.svg', value: '/guide/react/' },
|
||||
{ name: 'Vue', icon: '/framework-logos/vue.svg', value: '/guide/vue/' },
|
||||
@@ -48,7 +55,7 @@ const selected = computed(() => {
|
||||
return current || (fallbackFramework.value?.value ? fallbackFramework.value : frameworks[0]);
|
||||
});
|
||||
|
||||
function onSelectFramework(item: { name: string; icon: string; iconDark?: string; value: string }) {
|
||||
function onSelectFramework(item: Framework) {
|
||||
fallbackFramework.value = item;
|
||||
if (item.value !== router.route.path) {
|
||||
const likeRoute = router.route.path.replace(selected.value.value, item.value);
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
import { ref, shallowRef } from 'vue';
|
||||
import { data } from './HomeHeroIconsCard.data';
|
||||
import { useRouter } from 'vitepress';
|
||||
import { motion, Variants, useScroll, useTransform } from 'motion-v';
|
||||
import { motion, useScroll, useTransform, type Options as MotionOptions } from 'motion-v';
|
||||
import LucideIcon from '../base/LucideIcon.vue';
|
||||
|
||||
// motion-v re-exports framer-motion's `Variants`, which doesn't match the `variants` prop of its components
|
||||
type Variants = MotionOptions['variants'];
|
||||
|
||||
const emit = defineEmits(['animation-complete']);
|
||||
|
||||
const MotionLucideIcon = motion.create(LucideIcon);
|
||||
@@ -129,7 +132,7 @@ const onAnimationComplete = (item) => {
|
||||
|
||||
const randomIndex = ref(Math.floor(Math.random() * 64));
|
||||
|
||||
const iconAnimationVariants = {
|
||||
const iconAnimationVariants: Variants = {
|
||||
initial: {
|
||||
animationName: 'end',
|
||||
opacity: 0,
|
||||
@@ -149,7 +152,7 @@ const iconAnimationVariants = {
|
||||
|
||||
const shrinkIconAnimation = ref('initial');
|
||||
|
||||
const shrinkIconVariants = {
|
||||
const shrinkIconVariants: Variants = {
|
||||
initial: { strokeWidth: 2 },
|
||||
shrinkIcons: (index) => ({
|
||||
animationName: 'shrinkIcons',
|
||||
|
||||
@@ -69,7 +69,7 @@ const pickIcons = [
|
||||
'stethoscope',
|
||||
];
|
||||
|
||||
export default {
|
||||
const loader = {
|
||||
async load() {
|
||||
const icons = Object.entries(iconNodes).map(([name, iconNode]) => ({ name, iconNode }));
|
||||
|
||||
@@ -96,3 +96,8 @@ export default {
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
declare const data: Awaited<ReturnType<typeof loader.load>>;
|
||||
|
||||
export { data };
|
||||
export default loader;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export default {
|
||||
const loader = {
|
||||
async load() {
|
||||
const version = await fetch('https://api.github.com/repos/lucide-icons/lucide/releases/latest')
|
||||
.then((res) => {
|
||||
@@ -16,3 +16,8 @@ export default {
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
declare const data: Awaited<ReturnType<typeof loader.load>>;
|
||||
|
||||
export { data };
|
||||
export default loader;
|
||||
|
||||
@@ -15,10 +15,11 @@ import RocketDuotoneIcon from './RocketDuotoneIcon.ts';
|
||||
class="badge-special confetti-button animate"
|
||||
href="/guide/version-1"
|
||||
>
|
||||
<Icon :iconNode="RocketDuotoneIcon" />
|
||||
<Icon :iconNode="RocketDuotoneIcon" name="rocket-duotone" />
|
||||
Version 1
|
||||
<Icon
|
||||
:iconNode="moveRight"
|
||||
name="move-right"
|
||||
class=""
|
||||
/>
|
||||
</Badge>
|
||||
|
||||
@@ -41,8 +41,8 @@ const sizeCssVar = useCssVar(
|
||||
)
|
||||
|
||||
syncRef(color, colorCssVar)
|
||||
syncRef(strokeWidth, strokeWidthCssVar)
|
||||
syncRef(size, sizeCssVar)
|
||||
syncRef(strokeWidth, strokeWidthCssVar, { transform: { ltr: String, rtl: Number } })
|
||||
syncRef(size, sizeCssVar, { transform: { ltr: String, rtl: Number } })
|
||||
|
||||
function resetStyle () {
|
||||
color.value = 'currentColor'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export default {
|
||||
const loader = {
|
||||
async load() {
|
||||
return {
|
||||
packages: [
|
||||
@@ -61,3 +61,8 @@ export default {
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
declare const data: Awaited<ReturnType<typeof loader.load>>;
|
||||
|
||||
export { data };
|
||||
export default loader;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
headingLevel: 1 | 2 | 3 | 4 | 5 | 6,
|
||||
headingLevel?: 1 | 2 | 3 | 4 | 5 | 6,
|
||||
}>()
|
||||
|
||||
const headingElement = computed(() => `h${props.headingLevel ?? 2}`)
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
const RocketDuotoneIcon = [
|
||||
import type { LucideIconNode } from '@lucide/vue/src/types';
|
||||
|
||||
const RocketDuotoneIcon: LucideIconNode[] = [
|
||||
[
|
||||
'path',
|
||||
{
|
||||
@@ -16,6 +18,6 @@ const RocketDuotoneIcon = [
|
||||
'clip-rule': 'evenodd',
|
||||
},
|
||||
],
|
||||
] as const;
|
||||
];
|
||||
|
||||
export default RocketDuotoneIcon;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useData } from 'vitepress';
|
||||
import { useSessionStorage } from '@vueuse/core';
|
||||
import IconButton from '../base/IconButton.vue';
|
||||
import Button from '../base/Button.vue';
|
||||
import VPDocAsideCarbonAds from 'vitepress/dist/client/theme-default/components/VPDocAsideCarbonAds.vue';
|
||||
import { x } from '../../../data/iconNodes';
|
||||
import Icon from '@lucide/vue/src/Icon';
|
||||
@@ -40,16 +40,17 @@ onMounted(() => {
|
||||
class="floating-ad"
|
||||
v-if="theme.carbonAds"
|
||||
>
|
||||
<IconButton
|
||||
<Button
|
||||
@click="hideAd"
|
||||
class="hide-button"
|
||||
>
|
||||
<Icon
|
||||
:iconNode="x"
|
||||
name="x"
|
||||
:size="20"
|
||||
absoluteStrokeWidth
|
||||
/>
|
||||
</IconButton>
|
||||
</Button>
|
||||
<VPDocAsideCarbonAds :carbon-ads="theme.carbonAds" overlay />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { getAllCategoryFiles, mapCategoryIconCount } from '../../../lib/categories';
|
||||
import iconsMetaData from '../../../data/iconMetaData';
|
||||
|
||||
export default {
|
||||
const loader = {
|
||||
async load() {
|
||||
let categories = getAllCategoryFiles();
|
||||
|
||||
categories = mapCategoryIconCount(categories, Object.values(iconsMetaData));
|
||||
const categories = mapCategoryIconCount(getAllCategoryFiles(), Object.values(iconsMetaData));
|
||||
|
||||
return {
|
||||
categories,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
declare const data: Awaited<ReturnType<typeof loader.load>>;
|
||||
|
||||
export { data };
|
||||
export default loader;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useData } from 'vitepress'
|
||||
import VPLink from 'vitepress/dist/client/theme-default/components/VPLink.vue'
|
||||
import { isActive } from 'vitepress/dist/client/shared'
|
||||
import { isActive } from 'vitepress/dist/client/shared.js'
|
||||
import { useActiveAnchor } from '../../composables/useActiveAnchor'
|
||||
import { data } from './CategoryList.data'
|
||||
import CategoryListItem from './CategoryListItem.vue'
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import { pencilSparkles } from '../../../data/iconNodes';
|
||||
import { IconEntity } from '../../types';
|
||||
import getStudioLink from '../../utils/getStudioLink';
|
||||
import Button from '../base/Button.vue';
|
||||
import Icon from '@lucide/vue/src/Icon.ts';
|
||||
|
||||
defineProps<{
|
||||
icon: IconEntity
|
||||
}>()
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Button
|
||||
:href="getStudioLink(icon.name, icon.iconNode, 'icon-info')"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Icon :iconNode="pencilSparkles" name="pencil-sparkles" :size="24" />
|
||||
<span class="text">Edit in studio</span>
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.text {
|
||||
margin: 0 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -30,10 +30,15 @@ const props = defineProps<{
|
||||
.contributors {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
align-items: start;
|
||||
/* justify-content: flex-end; */
|
||||
gap: 16px;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.contributors .label {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.avatar-group {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@@ -16,19 +16,17 @@ const tags = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- <PageContainer class="overview"> -->
|
||||
<IconPreview
|
||||
:name="$params.name"
|
||||
:iconNode="$params.iconNode"
|
||||
class="preview"
|
||||
customizable
|
||||
/>
|
||||
<div class="details">
|
||||
<h1 class="title">
|
||||
{{ $params.name }}
|
||||
</h1>
|
||||
</div>
|
||||
<!-- </PageContainer> -->
|
||||
<IconPreview
|
||||
:name="$params.name"
|
||||
:iconNode="$params.iconNode"
|
||||
class="preview"
|
||||
customizable
|
||||
/>
|
||||
<div class="details">
|
||||
<h1 class="title">
|
||||
{{ $params.name }}
|
||||
</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -6,7 +6,11 @@ import useConfetti from '../../composables/useConfetti';
|
||||
const { animate, confetti } = useConfetti();
|
||||
const slots = useSlots();
|
||||
|
||||
const copiedText = computed(() => slots.default?.()[0].children);
|
||||
const copiedText = computed(() => {
|
||||
const children = slots.default?.()[0].children;
|
||||
|
||||
return typeof children === 'string' ? children : '';
|
||||
});
|
||||
|
||||
function copyText() {
|
||||
navigator.clipboard.writeText(copiedText.value);
|
||||
@@ -25,6 +29,7 @@ function copyText() {
|
||||
<slot />
|
||||
<Icon
|
||||
:iconNode="copy"
|
||||
name="copy"
|
||||
:size="20"
|
||||
class="copy-icon"
|
||||
/>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { IconEntity } from '../../types';
|
||||
import { computed } from 'vue';
|
||||
import createLucideIcon from '@lucide/vue/src/createLucideIcon';
|
||||
import IconButton from '../base/IconButton.vue';
|
||||
import Button from '../base/Button.vue';
|
||||
import IconContributors from './IconContributors.vue';
|
||||
import IconPreview from './IconPreview.vue';
|
||||
import { x, expand } from '../../../data/iconNodes';
|
||||
@@ -12,6 +12,7 @@ import Badge from '../base/Badge.vue';
|
||||
import { computedAsync } from '@vueuse/core';
|
||||
import { satisfies } from 'semver';
|
||||
import { useExternalLibs } from '../../composables/useExternalLibs';
|
||||
import EditInStudioButton from './EditInStudioButton.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
iconName: string | null;
|
||||
@@ -70,12 +71,19 @@ const Expand = createLucideIcon('Expand', expand);
|
||||
<div class="overlay-panel">
|
||||
<nav class="overlay-menu">
|
||||
<Badge
|
||||
v-if="icon.createdRelease"
|
||||
v-if="icon.awaitingRelease"
|
||||
class="version awaiting-release"
|
||||
>Unreleased</Badge
|
||||
>
|
||||
<Badge
|
||||
v-else-if="icon.createdRelease"
|
||||
class="version"
|
||||
:href="releaseTagLink(icon.createdRelease.version)"
|
||||
>v{{ icon.createdRelease.version }}</Badge
|
||||
>
|
||||
<IconButton
|
||||
{{ icon.createdRelease.version }}
|
||||
</Badge>
|
||||
<EditInStudioButton :icon="icon"/>
|
||||
<Button
|
||||
@click="
|
||||
go(
|
||||
icon.externalLibrary
|
||||
@@ -85,10 +93,10 @@ const Expand = createLucideIcon('Expand', expand);
|
||||
"
|
||||
>
|
||||
<component :is="Expand" />
|
||||
</IconButton>
|
||||
<IconButton @click="onClose">
|
||||
</Button>
|
||||
<Button @click="onClose">
|
||||
<component :is="CloseIcon" />
|
||||
</IconButton>
|
||||
</Button>
|
||||
</nav>
|
||||
<IconPreview
|
||||
id="previewer"
|
||||
@@ -200,7 +208,14 @@ const Expand = createLucideIcon('Expand', expand);
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
.version.awaiting-release {
|
||||
color: var(--vp-c-danger-3);
|
||||
}
|
||||
|
||||
.contributors {
|
||||
justify-content: flex-end;
|
||||
position: absolute;
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,30 +10,36 @@ import { computed } from 'vue';
|
||||
import createLucideIcon from '@lucide/vue/src/createLucideIcon';
|
||||
import { diamond } from '../../../data/iconNodes';
|
||||
import deprecationReasonTemplate from '../../../../../tools/build-icons/utils/deprecationReasonTemplate.ts';
|
||||
import type { IconDeprecationReason } from '../../../../../tools/build-icons/types.ts';
|
||||
import IconTags from "./IconTags.vue"
|
||||
import getStudioLink from '../../utils/getStudioLink';
|
||||
|
||||
const props = defineProps<{
|
||||
icon: IconEntity;
|
||||
popoverPosition?: 'top' | 'bottom';
|
||||
showEditButton?: boolean;
|
||||
}>();
|
||||
|
||||
const { go } = useRouter();
|
||||
const { page } = useData();
|
||||
|
||||
const tags = computed(() => {
|
||||
if (!props.icon || !props?.icon?.tags) return [];
|
||||
return props.icon.tags.join(' • ');
|
||||
});
|
||||
|
||||
const DiamondIcon = createLucideIcon('Diamond', diamond);
|
||||
|
||||
const deprecatedTitle = computed(() => {
|
||||
if (!props.icon.deprecationReason) return '';
|
||||
return deprecationReasonTemplate(props.icon.deprecationReason, {
|
||||
return deprecationReasonTemplate(props.icon.deprecationReason as IconDeprecationReason, {
|
||||
componentName: props.icon.name,
|
||||
iconName: props.icon.name,
|
||||
toBeRemovedInVersion: props.icon.toBeRemovedInVersion,
|
||||
});
|
||||
});
|
||||
|
||||
const iconPath = computed(() => {
|
||||
if (props.icon.externalLibrary) {
|
||||
return `icons/${props.icon.externalLibrary}/${props.icon.name}`;
|
||||
}
|
||||
|
||||
return `icons/${props.icon.name}`;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -60,14 +66,7 @@ const deprecatedTitle = computed(() => {
|
||||
Deprecated
|
||||
</Badge>
|
||||
</div>
|
||||
<div
|
||||
class="tags-scroller"
|
||||
v-if="tags.length"
|
||||
>
|
||||
<p class="icon-tags horizontal-scroller">
|
||||
{{ tags }}
|
||||
</p>
|
||||
</div>
|
||||
<IconTags :tags="props.icon.tags" />
|
||||
<div class="group">
|
||||
<Badge
|
||||
v-for="category in icon.categories"
|
||||
@@ -80,26 +79,10 @@ const deprecatedTitle = computed(() => {
|
||||
|
||||
<div class="group buttons">
|
||||
<VPButton
|
||||
v-if="
|
||||
!page?.relativePath?.startsWith?.(
|
||||
icon.externalLibrary
|
||||
? `icons/${icon.externalLibrary}/${icon.name}`
|
||||
: `icons/${icon.name}`,
|
||||
)
|
||||
"
|
||||
:href="
|
||||
icon.externalLibrary
|
||||
? `/icons/${icon.externalLibrary}/${icon.name}`
|
||||
: `/icons/${icon.name}`
|
||||
"
|
||||
v-if="!page?.relativePath?.startsWith?.(iconPath)"
|
||||
:href="iconPath"
|
||||
text="See in action"
|
||||
@click="
|
||||
go(
|
||||
icon.externalLibrary
|
||||
? `/icons/${icon.externalLibrary}/${icon.name}`
|
||||
: `/icons/${icon.name}`,
|
||||
)
|
||||
"
|
||||
@click="go(iconPath)"
|
||||
/>
|
||||
<CopySVGButton
|
||||
:name="icon.name"
|
||||
@@ -115,6 +98,38 @@ const deprecatedTitle = computed(() => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.studio-edit-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 0 14px;
|
||||
min-height: 40px;
|
||||
border: 1px solid var(--vp-button-alt-border);
|
||||
border-radius: 999px;
|
||||
background-color: var(--vp-button-alt-bg);
|
||||
color: var(--vp-button-alt-text);
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
text-decoration: none;
|
||||
transition:
|
||||
border-color 0.2s,
|
||||
color 0.2s,
|
||||
background-color 0.2s,
|
||||
transform 0.2s;
|
||||
margin-left: 0;
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
|
||||
.studio-edit-button:hover,
|
||||
.studio-edit-button:focus-visible {
|
||||
border-color: var(--vp-button-alt-hover-border);
|
||||
color: var(--vp-button-alt-hover-text);
|
||||
background-color: var(--vp-button-alt-hover-bg);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -156,66 +171,6 @@ const deprecatedTitle = computed(() => {
|
||||
background-color: var(--vp-c-brand-2);
|
||||
}
|
||||
|
||||
.icon-tags {
|
||||
font-size: 16px;
|
||||
color: var(--vp-c-text-2);
|
||||
font-weight: 500;
|
||||
line-height: 28px;
|
||||
white-space: nowrap;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.tags-scroller {
|
||||
position: relative;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
height: 28px;
|
||||
padding: 8px 0 16px;
|
||||
margin-bottom: 16px;
|
||||
margin-top: 8px;
|
||||
align-items: center;
|
||||
|
||||
--gradient-background: var(--tags-gradient-background, var(--vp-c-bg-elv));
|
||||
}
|
||||
.horizontal-scroller {
|
||||
overflow-x: scroll;
|
||||
/* Hide Scrollbar */
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
scrollbar-width: thin; /* can also be normal, or none, to not render scrollbar */
|
||||
scrollbar-color: var(--vp-c-text-4) transparent; /* foreground background */
|
||||
}
|
||||
.horizontal-scroller::-webkit-scrollbar {
|
||||
width: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.horizontal-scroller::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.horizontal-scroller::-webkit-scrollbar-thumb {
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.tags-scroller::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 32px;
|
||||
height: 100%;
|
||||
/* Background Gradient left to right */
|
||||
background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, var(--gradient-background) 100%);
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { IconEntity } from '../../types';
|
||||
import { computed, ref } from 'vue';
|
||||
import createLucideIcon from '@lucide/vue/src/createLucideIcon';
|
||||
import { useIconStyleContext } from '../../composables/useIconStyle';
|
||||
import { squarePen } from '../../../data/iconNodes';
|
||||
|
||||
const props = defineProps<{
|
||||
name: IconEntity['name'];
|
||||
@@ -11,6 +10,8 @@ const props = defineProps<{
|
||||
customizable?: boolean;
|
||||
}>();
|
||||
|
||||
const activeSize = ref('1000%');
|
||||
|
||||
const { size, color, strokeWidth, absoluteStrokeWidth } = useIconStyleContext();
|
||||
const previewIcon = ref();
|
||||
|
||||
@@ -20,31 +21,6 @@ const iconComponent = computed(() => {
|
||||
if (!props.name || !props.iconNode) return null;
|
||||
return createLucideIcon(props.name, props.iconNode);
|
||||
});
|
||||
|
||||
const SquarePenIcon = createLucideIcon('SquarePen', squarePen);
|
||||
|
||||
const innerSvg = computed(() => {
|
||||
if (!props.iconNode) return '';
|
||||
|
||||
const string = props.iconNode
|
||||
.map(([tag, attrs]) => {
|
||||
const attributes = Object.entries(attrs)
|
||||
.map(([key, value]) => `${key}="${value}"`)
|
||||
.join(' ');
|
||||
return `<${tag} ${attributes}/>`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
return string
|
||||
.replace(/>[\r\n ]+</g, '><')
|
||||
.replace(/(<.*?>)|\s+/g, (m, $1) => $1 || ' ')
|
||||
.trim();
|
||||
});
|
||||
|
||||
const base64InnerSvg = computed(() => {
|
||||
if (typeof window === 'undefined') return '';
|
||||
return btoa(innerSvg.value);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -55,6 +31,8 @@ const base64InnerSvg = computed(() => {
|
||||
:size="size"
|
||||
:color="color"
|
||||
:strokeWidth="absoluteStrokeWidth ? (Number(strokeWidth) * 24) / Number(size) : strokeWidth"
|
||||
class="preview-icon"
|
||||
:data-size="activeSize"
|
||||
/>
|
||||
<svg
|
||||
class="icon-grid"
|
||||
@@ -62,6 +40,7 @@ const base64InnerSvg = computed(() => {
|
||||
fill="none"
|
||||
stroke-width="0.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:class="{ show: activeSize === '1000%' }"
|
||||
>
|
||||
<g
|
||||
:key="`grid-${i}`"
|
||||
@@ -83,16 +62,17 @@ const base64InnerSvg = computed(() => {
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
<a
|
||||
:href="`https://studio.lucide.dev/edit?value=${encodeURIComponent(base64InnerSvg)}&name=${name}&utm_source=lucide.dev&utm_medium=icon-detail-preview`"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="studio-button"
|
||||
title="Open in Lucide Studio"
|
||||
aria-label="Open in Lucide Studio"
|
||||
>
|
||||
<SquarePenIcon :size="14" />
|
||||
</a>
|
||||
<div class="size-buttons" :class="{ show: activeSize !== '1000%' }">
|
||||
<button class="size-button" @click="activeSize = '1000%'" :class="{ active: activeSize === '1000%' }">
|
||||
1000%
|
||||
</button>
|
||||
<button class="size-button" @click="activeSize = '200%'" :class="{ active: activeSize === '200%' }">
|
||||
200%
|
||||
</button>
|
||||
<button class="size-button" @click="activeSize = '100%'" :class="{ active: activeSize === '100%' }">
|
||||
100%
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -104,56 +84,87 @@ const base64InnerSvg = computed(() => {
|
||||
top: 0;
|
||||
left: 0;
|
||||
stroke: var(--vp-c-divider);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.icon-grid.show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
height: 100%;
|
||||
aspect-ratio: 1/1;
|
||||
position: relative;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.icon-container > :deep(svg:not(.icon-grid)) {
|
||||
|
||||
.icon-container:hover .size-buttons {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.icon-container > :deep(svg.preview-icon) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: var(--vp-c-neutral);
|
||||
opacity: 0.8;
|
||||
transition: all 0.3s;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.studio-button {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
right: 8px;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
padding: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--vp-c-text-2);
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s, color 0.2s, background-color 0.2s;
|
||||
z-index: 2;
|
||||
.icon-container > :deep(svg.preview-icon[data-size='1000%']) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.icon-container:hover .studio-button,
|
||||
.studio-button:focus-visible {
|
||||
opacity: 1;
|
||||
.icon-container > :deep(svg.preview-icon[data-size='200%']) {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.studio-button:hover,
|
||||
.studio-button:focus-visible {
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-1);
|
||||
.icon-container > :deep(svg.preview-icon[data-size='100%']) {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.icon-component.customizable {
|
||||
will-change: width, height, stroke-width, stroke;
|
||||
/* color: var(--customize-color, currentColor);
|
||||
stroke-width: var(--customize-strokeWidth, 2); */
|
||||
}
|
||||
|
||||
.size-buttons {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
left: 50%;
|
||||
padding: 4px;
|
||||
border-radius: 12px;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
z-index: 99;
|
||||
background: var(--vp-code-tab-bg);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.size-buttons.show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.size-button {
|
||||
color: var(--vp-code-tab-text-color);
|
||||
padding: 2px 8px;
|
||||
}
|
||||
|
||||
.size-button.active {
|
||||
background: var(--vp-c-neutral-inverse);
|
||||
box-shadow: var(--vp-shadow-1);
|
||||
border-radius: 4px;
|
||||
color: var(--vp-code-tab-active-text-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -70,7 +70,6 @@ const prettyName = props.name
|
||||
class="showcase"
|
||||
v-if="iconComponent"
|
||||
>
|
||||
<h2 class="title">See this icon in action</h2>
|
||||
<div class="showcase-grid">
|
||||
<div class="showcase-item column">
|
||||
<div class="placeholder"></div>
|
||||
@@ -236,18 +235,17 @@ const prettyName = props.name
|
||||
.showcase-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 48px;
|
||||
margin-inline-start: 24px;
|
||||
margin-block-start: 48px;
|
||||
gap: 32px;
|
||||
background: var(--vp-code-block-bg);
|
||||
padding: 32px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.showcase-item {
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
background-color: var(--vp-c-bg);
|
||||
box-shadow:
|
||||
var(--vp-shadow-4),
|
||||
-24px -24px 0 var(--vp-c-bg-soft);
|
||||
box-shadow: var(--vp-shadow-2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
const MAX_VISIBLE_TAGS = 5;
|
||||
|
||||
const props = defineProps<{
|
||||
tags?: string[];
|
||||
}>();
|
||||
|
||||
const tags = computed(() => props.tags ?? []);
|
||||
const expanded = ref(false);
|
||||
const hiddenCount = computed(() => Math.max(tags.value.length - MAX_VISIBLE_TAGS, 0));
|
||||
const showAll = computed(() => expanded.value || hiddenCount.value === 0);
|
||||
|
||||
// The overlay reuses this component when switching icons, so collapse again
|
||||
watch(tags, () => {
|
||||
expanded.value = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul
|
||||
v-if="tags.length"
|
||||
class="icon-tags"
|
||||
>
|
||||
<!-- v-show keeps hidden tags in the rendered HTML so crawlers can index them -->
|
||||
<li
|
||||
v-for="(tag, index) in tags"
|
||||
v-show="showAll || index < MAX_VISIBLE_TAGS"
|
||||
:key="tag"
|
||||
class="tag"
|
||||
>
|
||||
{{ tag }}
|
||||
</li>
|
||||
<li
|
||||
v-if="!showAll"
|
||||
class="more"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="more-button"
|
||||
:aria-label="`Show ${hiddenCount} more tags`"
|
||||
@click="expanded = true"
|
||||
>
|
||||
+{{ hiddenCount }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.icon-tags {
|
||||
font-size: 16px;
|
||||
color: var(--vp-c-text-2);
|
||||
font-weight: 500;
|
||||
line-height: 28px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
list-style-type: disc;
|
||||
gap: 4px 8px;
|
||||
/* Clip bullets hanging before tags that start a wrapped line, padding leaves room for the focus ring */
|
||||
overflow: hidden;
|
||||
padding: 4px;
|
||||
margin: 4px -4px 12px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
margin-top: 0;
|
||||
padding-left: 0px;
|
||||
padding-right: 12px;
|
||||
list-style-position: outside;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tag:first-child {
|
||||
list-style: none;
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.more {
|
||||
margin-top: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.more-button {
|
||||
font: inherit;
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 12px;
|
||||
padding: 0 8px;
|
||||
margin-left: -4px;
|
||||
border-radius: 16px;
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
transition:
|
||||
color 0.25s,
|
||||
background-color 0.25s;
|
||||
}
|
||||
|
||||
.more-button:hover,
|
||||
.more-button:focus-visible {
|
||||
color: var(--vp-c-text-1);
|
||||
background-color: var(--vp-c-default-soft);
|
||||
}
|
||||
|
||||
.more-button:focus-visible {
|
||||
outline: 2px solid var(--vp-c-brand-1);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
</style>
|
||||
@@ -163,7 +163,7 @@ const IconDetailOverlay = defineAsyncComponent(() => import('./IconDetailOverlay
|
||||
function handleCloseDrawer() {
|
||||
setActiveIconName('');
|
||||
|
||||
const url = new URL(window.location);
|
||||
const url = new URL(window.location.href);
|
||||
url.pathname = '/icons/categories';
|
||||
|
||||
if (searchQueryDebounced.value) {
|
||||
|
||||
@@ -35,7 +35,6 @@ const SORTING = [
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
const initialGridItems = computed(() => {
|
||||
if (containerWidth.value === 0) return 120;
|
||||
|
||||
@@ -72,6 +71,8 @@ const sortedIcons = computed(() => {
|
||||
return [...props.icons].sort((a, b) => (b.popularity || 0) - (a.popularity || 0));
|
||||
case 'release-date':
|
||||
return [...props.icons].sort((a, b) => {
|
||||
if (a.awaitingRelease !== b.awaitingRelease) return a.awaitingRelease ? -1 : 1;
|
||||
|
||||
const aDate = a.createdRelease?.date ? new Date(a.createdRelease.date).getTime() : 0;
|
||||
const bDate = b.createdRelease?.date ? new Date(b.createdRelease.date).getTime() : 0;
|
||||
return bDate - aDate;
|
||||
@@ -172,7 +173,7 @@ watch(searchQueryDebounced, () => {
|
||||
function handleCloseDrawer() {
|
||||
setActiveIconName('');
|
||||
|
||||
const url = new URL(window.location);
|
||||
const url = new URL(window.location.href);
|
||||
url.pathname = '/icons/';
|
||||
|
||||
if (searchQueryDebounced.value) {
|
||||
@@ -207,6 +208,7 @@ function handleCloseDrawer() {
|
||||
<template #start-icon>
|
||||
<Icon
|
||||
:iconNode="listSortDescending"
|
||||
name="list-sort-descending"
|
||||
class="chevron-icon"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
@@ -39,7 +39,7 @@ interface Placeholder {
|
||||
finePrint?: string;
|
||||
}
|
||||
|
||||
const brandPlaceholders: Placeholder[] = shallowReadonly([
|
||||
const brandPlaceholders: readonly Placeholder[] = shallowReadonly([
|
||||
{
|
||||
title: 'Boooo! What a scary brand logo!',
|
||||
message:
|
||||
@@ -103,7 +103,7 @@ const brandPlaceholders: Placeholder[] = shallowReadonly([
|
||||
},
|
||||
]);
|
||||
|
||||
const notFoundPlaceholders: Omit<Placeholder, 'title'>[] = shallowReadonly([
|
||||
const notFoundPlaceholders: readonly Omit<Placeholder, 'title'>[] = shallowReadonly([
|
||||
{
|
||||
message: 'We’ve looked for this icon for a bird’s eye view, but could not find it.',
|
||||
icon: markRaw(bird),
|
||||
@@ -130,7 +130,7 @@ const notFoundPlaceholders: Omit<Placeholder, 'title'>[] = shallowReadonly([
|
||||
},
|
||||
]);
|
||||
|
||||
function randomItem<T>(arr: T[]): T {
|
||||
function randomItem<T>(arr: readonly T[]): T {
|
||||
return arr[Math.floor(Math.random() * arr.length)];
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ defineProps<{
|
||||
|
||||
<template>
|
||||
<section class="related-icons">
|
||||
<h2 class="title">More icons like this</h2>
|
||||
<IconGrid :icons="icons" />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -34,8 +34,8 @@ const sizeCssVar = useCssVar('--customize-size', props.rootEl?.value ?? document
|
||||
});
|
||||
|
||||
syncRef(color, colorCssVar, { direction: 'ltr' });
|
||||
syncRef(strokeWidth, strokeWidthCssVar, { direction: 'ltr' });
|
||||
syncRef(size, sizeCssVar, { direction: 'ltr' });
|
||||
syncRef(strokeWidth, strokeWidthCssVar, { direction: 'ltr', transform: { ltr: String } });
|
||||
syncRef(size, sizeCssVar, { direction: 'ltr', transform: { ltr: String } });
|
||||
|
||||
function resetStyle() {
|
||||
color.value = STYLE_DEFAULTS.color;
|
||||
|
||||
@@ -61,6 +61,50 @@
|
||||
}
|
||||
}
|
||||
|
||||
.search-bar :deep(.select-button) {
|
||||
padding: 11px 14px;
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
color: var(--vp-c-text-2);
|
||||
flex-shrink: 0;
|
||||
flex: 1;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.search-bar :deep(.select-button):hover,
|
||||
.search-bar :deep(.select-button):focus {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-bg-alt);
|
||||
}
|
||||
|
||||
.search-bar :deep(.select-options) {
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
.search-bar :deep(.select-option) {
|
||||
padding-right: 36px;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.search-bar :deep(.select-icon) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.search-bar :deep(.select-button) .select-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.search-bar :deep(.select-button) {
|
||||
padding: 11px 16px 11px 16px;
|
||||
}
|
||||
|
||||
.search-bar :deep(.select-button) .select-text {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.search-bar {
|
||||
padding-top: 32px;
|
||||
@@ -68,4 +112,7 @@
|
||||
top: calc(var(--vp-layout-top-height, 0px) + 64px);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import VPHero from 'vitepress/dist/client/theme-default/components/VPHero.vue'
|
||||
import { useData } from 'vitepress/dist/client/theme-default/composables/data'
|
||||
import { useData } from 'vitepress/dist/client/theme-default/composables/data.js'
|
||||
import FakeInput from '../base/FakeInput.vue';
|
||||
import { useRouter } from 'vitepress';
|
||||
import { data } from '../home/HomeHeroIconsCard.data'
|
||||
|
||||
@@ -41,7 +41,7 @@ import {
|
||||
sparkles,
|
||||
} from '../../../data/iconNodes';
|
||||
import createElement from 'lucide/src/createElement';
|
||||
import { IconNode } from 'lucide';
|
||||
import { IconNode } from 'lucide/src/types';
|
||||
|
||||
const icons: Component[] = [
|
||||
heart, star, zap, code, feather, cloud, sun, moon, camera, music,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useData } from 'vitepress';
|
||||
import { useSidebar } from 'vitepress/dist/client/theme-default/composables/sidebar';
|
||||
import { useSidebar } from 'vitepress/dist/client/theme-default/composables/sidebar.js';
|
||||
import VPLink from 'vitepress/dist/client/theme-default/components/VPLink.vue';
|
||||
import { computed } from 'vue';
|
||||
import SurveyAskOverlay from '../SurveyAskOverlay.vue';
|
||||
|
||||
@@ -2,7 +2,7 @@ import packageDataList from '../../../data/packageData.json';
|
||||
import thirdPartyPackages from '../../../data/packageData.thirdParty.json';
|
||||
import fetchPackages from '../../../lib/fetchPackages';
|
||||
|
||||
export default {
|
||||
const loader = {
|
||||
async load() {
|
||||
const packageJsonList = await fetchPackages();
|
||||
|
||||
@@ -29,3 +29,8 @@ export default {
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
declare const data: Awaited<ReturnType<typeof loader.load>>;
|
||||
|
||||
export { data };
|
||||
export default loader;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { onMounted, onUpdated, onUnmounted } from 'vue';
|
||||
import { throttleAndDebounce } from 'vitepress/dist/client/theme-default/support/utils';
|
||||
import { throttleAndDebounce } from 'vitepress/dist/client/theme-default/support/utils.js';
|
||||
|
||||
/*
|
||||
* This file is compied and adjusted from vitepress/dist/client/theme-default/composables/useActiveAnchor.ts
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import Fuse from 'fuse.js';
|
||||
import Fuse, { type FuseOptionKeyObject } from 'fuse.js';
|
||||
import { shallowRef, computed, Ref } from 'vue';
|
||||
|
||||
const useSearch = <T>(
|
||||
query: Ref<string>,
|
||||
collection: Ref<T[]>,
|
||||
keys: Fuse.FuseOptionKeyObject<T>[] = [],
|
||||
keys: FuseOptionKeyObject<T>[] = [],
|
||||
) => {
|
||||
const index = shallowRef(
|
||||
new Fuse(collection.value, {
|
||||
|
||||
@@ -152,6 +152,10 @@ html {
|
||||
.VPHomeHero .container {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.VPDoc:has(.vp-doc[class*=' _icons_']) {
|
||||
padding: 32px 32px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.VPNavBarHamburger .container > span {
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
export type IconNode = [elementName: string, attrs: Record<string, string>][];
|
||||
export type IconNodeWithKeys = [elementName: string, attrs: Record<string, string>, key: string][];
|
||||
|
||||
export interface Alias {
|
||||
name: string;
|
||||
deprecationReason: string;
|
||||
deprecated: boolean;
|
||||
}
|
||||
|
||||
export interface IconMetaData {
|
||||
tags: string[];
|
||||
categories: string[];
|
||||
contributors: string[];
|
||||
aliases?: string[];
|
||||
aliases?: Alias[];
|
||||
deprecated?: boolean;
|
||||
deprecationReason?: string;
|
||||
toBeRemovedInVersion?: string;
|
||||
@@ -18,6 +24,8 @@ export interface IconEntity extends IconMetaData {
|
||||
externalLibrary?: ExternalLibs;
|
||||
createdRelease?: Release;
|
||||
changedRelease?: Release;
|
||||
fromFork?: boolean;
|
||||
awaitingRelease?: boolean;
|
||||
popularity?: number;
|
||||
}
|
||||
|
||||
@@ -43,7 +51,8 @@ export interface PackageItem {
|
||||
packageDirname?: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
iconDark: string;
|
||||
iconDark?: string;
|
||||
iconClass?: string;
|
||||
shields: Shield[];
|
||||
source: string;
|
||||
documentation: string;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const chunkArray = <ArrayType>(stream: ArrayType, size: number) => {
|
||||
return stream.reduce<ArrayType[][]>(
|
||||
const chunkArray = <ItemType>(stream: ItemType[], size: number) => {
|
||||
return stream.reduce<ItemType[][]>(
|
||||
(chunks, item, idx, arr) =>
|
||||
idx % size == 0 ? [...chunks, arr.slice(idx, idx + size)] : chunks,
|
||||
[],
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { IconNode } from '../types';
|
||||
|
||||
function getInnerSvg(iconNode: IconNode) {
|
||||
return iconNode
|
||||
.map(([tag, attrs]) => {
|
||||
const attributes = Object.entries(attrs)
|
||||
.map(([key, value]) => `${key}="${value}"`)
|
||||
.join(' ');
|
||||
return `<${tag} ${attributes}/>`;
|
||||
})
|
||||
.join('')
|
||||
.replace(/>[\r\n ]+</g, '><')
|
||||
.replace(/(<.*?>)|\s+/g, (m, $1) => $1 || ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
export default function getStudioLink(
|
||||
name: string,
|
||||
iconNode: IconNode,
|
||||
utmMedium = 'icon-detail-preview',
|
||||
) {
|
||||
const base64InnerSvg = btoa(getInnerSvg(iconNode ?? []));
|
||||
|
||||
return `https://studio.lucide.dev/edit?value=${encodeURIComponent(base64InnerSvg)}&name=${name}&utm_source=lucide.dev&utm_medium=${utmMedium}`;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
/// <reference types="vitepress/client" />
|
||||
import { type IconNode } from '@lucide/vue/src/types';
|
||||
import Vue from 'vue';
|
||||
|
||||
@@ -31,7 +32,11 @@ declare module '*.node.json' {
|
||||
}
|
||||
|
||||
declare global {
|
||||
// Defined by VitePress at build time, used by its VPDocAsideCarbonAds component
|
||||
const __CARBON__: boolean;
|
||||
|
||||
interface Window {
|
||||
_carbonads?: unknown;
|
||||
ExpoSnack?: {
|
||||
/**
|
||||
* Initialize all snack players on the page
|
||||
|
||||
@@ -8,9 +8,7 @@ Lucide is a consistent icon set, not a collection of unrelated icons. Every icon
|
||||
|
||||
This guide explains the visual rules behind Lucide icons and how to use them when designing new icons.
|
||||
|
||||
::: info Requirement levels
|
||||
This guide uses normative terms such as **must**, **should**, and **may** as defined in [RFC 2119](https://www.rfc-editor.org/info/rfc2119/).
|
||||
:::
|
||||
For the full rule list, see the [icon design specification](./specification)
|
||||
|
||||
## 1. Use a 24 × 24-pixel canvas
|
||||
|
||||
@@ -96,9 +94,7 @@ Don't use **inside or outside** strokes. They change how line joins appear, crea
|
||||
|
||||
## 7. Use consistent corner radii
|
||||
|
||||
Corner radius depends on the size and angle of the element.
|
||||
|
||||
### 7.1. Sharp corners **should** be rounded unless the geometry of the icon requires otherwise
|
||||
Almost every sharp corner **should** be rounded.
|
||||
|
||||
:::: example
|
||||
::: do ../../images/border-radius-correct.svg?raw=true
|
||||
@@ -109,44 +105,29 @@ Don't use **sharp corners**.
|
||||
:::
|
||||
::::
|
||||
|
||||
### 7.2. Elements at least 8 pixels wide or tall **should** use a 2-pixel corner radius for 90° corners
|
||||
Corner radius depends on the size of the element and the angle. For 90° angles:
|
||||
|
||||
:::: example
|
||||
::: do ../../images/2px-border-radius.svg?raw=true
|
||||
Use a **2-pixel corner radius** for shapes that are at least 8 pixels wide or tall.
|
||||
:::
|
||||
::::
|
||||
|
||||
### 7.3. Elements smaller than 8 pixels **should** use a 1-pixel corner radius for 90° corners
|
||||
|
||||
Use a **1-pixel corner radius** for shapes smaller than 8 pixels.
|
||||
|
||||
:::: example
|
||||
::: do ../../images/1px-border-radius.svg?raw=true
|
||||
Use a **1-pixel corner radius** for shapes smaller than 8 pixels.
|
||||
:::
|
||||
::::
|
||||
|
||||
### 7.4. Diagonal lines meeting at a 90° angle **should** use a radius that preserves pixel-grid alignment
|
||||
|
||||
Diagonal lines that meet at a right angle usually need a **2.41 pixel** radius (1+√2) to stay aligned to the grid.
|
||||
There are exceptions:
|
||||
|
||||
:::: example
|
||||
::: do ../../images/border-radius-90deg.svg?raw=true
|
||||
Diagonal lines that meet at a right angle usually need a **2.41 pixel** radius (1+√2) to stay aligned to the grid.
|
||||
:::
|
||||
::::
|
||||
|
||||
### 7.5. Other acute corners **should** use an appropriate amount of rounding based on their geometry
|
||||
|
||||
Even **very sharp corners** usually need a small amount of rounding. The exact amount depends on the shape.
|
||||
|
||||
:::: example
|
||||
::: do ../../images/border-radius-arbitrary.svg?raw=true
|
||||
Even **very sharp corners** usually need a small amount of rounding. The exact amount depends on the shape.
|
||||
:::
|
||||
::::
|
||||
|
||||
### 7.6. Corners where lines cross **must not** be rounded
|
||||
|
||||
Corners where multiple lines meet **must not** be rounded. Keep corners sharp where multiple lines meet so the icon scales well.
|
||||
Avoid rounding corners **where multiple lines meet**.
|
||||
|
||||
:::: example
|
||||
::: do ../../images/border-radius-line-join.svg?raw=true
|
||||
@@ -157,15 +138,9 @@ Don't round corners where more than two lines meet. This can make the icon scale
|
||||
:::
|
||||
::::
|
||||
|
||||
### 7.7. Non-outlined arrows **must not** be rounded
|
||||
|
||||
Non-outlined arrows **must not** be rounded.
|
||||
|
||||
If the arrow is an outline shape, it **should** be rounded.
|
||||
|
||||
## 8. Keep at least 2 pixels between elements
|
||||
|
||||
### 8.1. Distinct elements **must** have at least 2 pixels of visual spacing between them
|
||||
Distinct elements **must** have at least 2 pixels of visual spacing between them.
|
||||
|
||||
:::: example
|
||||
::: do ../../images/2px-element-spacing.svg?raw=true
|
||||
@@ -176,9 +151,7 @@ Don't use gaps smaller than **2 pixels** between distinct elements.
|
||||
:::
|
||||
::::
|
||||
|
||||
### 8.2. Inner gaps **should** be at least 2 pixels wide
|
||||
|
||||
The 2-pixel gap rule also applies inside shapes. Most inner gaps **should** be at least 2 pixels wide.
|
||||
This also applies inside shapes. Most inner gaps **should** be at least 2 pixels wide.
|
||||
|
||||
:::: example
|
||||
::: do ../../images/2px-inner-spacing.svg?raw=true
|
||||
@@ -200,9 +173,7 @@ If the circle **overlaps** the shape, the gap should be **wider**.
|
||||
:::
|
||||
::::
|
||||
|
||||
### 8.3. Visual spacing **should** remain consistent when elements connect or intersect
|
||||
|
||||
Spacing **should** remain visually consistent where elements connect or intersect. Elements **must not** connect without keeping at least 2 pixels of spacing, and **should not** terminate with an abrupt cut.
|
||||
The 2 pixel spacing also applies when elements visually connect or intersect.
|
||||
|
||||
:::: example
|
||||
::: dont ../../images/2px-element-spacing-connected.svg?raw=true
|
||||
@@ -213,13 +184,9 @@ Don't use **abrupt cuts** where one element meets another.
|
||||
:::
|
||||
::::
|
||||
|
||||
### 8.4. Off-variants **may** omit the 2-pixel gap rule
|
||||
|
||||
The `-off` variants have their own design language and **may** omit the 2-pixel gap rule.
|
||||
|
||||
## 9. Aim for consistent visual weight
|
||||
|
||||
Icons **should** have a similar visual weight when viewed alongside each other.
|
||||
Icons should have a similar visual weight when viewed alongside each other.
|
||||
|
||||
:::: example
|
||||
|
||||
@@ -241,7 +208,7 @@ Place your icon next to `circle` or `square`, then blur both icons. Your icon sh
|
||||
|
||||
## 10. Balance icons visually
|
||||
|
||||
Icons **should** be visually centered within the canvas.
|
||||
Icons should look centered and balanced within the canvas.
|
||||
|
||||
:::: example
|
||||
::: do ../../images/visually-centered-do.svg?raw=true
|
||||
@@ -254,7 +221,7 @@ This icon feels shifted toward the top left.
|
||||
|
||||
:::: example
|
||||
::: do ../../images/visually-centered-symmetrical-do.svg?raw=true
|
||||
Symmetrical icons **should** remain geometrically centered.
|
||||
Symmetrical icons should remain geometrically centered.
|
||||
:::
|
||||
::: dont ../../images/visually-centered-symmetrical-dont.svg?raw=true
|
||||
Don't place symmetrical icons off-center for no good reason.
|
||||
@@ -262,14 +229,14 @@ Don't place symmetrical icons off-center for no good reason.
|
||||
::::
|
||||
|
||||
::: tip
|
||||
Geometric centering does not always look centered. Asymmetrical icons **may** be moved slightly off center so they look visually centered.
|
||||
Geometric centering does not always look centered. Asymmetrical shapes may need a small adjustment.
|
||||
|
||||
Compare your icon with `circle` or `square` side by side and stacked vertically. If it looks shifted, adjust it.
|
||||
:::
|
||||
|
||||
## 11. Keep density low
|
||||
|
||||
Icons **should** have a level of detail similar to other Lucide icons.
|
||||
Lucide icons should have a similar level of detail.
|
||||
|
||||
:::: example
|
||||
::: do ../../images/density-optimal.svg?raw=true
|
||||
@@ -290,7 +257,7 @@ View the icon at its intended size and blur it. Areas that turn dark may have to
|
||||
|
||||
## 12. Use smooth, simple curves
|
||||
|
||||
Curves **should** be smooth and as simple as the shape allows. Arcs and quadratic Bézier curves **should** be preferred where possible. Cubic Bézier curves **may** be used where necessary.
|
||||
Curves should be smooth and as simple as the shape allows.
|
||||
|
||||
:::: example
|
||||
::: do ../../images/curvature-smooth.svg?raw=true
|
||||
@@ -309,7 +276,7 @@ Avoid extra control points. They make shapes harder to edit and can create small
|
||||
|
||||
## 13. Design for the pixel grid
|
||||
|
||||
Lucide icons **should** look sharp at small sizes, including on low-density displays. Coordinates and centers of arcs and other geometric elements **should** align to the pixel grid where possible.
|
||||
Lucide icons should look sharp at small sizes, including on low-density displays.
|
||||
|
||||
:::: example
|
||||
::: do ../../images/pixel-perfection-ideal.svg?raw=true
|
||||
@@ -339,14 +306,12 @@ Avoid placing diagonal lines off-grid. They will appear blurry and optically wid
|
||||
::::
|
||||
|
||||
::: tip
|
||||
Pixel alignment **should** support the icon. Off-grid geometry **may** be used when required for visual balance, recognizable geometry, or smooth curvature. Pixel alignment **should not** take precedence over the visual quality of the icon.
|
||||
Pixel alignment should support the icon. Do not let it harm shape recognition, smooth curves, or visual balance.
|
||||
:::
|
||||
|
||||
## 14. Reuse established shapes
|
||||
|
||||
### 14.1. Variants **should** preserve base icon geometry
|
||||
|
||||
When creating a variant, the base icon geometry, placement, and orientation **should** be preserved unless the new icon requires a change.
|
||||
When creating a variant, keep the base icon geometry unless the new idea needs a change.
|
||||
|
||||
:::: example
|
||||
::: do ../../images/established-shapes-base-do.svg?raw=true
|
||||
@@ -357,7 +322,7 @@ Don't change the **geometry, placement, or orientation** of base shapes.
|
||||
:::
|
||||
::::
|
||||
|
||||
### 14.2. Related icons **should** share shapes and visual patterns when possible
|
||||
Related icons should share shapes and visual patterns when possible.
|
||||
|
||||
:::: example
|
||||
::: do ../../images/established-shapes-sub-do.svg?raw=true
|
||||
@@ -368,7 +333,7 @@ Don't redesign **existing elements**.
|
||||
:::
|
||||
::::
|
||||
|
||||
### 14.3. Common modifiers and elements **should** look consistent across the library
|
||||
Common modifiers and elements should look consistent across the library.
|
||||
|
||||
:::: example
|
||||
::: do ../../images/established-shapes-consistency-do.svg?raw=true
|
||||
@@ -382,7 +347,7 @@ Don't use **irregular placement or size** compared to existing icons.
|
||||
::: tip
|
||||
Preserving the base shape makes related icons work better when users switch between them.
|
||||
|
||||
Existing geometry **may** be modified when necessary to satisfy other Lucide design language rules. Consistency with an existing icon **should not** override clarity, balance, or readability.
|
||||
Consistency with an existing icon should not override clarity, balance, or readability. Adjust the shape when needed.
|
||||
:::
|
||||
|
||||
## Summary
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
---
|
||||
description: Rules Lucide icons must follow.
|
||||
---
|
||||
|
||||
# Icon design specification
|
||||
|
||||
This page lists the rules Lucide icons must follow.
|
||||
|
||||
For examples and guidance, see the [Lucide design language](./design-principles).
|
||||
|
||||
## Requirement levels
|
||||
|
||||
These terms show how strictly each rule applies:
|
||||
|
||||
- **Must** and **must not** mark required rules.
|
||||
- **Should** and **should not** mark rules that can flex when needed to make a better icon.
|
||||
|
||||
Any exception should be intentional and fit the [Lucide design language](./design-principles).
|
||||
|
||||
## Canvas
|
||||
|
||||
1. Icons **must** use a 24 × 24-pixel canvas.
|
||||
2. The canvas **must** be square.
|
||||
3. Icon strokes **must** remain at least 1 pixel from the edge of the canvas.
|
||||
|
||||
## Strokes
|
||||
|
||||
1. Strokes **must** be 2 pixels wide.
|
||||
2. Strokes **must** use round line joins.
|
||||
3. Open paths **must** use round line caps.
|
||||
4. Strokes **must** be centered on their paths.
|
||||
|
||||
## Corners
|
||||
|
||||
1. Sharp corners **should** be rounded unless the geometry of the icon requires otherwise.
|
||||
2. Elements at least 8 pixels wide or tall **should** use a 2-pixel corner radius for 90° corners.
|
||||
3. Elements smaller than 8 pixels **should** use a 1-pixel corner radius for 90° corners.
|
||||
4. Diagonal lines meeting at a 90° angle **should** use a radius that preserves pixel-grid alignment. This radius is typically approximately 2.41 pixels.
|
||||
5. Acute corners **should** use an appropriate amount of rounding based on their geometry.
|
||||
6. Corners where multiple lines meet **should** remain sharp when rounding would cause the icon to render inconsistently at different sizes.
|
||||
|
||||
## Spacing
|
||||
|
||||
1. Distinct elements **must** have at least 2 pixels of visual separation.
|
||||
2. Gaps between distinct elements **must not** be smaller than 2 pixels.
|
||||
3. Inner gaps **should** be at least 2 pixels wide.
|
||||
4. Spacing **should** remain visually consistent where elements connect or intersect.
|
||||
5. Elements **should not** terminate with an abrupt cut where another element visually continues or intersects them.
|
||||
|
||||
## Visual weight
|
||||
|
||||
1. Icons **should** have a visual weight similar to the `circle` and `square` icons.
|
||||
2. Icons **should not** appear substantially larger, smaller, heavier, or lighter than comparable Lucide icons.
|
||||
|
||||
## Alignment and balance
|
||||
|
||||
1. Icons **should** be visually centered within the canvas.
|
||||
2. Asymmetrical icons **may** be moved slightly off center so they look visually centered.
|
||||
3. Symmetrical icons **should** be geometrically centered.
|
||||
|
||||
## Visual density
|
||||
|
||||
1. Icons **should** have a level of detail similar to other Lucide icons.
|
||||
2. Details **should** be simplified when they cause an icon to appear substantially denser than comparable icons.
|
||||
3. Details that are not necessary for recognition **should** be omitted.
|
||||
|
||||
## Curves
|
||||
|
||||
1. Continuous curves **should** have smooth curvature.
|
||||
2. Curves **should not** contain unintended or abrupt changes in curvature.
|
||||
3. Arcs and quadratic Bézier curves **should** be preferred where they can accurately represent the intended geometry.
|
||||
4. Cubic Bézier curves **may** be used where necessary.
|
||||
5. Control points of adjoining cubic Bézier curves **should** be aligned where required to maintain continuous curvature.
|
||||
6. Curves **should not** contain unnecessary control points.
|
||||
|
||||
## Pixel alignment
|
||||
|
||||
1. Coordinates **should** align to the pixel grid where possible.
|
||||
2. Centers of arcs and other geometric elements **should** align to the pixel grid where possible.
|
||||
3. Off-grid geometry **may** be used when required for visual balance, recognizable geometry, or smooth curvature.
|
||||
4. Pixel alignment **should not** take precedence over the visual quality of the icon.
|
||||
|
||||
## Shared geometry
|
||||
|
||||
1. Variants of an existing icon **should** preserve the geometry, placement, and orientation of the base icon.
|
||||
2. Elements that already exist in other Lucide icons **should** reuse established geometry where applicable.
|
||||
3. Related icons **should** use consistent representations of shared elements.
|
||||
4. Common modifiers and add-ons **should** use consistent geometry, size, and placement across related icons.
|
||||
5. Existing geometry **may** be modified when necessary to satisfy other Lucide design language rules.
|
||||
|
||||
## Priority of requirements
|
||||
|
||||
When two non-required rules conflict, choose visual clarity and consistency with the Lucide design language.
|
||||
|
||||
Rules marked **must** or **must not** can only be broken when the project defines an explicit exception.
|
||||
@@ -25,7 +25,7 @@ This is useful if you want all icons to share the same size, color, or stroke wi
|
||||
You can call `setLucideProps` in your main entry file or in a top-level component to set the default properties for all icons.
|
||||
|
||||
```js
|
||||
import { setLucideProps } from '@lucide/svelte';
|
||||
import { setLucideProps } from '@lucide/svelte/context';
|
||||
|
||||
setLucideProps({
|
||||
size: 32,
|
||||
|
||||
@@ -12,6 +12,7 @@ import { computed } from 'vue'
|
||||
import { useData } from 'vitepress'
|
||||
import IconPreview from '~/.vitepress/theme/components/icons/IconPreview.vue'
|
||||
import IconPreviewSmall from '~/.vitepress/theme/components/icons/IconPreviewSmall.vue'
|
||||
import EditInStudioButton from '~/.vitepress/theme/components/icons/EditInStudioButton.vue'
|
||||
import IconInfo from '~/.vitepress/theme/components/icons/IconInfo.vue'
|
||||
import IconContributors from '~/.vitepress/theme/components/icons/IconContributors.vue'
|
||||
import IconShowcase from '~/.vitepress/theme/components/icons/IconShowcase.vue'
|
||||
@@ -19,9 +20,11 @@ import RelatedIcons from '~/.vitepress/theme/components/icons/RelatedIcons.vue'
|
||||
import CodeGroup from '~/.vitepress/theme/components/base/CodeGroup.vue'
|
||||
import Badge from '~/.vitepress/theme/components/base/Badge.vue'
|
||||
import Label from '~/.vitepress/theme/components/base/Label.vue'
|
||||
import PageTabs from '~/.vitepress/theme/components/base/PageTabs.vue'
|
||||
import { data } from './codeExamples.data'
|
||||
import { toCamelCase, toPascalCase } from '@lucide/shared'
|
||||
import { satisfies } from 'semver'
|
||||
import CarbonAdOverlay from '~/.vitepress/theme/components/icons/CarbonAdOverlay.vue';
|
||||
|
||||
const { params } = useData()
|
||||
|
||||
@@ -49,75 +52,92 @@ function releaseTagLink(version) {
|
||||
}
|
||||
</script>
|
||||
|
||||
<div :class="$style.layout">
|
||||
<div :class="$style.iconPreviews">
|
||||
<div class="layout">
|
||||
<div class="actions">
|
||||
<EditInStudioButton :icon="params" />
|
||||
</div>
|
||||
<div class="iconPreviews">
|
||||
<IconPreview
|
||||
id="previewer"
|
||||
:name="params.name"
|
||||
:iconNode="params.iconNode"
|
||||
:class="$style.preview"
|
||||
/>
|
||||
<IconPreviewSmall
|
||||
:name="params.name"
|
||||
:iconNode="params.iconNode"
|
||||
:class="$style.smallPreview"
|
||||
class="preview"
|
||||
/>
|
||||
</div>
|
||||
<div >
|
||||
<div :class="$style.info">
|
||||
|
||||
<div>
|
||||
<div class="info">
|
||||
<IconInfo :icon="params" />
|
||||
<div :class="$style.meta">
|
||||
<div class="meta">
|
||||
<div
|
||||
v-if="params.createdRelease?.version"
|
||||
:class="$style.version"
|
||||
v-if="params.awaitingRelease"
|
||||
class="version"
|
||||
>
|
||||
<Label>Created:</Label>
|
||||
<Badge class="awaiting-release">Unreleased</Badge>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="params.createdRelease?.version"
|
||||
class="version"
|
||||
>
|
||||
<Label>Created:</Label>
|
||||
<Badge
|
||||
:href="releaseTagLink(params.createdRelease.version)"
|
||||
>
|
||||
v{{params.createdRelease.version}}
|
||||
{{params.createdRelease.version}}
|
||||
</Badge>
|
||||
</div>
|
||||
<div
|
||||
v-if="params.changedRelease?.version"
|
||||
:class="$style.version"
|
||||
v-if="!params.awaitingRelease && params.changedRelease?.version"
|
||||
class="version"
|
||||
>
|
||||
<Label>Last changed:</Label>
|
||||
<Badge
|
||||
:href="releaseTagLink(params.changedRelease.version)"
|
||||
>
|
||||
v{{params.changedRelease.version}}
|
||||
{{params.changedRelease.version}}
|
||||
</Badge>
|
||||
</div>
|
||||
<IconContributors :icon="params" :class="$style.contributors"/>
|
||||
<IconContributors :icon="params" class="contributors"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PageTabs
|
||||
:tabs="['More like this', 'Code examples', 'See in action']"
|
||||
>
|
||||
<template #tab-0>
|
||||
<RelatedIcons
|
||||
v-if="params.relatedIcons"
|
||||
:icons="params.relatedIcons"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #tab-1>
|
||||
<CodeGroup
|
||||
:groups="tabs"
|
||||
groupName="icon-code-example"
|
||||
:class="$style.code"
|
||||
class="code"
|
||||
>
|
||||
<div
|
||||
class="blocks"
|
||||
v-html="codeExample"
|
||||
/>
|
||||
</CodeGroup>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="icon-page-sections">
|
||||
<IconShowcase
|
||||
:name="params.name"
|
||||
:iconNode="params.iconNode"
|
||||
/>
|
||||
<template #tab-2>
|
||||
<IconShowcase
|
||||
:name="params.name"
|
||||
:iconNode="params.iconNode"
|
||||
/>
|
||||
</template>
|
||||
</PageTabs>
|
||||
|
||||
<RelatedIcons
|
||||
v-if="params.relatedIcons"
|
||||
:icons="params.relatedIcons"
|
||||
/>
|
||||
</div>
|
||||
<CarbonAdOverlay :drawerOpen="false" />
|
||||
|
||||
<style module>
|
||||
<style scoped>
|
||||
.preview {
|
||||
grid-area: preview;
|
||||
margin-bottom: 24px;
|
||||
@@ -130,31 +150,52 @@ function releaseTagLink(version) {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.actions {
|
||||
text-align: right;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin: -12px -12px 24px 0;
|
||||
}
|
||||
|
||||
.meta {
|
||||
margin-left: auto;
|
||||
margin-top: 24px;
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.info {
|
||||
--tags-gradient-background: var(--vp-c-bg);
|
||||
|
||||
}
|
||||
|
||||
.icon-info {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.version .label {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.version, .contributors {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 0px;
|
||||
justify-content: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.version:first-child {
|
||||
margin-bottom: 8px;
|
||||
.version .awaiting-release {
|
||||
color: var(--vp-c-danger-3);
|
||||
}
|
||||
|
||||
.iconPreviews {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
@@ -178,6 +219,13 @@ function releaseTagLink(version) {
|
||||
|
||||
.iconPreviews {
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.actions {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.smallPreview {
|
||||
@@ -185,61 +233,6 @@ function releaseTagLink(version) {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 860px) {
|
||||
.info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.meta {
|
||||
border-left: 1px solid var(--vp-c-divider);
|
||||
padding-left: 16px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.version, .contributors {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.info {
|
||||
display: block;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.meta {
|
||||
border-left: none;
|
||||
padding-left: 0;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.version, .contributors {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1152px) {
|
||||
.info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.meta {
|
||||
border-left: 1px solid var(--vp-c-divider);
|
||||
padding-left: 16px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.version, .contributors {
|
||||
flex-direction: row;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
@@ -249,4 +242,19 @@ function releaseTagLink(version) {
|
||||
margin-block-end: 64px;
|
||||
padding-top: 32px;
|
||||
}
|
||||
|
||||
.tab-list {
|
||||
margin: 24px -24px 24px;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.tab-list {
|
||||
margin: 24px 0 24px;
|
||||
}
|
||||
|
||||
.iconPreviews {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,17 +3,28 @@ import * as iconDetails from '../.vitepress/data/iconDetails';
|
||||
export default {
|
||||
async load() {
|
||||
return {
|
||||
icons: Object.entries(iconDetails).map(
|
||||
([, { name, iconNode, popularity, createdRelease, aliases = [] }]) => ({
|
||||
name,
|
||||
iconNode,
|
||||
popularity: popularity?.count ?? 0,
|
||||
aliases: aliases
|
||||
.map((alias) => (typeof alias === 'string' ? alias : alias?.name))
|
||||
.filter((alias): alias is string => Boolean(alias)),
|
||||
createdRelease,
|
||||
}),
|
||||
),
|
||||
icons: Object.entries(iconDetails)
|
||||
.map(
|
||||
([
|
||||
,
|
||||
{ name, iconNode, popularity, createdRelease, awaitingRelease, aliases = [], fromFork },
|
||||
]) => ({
|
||||
name,
|
||||
iconNode,
|
||||
popularity: popularity?.count ?? 0,
|
||||
aliases: aliases
|
||||
.map((alias) => (typeof alias === 'string' ? alias : alias?.name))
|
||||
.filter((alias): alias is string => Boolean(alias)),
|
||||
createdRelease,
|
||||
awaitingRelease,
|
||||
fromFork,
|
||||
}),
|
||||
// We hide icons that are not released yet, we make exceptions for preview environments
|
||||
)
|
||||
.filter(
|
||||
({ awaitingRelease }) =>
|
||||
!awaitingRelease || process.env.VITE_SHOW_AWAITING_RELEASE === 'true',
|
||||
),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import iconNodes from '../../.vitepress/data/lab/iconNodes';
|
||||
import iconPopularity from '../../.vitepress/data/lab/iconPopularity';
|
||||
import iconMetaData from '../../.vitepress/data/lab/iconMetaData';
|
||||
import { type IconEntity } from '../../.vitepress/theme/types';
|
||||
|
||||
export default {
|
||||
paths: async () => {
|
||||
return (Object.entries(iconNodes) as unknown as IconEntity[]).map(([name, iconNode]) => {
|
||||
return Object.entries(iconNodes).map(([name, iconNode]) => {
|
||||
const iconDetails = iconMetaData[name];
|
||||
|
||||
delete iconDetails['$schema'];
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"prebuild:categoriesJson": "node ./scripts/writeCategoriesMetadata.mts",
|
||||
"prebuild:relatedIcons": "node ./scripts/writeIconRelatedIcons.mts",
|
||||
"prebuild:iconDetails": "node ./scripts/writeIconDetails.mts",
|
||||
"prebuild:iconPopularity": "node ./scripts/writeiconPopularity.mts && node ./scripts/writeiconPopularityIndex.mts",
|
||||
"prebuild:iconPopularity": "node --env-file-if-exists=.env ./scripts/writeiconPopularity.mts && node ./scripts/writeiconPopularityIndex.mts",
|
||||
"prebuild:brandStopwords": "node ./scripts/writeBrandStopwords.mts",
|
||||
"prebuild:writeLabIconNodes": "node ./scripts/writeLabIconNodes.mts",
|
||||
"prebuild:writeLabIconMetaIndex": "node ./scripts/writeLabIconMetaIndex.mts",
|
||||
@@ -24,7 +24,8 @@
|
||||
"prebuild:api": "nitropack prepare",
|
||||
"build:api": "NODE_OPTIONS=--max-old-space-size=8192 nitropack build",
|
||||
"build": "pnpm run /^prebuild:.*/ && pnpm run build:api && pnpm run build:docs && pnpm postbuild:vercelJson",
|
||||
"preview": "node .output/server/index.mjs"
|
||||
"preview": "node .output/server/index.mjs",
|
||||
"typecheck": "vue-tsc --noEmit"
|
||||
},
|
||||
"author": "Eric Fennis",
|
||||
"license": "ISC",
|
||||
@@ -47,9 +48,11 @@
|
||||
"svg-path-commander": "^2.3.3",
|
||||
"svg-pathdata": "^9.0.0",
|
||||
"svgson": "^5.3.1",
|
||||
"typescript": "^6.0.3",
|
||||
"vitepress": "^1.6.4",
|
||||
"vitepress-plugin-group-icons": "^1.7.6",
|
||||
"vitepress-plugin-llms": "^1.13.5"
|
||||
"vitepress-plugin-llms": "^1.13.5",
|
||||
"vue-tsc": "^3.3.11"
|
||||
},
|
||||
"dependencies": {
|
||||
"@floating-ui/vue": "^2.0.1",
|
||||
@@ -68,6 +71,6 @@
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"sandpack-vue3": "3.1.12",
|
||||
"vue": "^3.5.42"
|
||||
"vue": "^3.5.43"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,12 +27,13 @@ import iconNode from '../iconNodes/${iconName}.node.json' with { type: 'json' };
|
||||
import metaData from '../../../../icons/${iconName}.json' with { type: 'json' };
|
||||
import releaseData from '../releaseMetadata/${iconName}.json' with { type: 'json' };
|
||||
import popularity from '../iconPopularity/${iconName}.json' with { type: 'json' };
|
||||
import { IconMetaData, IconNode } from '~/.vitepress/theme/types';
|
||||
|
||||
const { tags, categories, contributors, aliases, deprecated, deprecationReason, toBeRemovedInVersion } = metaData;
|
||||
const { tags, categories, contributors, aliases, deprecated, deprecationReason, toBeRemovedInVersion }: IconMetaData = metaData;
|
||||
|
||||
const iconDetails = {
|
||||
name: '${iconName}',
|
||||
iconNode,
|
||||
iconNode: iconNode as IconNode,
|
||||
contributors,
|
||||
tags,
|
||||
categories,
|
||||
|
||||
@@ -20,6 +20,7 @@ if (!fs.existsSync(iconNodesDirectory)) {
|
||||
|
||||
const iconIndexFile = path.resolve(iconNodesDirectory, `index.ts`);
|
||||
const iconIndexFileImports = [];
|
||||
const iconIndexFileNodes = [];
|
||||
const iconIndexFileExports = [];
|
||||
const iconIndexFileDefaultExports = [];
|
||||
|
||||
@@ -32,7 +33,10 @@ const writeIconFiles = Object.entries(icons).map(async ([iconName, { children }]
|
||||
await fs.promises.writeFile(location, output, 'utf-8');
|
||||
|
||||
iconIndexFileImports.push(
|
||||
`import ${toCamelCase(iconName)}Node from './${iconName}.node.json' with { type: "json" };`,
|
||||
`import ${toCamelCase(iconName)}Json from './${iconName}.node.json' with { type: "json" };`,
|
||||
);
|
||||
iconIndexFileNodes.push(
|
||||
`const ${toCamelCase(iconName)}Node = ${toCamelCase(iconName)}Json as IconNode;`,
|
||||
);
|
||||
iconIndexFileExports.push(` ${toCamelCase(iconName)}Node as ${toCamelCase(iconName)},`);
|
||||
iconIndexFileDefaultExports.push(` '${iconName}': ${toCamelCase(iconName)}Node,`);
|
||||
@@ -43,16 +47,19 @@ try {
|
||||
await fs.promises.writeFile(
|
||||
iconIndexFile,
|
||||
`\
|
||||
import type { IconNode } from '../../theme/types';
|
||||
${iconIndexFileImports.join('\n')}
|
||||
|
||||
${iconIndexFileNodes.join('\n')}
|
||||
|
||||
export {
|
||||
${iconIndexFileExports.join('\n')}
|
||||
}
|
||||
|
||||
export default {
|
||||
${iconIndexFileDefaultExports.join('\n')}
|
||||
}
|
||||
`,
|
||||
} satisfies Record<string, IconNode>;
|
||||
`,
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ if (!fs.existsSync(iconNodesDirectory)) {
|
||||
|
||||
const iconIndexFile = path.resolve(iconNodesDirectory, `index.ts`);
|
||||
const iconIndexFileImports: string[] = [];
|
||||
const iconIndexFileNodes: string[] = [];
|
||||
const iconIndexFileExports: string[] = [];
|
||||
const iconIndexFileDefaultExports: string[] = [];
|
||||
|
||||
@@ -32,7 +33,10 @@ const writeIconFiles = Object.entries(icons).map(async ([iconName, { children }]
|
||||
await fs.promises.writeFile(location, output, 'utf-8');
|
||||
|
||||
iconIndexFileImports.push(
|
||||
`import ${toCamelCase(iconName)}Node from './${iconName}.node.json' with { type: "json" };`,
|
||||
`import ${toCamelCase(iconName)}Json from './${iconName}.node.json' with { type: "json" };`,
|
||||
);
|
||||
iconIndexFileNodes.push(
|
||||
`const ${toCamelCase(iconName)}Node = ${toCamelCase(iconName)}Json as IconNode;`,
|
||||
);
|
||||
iconIndexFileExports.push(` ${toCamelCase(iconName)}Node as ${toCamelCase(iconName)},`);
|
||||
iconIndexFileDefaultExports.push(` '${iconName}': ${toCamelCase(iconName)}Node,`);
|
||||
@@ -43,16 +47,19 @@ try {
|
||||
await fs.promises.writeFile(
|
||||
iconIndexFile,
|
||||
`\
|
||||
import type { IconNode } from '../../../theme/types';
|
||||
${iconIndexFileImports.join('\n')}
|
||||
|
||||
${iconIndexFileNodes.join('\n')}
|
||||
|
||||
export {
|
||||
${iconIndexFileExports.join('\n')}
|
||||
}
|
||||
|
||||
export default {
|
||||
${iconIndexFileDefaultExports.join('\n')}
|
||||
}
|
||||
`,
|
||||
} satisfies Record<string, IconNode>;
|
||||
`,
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
|
||||
@@ -40,7 +40,21 @@ const fetchAllReleases = async () => {
|
||||
);
|
||||
};
|
||||
|
||||
const getIconNamesAtTag = async (tag: string) => {
|
||||
// --full-tree resolves `icons/` from the repo root instead of the cwd (docs/)
|
||||
const files = await git.raw(['ls-tree', '--full-tree', '--name-only', tag, 'icons/']);
|
||||
|
||||
return new Set(
|
||||
files
|
||||
.split('\n')
|
||||
.filter((file) => file.endsWith('.svg'))
|
||||
.map((file) => path.basename(file, '.svg')),
|
||||
);
|
||||
};
|
||||
|
||||
const tags = await fetchAllReleases();
|
||||
const forkIconNames = await getIconNamesAtTag(tags[0]);
|
||||
const releasedIconNames = await getIconNamesAtTag(tags[tags.length - 1]);
|
||||
|
||||
const comparisonsPromises = tags.map(async (tag, index) => {
|
||||
const previousTag = tags[index - 1];
|
||||
@@ -150,19 +164,26 @@ try {
|
||||
const iconName = path.basename(iconJsonFile, '.json');
|
||||
const metaDir = path.resolve(releaseMetaDataDirectory, `${iconName}.json`);
|
||||
|
||||
if (!(iconName in newReleaseMetaData)) {
|
||||
const metaData = await fs.promises.readFile(path.join(ICONS_DIR, iconJsonFile), 'utf-8');
|
||||
const iconMetaData = JSON.parse(metaData);
|
||||
const aliases = iconMetaData.aliases ?? [];
|
||||
|
||||
const fromFork = [iconName, ...aliases.map((alias) => alias.name)].some((name) =>
|
||||
forkIconNames.has(name),
|
||||
);
|
||||
const awaitingRelease = !releasedIconNames.has(iconName);
|
||||
|
||||
if (!(iconName in newReleaseMetaData) && !fromFork && !awaitingRelease) {
|
||||
console.error(`Could not find release metadata for icon '${iconName}'.`);
|
||||
}
|
||||
|
||||
const contents = {
|
||||
fromFork,
|
||||
awaitingRelease,
|
||||
...defaultReleaseMetaData,
|
||||
...(newReleaseMetaData[iconName] ?? {}),
|
||||
};
|
||||
|
||||
const metaData = await fs.promises.readFile(path.join(ICONS_DIR, iconJsonFile), 'utf-8');
|
||||
const iconMetaData = JSON.parse(metaData);
|
||||
const aliases = iconMetaData.aliases ?? [];
|
||||
|
||||
if (aliases.length) {
|
||||
aliases.forEach((alias) => {
|
||||
if (!(alias.name in newReleaseMetaData)) {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"allowImportingTsExtensions": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"noEmit": true,
|
||||
"skipLibCheck": true,
|
||||
"paths": {
|
||||
"~/.vitepress/*": ["./.vitepress/*"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"$schema": "../icon.schema.json",
|
||||
"contributors": [
|
||||
"TanvirMahin24",
|
||||
"karsa-mistmere"
|
||||
],
|
||||
"use-cases": [
|
||||
"Showing product prices in BDT",
|
||||
"Selecting BDT in multi-currency interfaces",
|
||||
"Showing BDT exchange rates in currency converters",
|
||||
"Indicating BDT amounts in invoices and payments"
|
||||
],
|
||||
"tags": [
|
||||
"currency",
|
||||
"money",
|
||||
"payment",
|
||||
"bdt",
|
||||
"৳",
|
||||
"currencysymbol",
|
||||
"cash",
|
||||
"banknote",
|
||||
"price",
|
||||
"remittance",
|
||||
"ecommerce"
|
||||
],
|
||||
"categories": [
|
||||
"finance",
|
||||
"shopping",
|
||||
"travel"
|
||||
]
|
||||
}
|
||||
@@ -9,8 +9,6 @@
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M8 7V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
||||
<rect width="20" height="14" x="2" y="7" rx="2" />
|
||||
<path d="M15 14H9" />
|
||||
<path d="M12 11v6" />
|
||||
<path d="M6 5a2 2 0 0 1 4 0v12a4 4 0 0 0 8 0 2 2 0 0 0-4 0" />
|
||||
<path d="M6 9h12" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 363 B After Width: | Height: | Size: 296 B |
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"$schema": "../icon.schema.json",
|
||||
"contributors": [
|
||||
"colebemis",
|
||||
"csandman",
|
||||
"ericfennis",
|
||||
"tylerkade",
|
||||
"jguddas"
|
||||
],
|
||||
"use-cases": [
|
||||
"Indicating you're adding luggage",
|
||||
"adding a new business or company profile",
|
||||
"creating or saving a professional portfolio",
|
||||
"adding a job or role to a user's profile",
|
||||
"indicating add to briefcase or save for later in a product/service listing"
|
||||
],
|
||||
"tags": [
|
||||
"work",
|
||||
"bag",
|
||||
"baggage",
|
||||
"folder",
|
||||
"new",
|
||||
"add",
|
||||
"create",
|
||||
"increase",
|
||||
"briefcase",
|
||||
"portfolio",
|
||||
"business",
|
||||
"career",
|
||||
"employment",
|
||||
"professional",
|
||||
"plus"
|
||||
],
|
||||
"categories": [
|
||||
"transportation",
|
||||
"files",
|
||||
"account"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M13.354 20H4a2 2 0 01-2-2V8a2 2 0 012-2h16a2 2 0 012 2v3.354" />
|
||||
<path d="M16 11.354V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v16" />
|
||||
<path d="M16 17h6" />
|
||||
<path d="M19 14v6" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 392 B |
@@ -3,7 +3,8 @@
|
||||
"contributors": [
|
||||
"rrod497",
|
||||
"jamiemlaw",
|
||||
"karsa-mistmere"
|
||||
"karsa-mistmere",
|
||||
"jguddas"
|
||||
],
|
||||
"use-cases": [],
|
||||
"tags": [
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M10 13a3 3 0 0 1-2.121-5.121" />
|
||||
<path d="M15.606 14.204c-3.5 1.5-5.899 4.503-8.899 7.503A1 1 0 0 1 6 22c-2 0-4-2-4-4a1 1 0 0 1 .293-.707c1.911-1.911 3.823-3.578 5.347-5.441" />
|
||||
<path d="M16.573 14.737A4 4 0 0 1 14 11" />
|
||||
<path d="M7.14 10.907a4 4 0 1 1 2.756-7.43A4 4 0 0 1 16.7 4.48a2 2 0 0 1 2.82 2.82 4 4 0 0 1 1.002 6.805A4 4 0 1 1 13 16" />
|
||||
<path d="M10 13a3 3 0 01-2.121-5.121" />
|
||||
<path d="M15.606 14.204c-3.5 1.5-5.899 4.503-8.899 7.503A1 1 0 016 22c-2 0-4-2-4-4a1 1 0 01.293-.707c1.911-1.911 3.823-3.578 5.347-5.441" />
|
||||
<path d="M16.573 14.737A4 4 0 0114 11" />
|
||||
<path d="M7.14 10.907a4 4 0 112.756-7.43A4 4 0 0116.7 4.48a2 2 0 012.82 2.82 4 4 0 011.002 6.805 4 4 0 11-7.51 1.59" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 572 B After Width: | Height: | Size: 560 B |
@@ -12,7 +12,7 @@
|
||||
<path d="m13 21 3-3-3-3" />
|
||||
<path d="M16 2v3" />
|
||||
<path d="m19 21 3-3-3-3" />
|
||||
<path d="M21 11.5V5.05a2 2 0 00-2-2H5a2 2 0 00-2 2v14a2 2 0 002 2L9 21" />
|
||||
<path d="M21 11.5V5a2 2 0 00-2-2H5a2 2 0 00-2 2v14a2 2 0 002 2h4" />
|
||||
<path d="M3 9h18" />
|
||||
<path d="M8 2v3" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 413 B After Width: | Height: | Size: 407 B |
@@ -12,5 +12,5 @@
|
||||
<path d="M12 14v4" />
|
||||
<path d="M14.172 2a2 2 0 0 1 1.414.586l3.828 3.828A2 2 0 0 1 20 7.828V20a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2z" />
|
||||
<path d="M8 14h8" />
|
||||
<rect x="8" y="10" width="8" height="8" rx="1" />
|
||||
<rect x="8" y="10" width="8" height="8" rx="2" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 433 B After Width: | Height: | Size: 433 B |
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"$schema": "../icon.schema.json",
|
||||
"contributors": [
|
||||
"AnnaSasDev",
|
||||
"briz123",
|
||||
"ajokt123",
|
||||
"rrod497",
|
||||
"karsa-mistmere"
|
||||
],
|
||||
"use-cases": [
|
||||
"Representing cupcakes in bakery product catalogs",
|
||||
"Filtering dessert options in food ordering apps",
|
||||
"Marking treats for parties or celebrations"
|
||||
],
|
||||
"tags": [
|
||||
"cake",
|
||||
"dessert",
|
||||
"food",
|
||||
"celebration",
|
||||
"sweet",
|
||||
"baking",
|
||||
"cooking"
|
||||
],
|
||||
"categories": [
|
||||
"food-beverage"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M12 22v-9" />
|
||||
<path d="M14 4h1a3 3 0 013 3l-.004.125A4 4 0 0121 11v2" />
|
||||
<path d="m15.5 22 1.5-9" />
|
||||
<path d="M21 13a1 1 0 01.919 1.394l-2.74 6.394A2 2 0 0117.34 22H6.659a2 2 0 01-1.838-1.212l-2.74-6.394A1 1 0 013 13z" />
|
||||
<path d="M3 13v-2a4 4 0 013.003-3.875L6 7a3 3 0 013-3h1" />
|
||||
<path d="M8.5 22 7 13" />
|
||||
<circle cx="12" cy="4" r="2" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 571 B |