Compare commits

..
Author SHA1 Message Date
Jakob Guddas 4f94ac6b77 docs: lower cased and at ref to rfc 2026-09-13 23:00:35 +02:00
Jakob Guddas 93dd84c0d0 Apply suggestion from @jguddas 2026-09-13 15:27:43 +02:00
Jakob Guddas feb9abc633 Update design-principles.md 2026-09-13 15:12:42 +02:00
Jakob Guddas 44438b8d8a docs: remove specification 2026-09-13 15:05:41 +02:00
176 changed files with 2385 additions and 3595 deletions
@@ -298,11 +298,11 @@ Link text should describe the destination or concept.
**Do:**
> See the [Icon Design Specification](./specification) for exact dimensions and spacing requirements.
> See the [Icon Design Guide](./design-principles) for exact dimensions and spacing requirements.
**Don't:**
> For more information, [click here](./specification).
> For more information, [click here](./design-principles).
Avoid generic link text such as:
-55
View File
@@ -1,55 +0,0 @@
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
-16
View File
@@ -46,19 +46,3 @@ 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
-16
View File
@@ -37,19 +37,3 @@ 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
-16
View File
@@ -29,19 +29,3 @@ 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
-16
View File
@@ -31,19 +31,3 @@ 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
-16
View File
@@ -47,19 +47,3 @@ 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
-16
View File
@@ -70,19 +70,3 @@ 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
+1 -17
View File
@@ -27,20 +27,4 @@ jobs:
run: pnpm install --frozen-lockfile
- name: 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
run: pnpm --filter lucide-react test
-16
View File
@@ -47,19 +47,3 @@ 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
-16
View File
@@ -26,19 +26,3 @@ 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
+1 -1
View File
@@ -61,5 +61,5 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Typecheck
- name: Test
run: pnpm --filter @lucide/svelte typecheck
-16
View File
@@ -47,19 +47,3 @@ 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
-16
View File
@@ -46,19 +46,3 @@ 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
View File
@@ -1,7 +1,6 @@
.DS_Store
.idea
.next
.astro
.obsidian
.now
.cache
+2 -1
View File
@@ -1,12 +1,13 @@
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.map(([name, { key, ...attrs }]) => {
const newIconNode = (iconNode as IconNodeWithKeys).map(([name, { key, ...attrs }]) => {
return [name, attrs];
});
+2 -1
View File
@@ -1,5 +1,6 @@
import { eventHandler, getQuery, setResponseHeader } from 'h3';
import iconNodes from '../../data/iconNodes';
import { IconNodeWithKeys } from '../../theme/types';
export default eventHandler((event) => {
const query = getQuery(event);
@@ -18,7 +19,7 @@ export default eventHandler((event) => {
return [name, iconNode];
}
const newIconNode = iconNode.map(([name, { key, ...attrs }]) => {
const newIconNode = (iconNode as IconNodeWithKeys).map(([name, { key, ...attrs }]) => {
return [name, attrs];
});
+1 -1
View File
@@ -97,7 +97,7 @@ export default defineConfig({
{
text: 'Resources',
items: [
...resourcesSidebar[0].items.map(({ text, link }) => ({ text, link })),
...resourcesSidebar[0].items,
{ text: 'How to use icons', link: '/how-to/' },
{ text: 'Contributing icons', link: '/contribute/icons/' },
],
+3 -2
View File
@@ -1,3 +1,4 @@
import { SVGProps } from 'react';
import { getCommands } from './utils';
export type Point = { x: number; y: number };
@@ -14,8 +15,8 @@ export type Path = {
};
export type PathProps<
RequiredProps extends keyof React.SVGProps<SVGPathElement | SVGRectElement | SVGCircleElement>,
NeverProps extends keyof React.SVGProps<SVGPathElement | SVGRectElement | SVGCircleElement>,
RequiredProps extends keyof SVGProps<SVGPathElement | SVGRectElement | SVGCircleElement>,
NeverProps extends keyof SVGProps<SVGPathElement | SVGRectElement | SVGCircleElement>,
> = Required<Pick<React.SVGProps<SVGElement & SVGRectElement & SVGCircleElement>, RequiredProps>> &
Omit<
React.SVGProps<SVGPathElement & SVGRectElement & SVGCircleElement>,
+4 -1
View File
@@ -53,7 +53,10 @@ export const getCommands = (src: string) =>
getNodes(src)
.map(convertToPathNode)
.flatMap(({ d, name }, idx) =>
new SVGPathData(d).toAbs().commands.map((c, cIdx) => ({ ...c, id: idx, idx: cIdx, name })),
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 })),
);
const getPaths = (src: string) => {
+3 -3
View File
@@ -4,7 +4,7 @@ import { Category, IconEntity } from '../theme/types';
const directory = path.join(process.cwd(), '../categories');
export function getAllCategoryFiles(): Omit<Category, 'iconCount'>[] {
export function getAllCategoryFiles(): Category[] {
const fileNames = fs.readdirSync(directory).filter((file) => path.extname(file) === '.json');
return fileNames.map((fileName) => {
@@ -21,9 +21,9 @@ export function getAllCategoryFiles(): Omit<Category, 'iconCount'>[] {
}
export function mapCategoryIconCount(
categories: Omit<Category, 'iconCount'>[],
categories: Category[],
icons: { categories: IconEntity['categories'] }[],
): Category[] {
) {
return categories.map((category) => ({
...category,
iconCount: icons.reduce(
+1 -2
View File
@@ -1,5 +1,4 @@
import React from 'react';
import createLucideIcon from 'lucide-react/src/createLucideIcon';
import { createLucideIcon } from 'lucide-react/src/lucide-react';
import { type LucideProps, type IconNode } from 'lucide-react/src/types';
import { IconEntity } from '../theme/types';
import { renderToStaticMarkup } from 'react-dom/server';
-2
View File
@@ -20,8 +20,6 @@ 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,
+2 -1
View File
@@ -1,6 +1,7 @@
import { readFileSync } from 'node:fs';
import { dirname, isAbsolute, resolve } from 'node:path';
import type { MarkdownIt, RenderRule } from './types';
import type MarkdownIt from 'markdown-it';
import type { RenderRule } from 'markdown-it/lib/renderer.mjs';
import container from 'markdown-it-container';
type ContainerArgs = [typeof container, string, { render: RenderRule }];
+3 -1
View File
@@ -1,4 +1,6 @@
import type { MarkdownIt, RenderRule, Token } from './types';
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 container from 'markdown-it-container';
import sandpackTheme from '../theme/sandpackTheme.json';
+1 -1
View File
@@ -16,7 +16,7 @@
* />
*/
import type { MarkdownIt } from './types';
import type MarkdownIt from 'markdown-it';
type SnackParams = Record<string, string>;
-7
View File
@@ -1,7 +0,0 @@
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];
-5
View File
@@ -60,11 +60,6 @@ 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 Button from './base/Button.vue';
import IconButton from './base/IconButton.vue';
import { x } from '../../data/iconNodes';
import Icon from '@lucide/vue/src/Icon';
import { computed, onMounted, ref } from 'vue';
@@ -65,17 +65,16 @@ function hidePopup() {
}"
class="ask-overlay"
>
<Button
<IconButton
@click="hidePopup"
class="hide-button"
>
<Icon
:iconNode="x"
name="x"
:size="20"
absoluteStrokeWidth
/>
</Button>
</IconButton>
<h2 class="title">
We value your feedback!
@@ -1,38 +1,7 @@
<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>
<component
:is="component"
v-bind="$attrs"
:href="href"
:target="target"
:rel="rel"
class="icon-button"
>
<button v-bind="$attrs" class="icon-button">
<slot />
</component>
</button>
</template>
<style scoped>
@@ -47,21 +16,12 @@ const rel = computed(() => {
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; */
gap: 8px;
font-size: 16px;
font-weight: 500;
font-size: 24px;
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 Button from './Button.vue';
import IconButton from './IconButton.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 as HTMLInputElement).value)"
@input="$emit('update:modelValue', $event.target.value)"
/>
<Button
<IconButton
@click="onClear"
v-if="type === 'search' && modelValue"
class="clear-button"
@@ -79,10 +79,9 @@ defineExpose({
>
<Icon
:iconNode="x"
name="x"
:size="20"
/>
</Button>
</IconButton>
<kbd
v-if="shortcut"
class="shortcut"
@@ -46,7 +46,6 @@ const value = computed({
<template #icon>
<Icon
:iconNode="search"
name="search"
class="search-icon"
/>
</template>
@@ -9,8 +9,7 @@ import RocketDuotoneIcon from '../home/RocketDuotoneIcon.ts';
<p>
<Icon
:iconNode="RocketDuotoneIcon"
name="rocket-duotone"
:size="18"
size="18"
class="rocket-icon"
/>
<VPLink
@@ -1,74 +0,0 @@
<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 as HTMLInputElement).value))"
v-on:input="$emit('update:modelValue', Number($event.target.value))"
:min="min"
:max="max"
:step="step"
@@ -1,17 +1,16 @@
<script setup lang="ts">
import { rotateCw } from '../../../data/iconNodes';
import Icon from '@lucide/vue/src/Icon';
import Button from './Button.vue';
import IconButton from './IconButton.vue';
</script>
<template>
<Button class="reset-button">
<IconButton class="reset-button">
<Icon
:size="20"
:iconNode="rotateCw"
name="rotate-cw"
/>
</Button>
</IconButton>
</template>
<style scoped>
@@ -27,7 +27,6 @@ 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"
/>
@@ -58,7 +57,6 @@ 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/src/types';
import { type LucideProps } from '@lucide/vue';
import { Sandpack } from 'sandpack-vue3';
defineProps<FunctionalComponent<LucideProps>>();
@@ -10,14 +10,7 @@ import { useLocalStorage } from '@vueuse/core';
const { page } = useData();
const router = useRouter();
interface Framework {
name: string;
icon: string;
iconDark?: string;
value: string;
}
const frameworks: Framework[] = [
const frameworks = [
{ 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/' },
@@ -55,7 +48,7 @@ const selected = computed(() => {
return current || (fallbackFramework.value?.value ? fallbackFramework.value : frameworks[0]);
});
function onSelectFramework(item: Framework) {
function onSelectFramework(item: { name: string; icon: string; iconDark?: string; value: string }) {
fallbackFramework.value = item;
if (item.value !== router.route.path) {
const likeRoute = router.route.path.replace(selected.value.value, item.value);
@@ -2,12 +2,9 @@
import { ref, shallowRef } from 'vue';
import { data } from './HomeHeroIconsCard.data';
import { useRouter } from 'vitepress';
import { motion, useScroll, useTransform, type Options as MotionOptions } from 'motion-v';
import { motion, Variants, useScroll, useTransform } 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);
@@ -132,7 +129,7 @@ const onAnimationComplete = (item) => {
const randomIndex = ref(Math.floor(Math.random() * 64));
const iconAnimationVariants: Variants = {
const iconAnimationVariants = {
initial: {
animationName: 'end',
opacity: 0,
@@ -152,7 +149,7 @@ const iconAnimationVariants: Variants = {
const shrinkIconAnimation = ref('initial');
const shrinkIconVariants: Variants = {
const shrinkIconVariants = {
initial: { strokeWidth: 2 },
shrinkIcons: (index) => ({
animationName: 'shrinkIcons',
@@ -69,7 +69,7 @@ const pickIcons = [
'stethoscope',
];
const loader = {
export default {
async load() {
const icons = Object.entries(iconNodes).map(([name, iconNode]) => ({ name, iconNode }));
@@ -96,8 +96,3 @@ const loader = {
};
},
};
declare const data: Awaited<ReturnType<typeof loader.load>>;
export { data };
export default loader;
@@ -1,4 +1,4 @@
const loader = {
export default {
async load() {
const version = await fetch('https://api.github.com/repos/lucide-icons/lucide/releases/latest')
.then((res) => {
@@ -16,8 +16,3 @@ const loader = {
};
},
};
declare const data: Awaited<ReturnType<typeof loader.load>>;
export { data };
export default loader;
@@ -15,11 +15,10 @@ import RocketDuotoneIcon from './RocketDuotoneIcon.ts';
class="badge-special confetti-button animate"
href="/guide/version-1"
>
<Icon :iconNode="RocketDuotoneIcon" name="rocket-duotone" />
<Icon :iconNode="RocketDuotoneIcon" />
Version 1
<Icon
:iconNode="moveRight"
name="move-right"
class=""
/>
</Badge>
@@ -41,8 +41,8 @@ const sizeCssVar = useCssVar(
)
syncRef(color, colorCssVar)
syncRef(strokeWidth, strokeWidthCssVar, { transform: { ltr: String, rtl: Number } })
syncRef(size, sizeCssVar, { transform: { ltr: String, rtl: Number } })
syncRef(strokeWidth, strokeWidthCssVar)
syncRef(size, sizeCssVar)
function resetStyle () {
color.value = 'currentColor'
@@ -1,4 +1,4 @@
const loader = {
export default {
async load() {
return {
packages: [
@@ -61,8 +61,3 @@ const loader = {
};
},
};
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,6 +1,4 @@
import type { LucideIconNode } from '@lucide/vue/src/types';
const RocketDuotoneIcon: LucideIconNode[] = [
const RocketDuotoneIcon = [
[
'path',
{
@@ -18,6 +16,6 @@ const RocketDuotoneIcon: LucideIconNode[] = [
'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 Button from '../base/Button.vue';
import IconButton from '../base/IconButton.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,17 +40,16 @@ onMounted(() => {
class="floating-ad"
v-if="theme.carbonAds"
>
<Button
<IconButton
@click="hideAd"
class="hide-button"
>
<Icon
:iconNode="x"
name="x"
:size="20"
absoluteStrokeWidth
/>
</Button>
</IconButton>
<VPDocAsideCarbonAds :carbon-ads="theme.carbonAds" overlay />
</div>
</template>
@@ -1,17 +1,14 @@
import { getAllCategoryFiles, mapCategoryIconCount } from '../../../lib/categories';
import iconsMetaData from '../../../data/iconMetaData';
const loader = {
export default {
async load() {
const categories = mapCategoryIconCount(getAllCategoryFiles(), Object.values(iconsMetaData));
let categories = getAllCategoryFiles();
categories = mapCategoryIconCount(categories, 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.js'
import { isActive } from 'vitepress/dist/client/shared'
import { useActiveAnchor } from '../../composables/useActiveAnchor'
import { data } from './CategoryList.data'
import CategoryListItem from './CategoryListItem.vue'
@@ -1,29 +0,0 @@
<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,15 +30,10 @@ const props = defineProps<{
.contributors {
display: flex;
flex-direction: row;
align-items: start;
align-items: center;
/* justify-content: flex-end; */
gap: 2px;
gap: 16px;
}
.contributors .label {
margin-top: 12px;
}
.avatar-group {
display: flex;
}
@@ -16,17 +16,19 @@ const tags = computed(() => {
</script>
<template>
<IconPreview
:name="$params.name"
:iconNode="$params.iconNode"
class="preview"
customizable
/>
<div class="details">
<h1 class="title">
{{ $params.name }}
</h1>
</div>
<!-- <PageContainer class="overview"> -->
<IconPreview
:name="$params.name"
:iconNode="$params.iconNode"
class="preview"
customizable
/>
<div class="details">
<h1 class="title">
{{ $params.name }}
</h1>
</div>
<!-- </PageContainer> -->
</template>
<style scoped>
@@ -6,11 +6,7 @@ import useConfetti from '../../composables/useConfetti';
const { animate, confetti } = useConfetti();
const slots = useSlots();
const copiedText = computed(() => {
const children = slots.default?.()[0].children;
return typeof children === 'string' ? children : '';
});
const copiedText = computed(() => slots.default?.()[0].children);
function copyText() {
navigator.clipboard.writeText(copiedText.value);
@@ -29,7 +25,6 @@ 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 Button from '../base/Button.vue';
import IconButton from '../base/IconButton.vue';
import IconContributors from './IconContributors.vue';
import IconPreview from './IconPreview.vue';
import { x, expand } from '../../../data/iconNodes';
@@ -12,7 +12,6 @@ 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;
@@ -71,19 +70,12 @@ const Expand = createLucideIcon('Expand', expand);
<div class="overlay-panel">
<nav class="overlay-menu">
<Badge
v-if="icon.awaitingRelease"
class="version awaiting-release"
>Unreleased</Badge
>
<Badge
v-else-if="icon.createdRelease"
v-if="icon.createdRelease"
class="version"
:href="releaseTagLink(icon.createdRelease.version)"
>v{{ icon.createdRelease.version }}</Badge
>
{{ icon.createdRelease.version }}
</Badge>
<EditInStudioButton :icon="icon"/>
<Button
<IconButton
@click="
go(
icon.externalLibrary
@@ -93,10 +85,10 @@ const Expand = createLucideIcon('Expand', expand);
"
>
<component :is="Expand" />
</Button>
<Button @click="onClose">
</IconButton>
<IconButton @click="onClose">
<component :is="CloseIcon" />
</Button>
</IconButton>
</nav>
<IconPreview
id="previewer"
@@ -208,14 +200,7 @@ 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,36 +10,30 @@ 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 as IconDeprecationReason, {
return deprecationReasonTemplate(props.icon.deprecationReason, {
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>
@@ -66,7 +60,14 @@ const iconPath = computed(() => {
Deprecated
</Badge>
</div>
<IconTags :tags="props.icon.tags" />
<div
class="tags-scroller"
v-if="tags.length"
>
<p class="icon-tags horizontal-scroller">
{{ tags }}
</p>
</div>
<div class="group">
<Badge
v-for="category in icon.categories"
@@ -79,10 +80,26 @@ const iconPath = computed(() => {
<div class="group buttons">
<VPButton
v-if="!page?.relativePath?.startsWith?.(iconPath)"
:href="iconPath"
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}`
"
text="See in action"
@click="go(iconPath)"
@click="
go(
icon.externalLibrary
? `/icons/${icon.externalLibrary}/${icon.name}`
: `/icons/${icon.name}`,
)
"
/>
<CopySVGButton
:name="icon.name"
@@ -98,38 +115,6 @@ const iconPath = 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;
@@ -171,6 +156,66 @@ const iconPath = 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,6 +3,7 @@ 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'];
@@ -10,8 +11,6 @@ const props = defineProps<{
customizable?: boolean;
}>();
const activeSize = ref('1000%');
const { size, color, strokeWidth, absoluteStrokeWidth } = useIconStyleContext();
const previewIcon = ref();
@@ -21,6 +20,31 @@ 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>
@@ -31,8 +55,6 @@ const iconComponent = computed(() => {
:size="size"
:color="color"
:strokeWidth="absoluteStrokeWidth ? (Number(strokeWidth) * 24) / Number(size) : strokeWidth"
class="preview-icon"
:data-size="activeSize"
/>
<svg
class="icon-grid"
@@ -40,7 +62,6 @@ const iconComponent = computed(() => {
fill="none"
stroke-width="0.1"
xmlns="http://www.w3.org/2000/svg"
:class="{ show: activeSize === '1000%' }"
>
<g
:key="`grid-${i}`"
@@ -62,17 +83,16 @@ const iconComponent = computed(() => {
/>
</g>
</svg>
<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>
<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>
</template>
@@ -84,87 +104,56 @@ const iconComponent = 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:hover .size-buttons {
opacity: 1;
}
.icon-container > :deep(svg.preview-icon) {
.icon-container > :deep(svg:not(.icon-grid)) {
width: 100%;
height: 100%;
position: relative;
z-index: 1;
color: var(--vp-c-neutral);
opacity: 0.8;
transition: all 0.3s;
margin: auto;
}
.icon-container > :deep(svg.preview-icon[data-size='1000%']) {
width: 100%;
height: 100%;
.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='200%']) {
width: 48px;
height: 48px;
.icon-container:hover .studio-button,
.studio-button:focus-visible {
opacity: 1;
}
.icon-container > :deep(svg.preview-icon[data-size='100%']) {
width: 24px;
height: 24px;
.studio-button:hover,
.studio-button:focus-visible {
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
}
.icon-component.customizable {
will-change: width, height, stroke-width, stroke;
}
.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;
/* color: var(--customize-color, currentColor);
stroke-width: var(--customize-strokeWidth, 2); */
}
</style>
@@ -70,6 +70,7 @@ 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>
@@ -235,17 +236,18 @@ const prettyName = props.name
.showcase-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 32px;
background: var(--vp-code-block-bg);
padding: 32px;
border-radius: 8px;
gap: 48px;
margin-inline-start: 24px;
margin-block-start: 48px;
}
.showcase-item {
padding: 24px;
border-radius: 8px;
background-color: var(--vp-c-bg);
box-shadow: var(--vp-shadow-2);
box-shadow:
var(--vp-shadow-4),
-24px -24px 0 var(--vp-c-bg-soft);
display: flex;
align-items: center;
justify-content: center;
@@ -1,110 +0,0 @@
<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.href);
const url = new URL(window.location);
url.pathname = '/icons/categories';
if (searchQueryDebounced.value) {
@@ -35,6 +35,7 @@ const SORTING = [
},
]
const initialGridItems = computed(() => {
if (containerWidth.value === 0) return 120;
@@ -71,8 +72,6 @@ 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;
@@ -173,7 +172,7 @@ watch(searchQueryDebounced, () => {
function handleCloseDrawer() {
setActiveIconName('');
const url = new URL(window.location.href);
const url = new URL(window.location);
url.pathname = '/icons/';
if (searchQueryDebounced.value) {
@@ -208,7 +207,6 @@ 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: readonly Placeholder[] = shallowReadonly([
const brandPlaceholders: Placeholder[] = shallowReadonly([
{
title: 'Boooo! What a scary brand logo!',
message:
@@ -103,7 +103,7 @@ const brandPlaceholders: readonly Placeholder[] = shallowReadonly([
},
]);
const notFoundPlaceholders: readonly Omit<Placeholder, 'title'>[] = shallowReadonly([
const notFoundPlaceholders: 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: readonly Omit<Placeholder, 'title'>[] = shallowReado
},
]);
function randomItem<T>(arr: readonly T[]): T {
function randomItem<T>(arr: T[]): T {
return arr[Math.floor(Math.random() * arr.length)];
}
@@ -9,6 +9,7 @@ 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', transform: { ltr: String } });
syncRef(size, sizeCssVar, { direction: 'ltr', transform: { ltr: String } });
syncRef(strokeWidth, strokeWidthCssVar, { direction: 'ltr' });
syncRef(size, sizeCssVar, { direction: 'ltr' });
function resetStyle() {
color.value = STYLE_DEFAULTS.color;
@@ -61,50 +61,6 @@
}
}
.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;
@@ -112,7 +68,4 @@
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.js'
import { useData } from 'vitepress/dist/client/theme-default/composables/data'
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/src/types';
import { IconNode } from 'lucide';
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.js';
import { useSidebar } from 'vitepress/dist/client/theme-default/composables/sidebar';
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';
const loader = {
export default {
async load() {
const packageJsonList = await fetchPackages();
@@ -29,8 +29,3 @@ const loader = {
};
},
};
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.js';
import { throttleAndDebounce } from 'vitepress/dist/client/theme-default/support/utils';
/*
* This file is compied and adjusted from vitepress/dist/client/theme-default/composables/useActiveAnchor.ts
@@ -1,10 +1,10 @@
import Fuse, { type FuseOptionKeyObject } from 'fuse.js';
import Fuse from 'fuse.js';
import { shallowRef, computed, Ref } from 'vue';
const useSearch = <T>(
query: Ref<string>,
collection: Ref<T[]>,
keys: FuseOptionKeyObject<T>[] = [],
keys: Fuse.FuseOptionKeyObject<T>[] = [],
) => {
const index = shallowRef(
new Fuse(collection.value, {
-4
View File
@@ -152,10 +152,6 @@ html {
.VPHomeHero .container {
flex-direction: row;
}
.VPDoc:has(.vp-doc[class*=' _icons_']) {
padding: 32px 32px 0;
}
}
.VPNavBarHamburger .container > span {
+2 -11
View File
@@ -1,17 +1,11 @@
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?: Alias[];
aliases?: string[];
deprecated?: boolean;
deprecationReason?: string;
toBeRemovedInVersion?: string;
@@ -24,8 +18,6 @@ export interface IconEntity extends IconMetaData {
externalLibrary?: ExternalLibs;
createdRelease?: Release;
changedRelease?: Release;
fromFork?: boolean;
awaitingRelease?: boolean;
popularity?: number;
}
@@ -51,8 +43,7 @@ export interface PackageItem {
packageDirname?: string;
description: string;
icon: string;
iconDark?: string;
iconClass?: string;
iconDark: string;
shields: Shield[];
source: string;
documentation: string;
+2 -2
View File
@@ -1,5 +1,5 @@
const chunkArray = <ItemType>(stream: ItemType[], size: number) => {
return stream.reduce<ItemType[][]>(
const chunkArray = <ArrayType>(stream: ArrayType, size: number) => {
return stream.reduce<ArrayType[][]>(
(chunks, item, idx, arr) =>
idx % size == 0 ? [...chunks, arr.slice(idx, idx + size)] : chunks,
[],
@@ -1,25 +0,0 @@
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}`;
}
-5
View File
@@ -1,4 +1,3 @@
/// <reference types="vitepress/client" />
import { type IconNode } from '@lucide/vue/src/types';
import Vue from 'vue';
@@ -32,11 +31,7 @@ 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
+60 -25
View File
@@ -8,7 +8,9 @@ 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.
For the full rule list, see the [icon design specification](./specification)
::: 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/).
:::
## 1. Use a 24 × 24-pixel canvas
@@ -94,7 +96,9 @@ Don't use **inside or outside** strokes. They change how line joins appear, crea
## 7. Use consistent corner radii
Almost every sharp corner **should** be rounded.
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
:::: example
::: do ../../images/border-radius-correct.svg?raw=true
@@ -105,29 +109,44 @@ Don't use **sharp corners**.
:::
::::
Corner radius depends on the size of the element and the angle. For 90° angles:
### 7.2. Elements at least 8 pixels wide or tall **should** use a 2-pixel corner radius for 90° corners
:::: 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.
:::
::: do ../../images/1px-border-radius.svg?raw=true
::::
### 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
:::
::::
There are exceptions:
### 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.
:::: 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.
:::
::: 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.
:::
::::
Avoid rounding corners **where multiple lines meet**.
### 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
:::
::::
### 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.
:::: example
::: do ../../images/border-radius-line-join.svg?raw=true
@@ -138,9 +157,15 @@ 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
Distinct elements **must** have at least 2 pixels of visual spacing between them.
### 8.1. Distinct elements **must** have at least 2 pixels of visual spacing between them
:::: example
::: do ../../images/2px-element-spacing.svg?raw=true
@@ -151,7 +176,9 @@ Don't use gaps smaller than **2 pixels** between distinct elements.
:::
::::
This also applies inside shapes. Most inner gaps **should** be at least 2 pixels wide.
### 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.
:::: example
::: do ../../images/2px-inner-spacing.svg?raw=true
@@ -173,7 +200,9 @@ If the circle **overlaps** the shape, the gap should be **wider**.
:::
::::
The 2 pixel spacing also applies when elements visually connect or intersect.
### 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.
:::: example
::: dont ../../images/2px-element-spacing-connected.svg?raw=true
@@ -184,9 +213,13 @@ 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
@@ -208,7 +241,7 @@ Place your icon next to `circle` or `square`, then blur both icons. Your icon sh
## 10. Balance icons visually
Icons should look centered and balanced within the canvas.
Icons **should** be visually centered within the canvas.
:::: example
::: do ../../images/visually-centered-do.svg?raw=true
@@ -221,7 +254,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.
@@ -229,14 +262,14 @@ Don't place symmetrical icons off-center for no good reason.
::::
::: tip
Geometric centering does not always look centered. Asymmetrical shapes may need a small adjustment.
Geometric centering does not always look centered. Asymmetrical icons **may** be moved slightly off center so they look visually centered.
Compare your icon with `circle` or `square` side by side and stacked vertically. If it looks shifted, adjust it.
:::
## 11. Keep density low
Lucide icons should have a similar level of detail.
Icons **should** have a level of detail similar to other Lucide icons.
:::: example
::: do ../../images/density-optimal.svg?raw=true
@@ -257,7 +290,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.
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.
:::: example
::: do ../../images/curvature-smooth.svg?raw=true
@@ -276,7 +309,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.
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.
:::: example
::: do ../../images/pixel-perfection-ideal.svg?raw=true
@@ -306,12 +339,14 @@ Avoid placing diagonal lines off-grid. They will appear blurry and optically wid
::::
::: tip
Pixel alignment should support the icon. Do not let it harm shape recognition, smooth curves, or visual balance.
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.
:::
## 14. Reuse established shapes
When creating a variant, keep the base icon geometry unless the new idea needs a change.
### 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.
:::: example
::: do ../../images/established-shapes-base-do.svg?raw=true
@@ -322,7 +357,7 @@ Don't change the **geometry, placement, or orientation** of base shapes.
:::
::::
Related icons should share shapes and visual patterns when possible.
### 14.2. Related icons **should** share shapes and visual patterns when possible
:::: example
::: do ../../images/established-shapes-sub-do.svg?raw=true
@@ -333,7 +368,7 @@ Don't redesign **existing elements**.
:::
::::
Common modifiers and elements should look consistent across the library.
### 14.3. Common modifiers and elements **should** look consistent across the library
:::: example
::: do ../../images/established-shapes-consistency-do.svg?raw=true
@@ -347,7 +382,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.
Consistency with an existing icon should not override clarity, balance, or readability. Adjust the shape when needed.
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.
:::
## Summary
-95
View File
@@ -1,95 +0,0 @@
---
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.
+93 -101
View File
@@ -12,7 +12,6 @@ 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'
@@ -20,11 +19,9 @@ 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()
@@ -52,92 +49,75 @@ function releaseTagLink(version) {
}
</script>
<div class="layout">
<div class="actions">
<EditInStudioButton :icon="params" />
</div>
<div class="iconPreviews">
<div :class="$style.layout">
<div :class="$style.iconPreviews">
<IconPreview
id="previewer"
:name="params.name"
:iconNode="params.iconNode"
class="preview"
:class="$style.preview"
/>
<IconPreviewSmall
:name="params.name"
:iconNode="params.iconNode"
:class="$style.smallPreview"
/>
</div>
<div>
<div class="info">
<div >
<div :class="$style.info">
<IconInfo :icon="params" />
<div class="meta">
<div :class="$style.meta">
<div
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"
v-if="params.createdRelease?.version"
:class="$style.version"
>
<Label>Created:</Label>
<Badge
:href="releaseTagLink(params.createdRelease.version)"
>
{{params.createdRelease.version}}
v{{params.createdRelease.version}}
</Badge>
</div>
<div
v-if="!params.awaitingRelease && params.changedRelease?.version"
class="version"
v-if="params.changedRelease?.version"
:class="$style.version"
>
<Label>Last changed:</Label>
<Badge
:href="releaseTagLink(params.changedRelease.version)"
>
{{params.changedRelease.version}}
v{{params.changedRelease.version}}
</Badge>
</div>
<IconContributors :icon="params" class="contributors"/>
<IconContributors :icon="params" :class="$style.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="code"
:class="$style.code"
>
<div
class="blocks"
v-html="codeExample"
/>
</CodeGroup>
</template>
</div>
</div>
<template #tab-2>
<IconShowcase
:name="params.name"
:iconNode="params.iconNode"
/>
</template>
</PageTabs>
<div class="icon-page-sections">
<IconShowcase
:name="params.name"
:iconNode="params.iconNode"
/>
<CarbonAdOverlay :drawerOpen="false" />
<RelatedIcons
v-if="params.relatedIcons"
:icons="params.relatedIcons"
/>
</div>
<style scoped>
<style module>
.preview {
grid-area: preview;
margin-bottom: 24px;
@@ -150,52 +130,31 @@ function releaseTagLink(version) {
align-items: flex-start;
}
.actions {
text-align: right;
display: flex;
justify-content: flex-end;
margin: -12px -12px 24px 0;
}
.meta {
display: flex;
gap: 24px;
align-items: flex-start;
margin-left: auto;
margin-top: 24px;
}
.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 .awaiting-release {
color: var(--vp-c-danger-3);
.version:first-child {
margin-bottom: 8px;
}
.iconPreviews {
display: flex;
justify-content: center;
justify-content: flex-start;
gap: 24px;
}
@@ -219,13 +178,6 @@ function releaseTagLink(version) {
.iconPreviews {
flex-direction: column;
justify-content: flex-start;
}
.actions {
position: absolute;
right: 0;
margin: 0;
}
.smallPreview {
@@ -233,6 +185,61 @@ 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>
@@ -242,19 +249,4 @@ 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>
+11 -22
View File
@@ -3,28 +3,17 @@ import * as iconDetails from '../.vitepress/data/iconDetails';
export default {
async load() {
return {
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',
),
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,
}),
),
};
},
};
+2 -1
View File
@@ -1,10 +1,11 @@
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).map(([name, iconNode]) => {
return (Object.entries(iconNodes) as unknown as IconEntity[]).map(([name, iconNode]) => {
const iconDetails = iconMetaData[name];
delete iconDetails['$schema'];
+4 -7
View File
@@ -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 --env-file-if-exists=.env ./scripts/writeiconPopularity.mts && node ./scripts/writeiconPopularityIndex.mts",
"prebuild:iconPopularity": "node ./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,8 +24,7 @@
"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",
"typecheck": "vue-tsc --noEmit"
"preview": "node .output/server/index.mjs"
},
"author": "Eric Fennis",
"license": "ISC",
@@ -48,11 +47,9 @@
"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",
"vue-tsc": "^3.3.11"
"vitepress-plugin-llms": "^1.13.5"
},
"dependencies": {
"@floating-ui/vue": "^2.0.1",
@@ -71,6 +68,6 @@
"react": "18.3.1",
"react-dom": "18.3.1",
"sandpack-vue3": "3.1.12",
"vue": "^3.5.43"
"vue": "^3.5.42"
}
}
+2 -3
View File
@@ -27,13 +27,12 @@ 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 }: IconMetaData = metaData;
const { tags, categories, contributors, aliases, deprecated, deprecationReason, toBeRemovedInVersion } = metaData;
const iconDetails = {
name: '${iconName}',
iconNode: iconNode as IconNode,
iconNode,
contributors,
tags,
categories,
+3 -10
View File
@@ -20,7 +20,6 @@ if (!fs.existsSync(iconNodesDirectory)) {
const iconIndexFile = path.resolve(iconNodesDirectory, `index.ts`);
const iconIndexFileImports = [];
const iconIndexFileNodes = [];
const iconIndexFileExports = [];
const iconIndexFileDefaultExports = [];
@@ -33,10 +32,7 @@ const writeIconFiles = Object.entries(icons).map(async ([iconName, { children }]
await fs.promises.writeFile(location, output, 'utf-8');
iconIndexFileImports.push(
`import ${toCamelCase(iconName)}Json from './${iconName}.node.json' with { type: "json" };`,
);
iconIndexFileNodes.push(
`const ${toCamelCase(iconName)}Node = ${toCamelCase(iconName)}Json as IconNode;`,
`import ${toCamelCase(iconName)}Node from './${iconName}.node.json' with { type: "json" };`,
);
iconIndexFileExports.push(` ${toCamelCase(iconName)}Node as ${toCamelCase(iconName)},`);
iconIndexFileDefaultExports.push(` '${iconName}': ${toCamelCase(iconName)}Node,`);
@@ -47,19 +43,16 @@ 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',
);
+3 -10
View File
@@ -20,7 +20,6 @@ if (!fs.existsSync(iconNodesDirectory)) {
const iconIndexFile = path.resolve(iconNodesDirectory, `index.ts`);
const iconIndexFileImports: string[] = [];
const iconIndexFileNodes: string[] = [];
const iconIndexFileExports: string[] = [];
const iconIndexFileDefaultExports: string[] = [];
@@ -33,10 +32,7 @@ const writeIconFiles = Object.entries(icons).map(async ([iconName, { children }]
await fs.promises.writeFile(location, output, 'utf-8');
iconIndexFileImports.push(
`import ${toCamelCase(iconName)}Json from './${iconName}.node.json' with { type: "json" };`,
);
iconIndexFileNodes.push(
`const ${toCamelCase(iconName)}Node = ${toCamelCase(iconName)}Json as IconNode;`,
`import ${toCamelCase(iconName)}Node from './${iconName}.node.json' with { type: "json" };`,
);
iconIndexFileExports.push(` ${toCamelCase(iconName)}Node as ${toCamelCase(iconName)},`);
iconIndexFileDefaultExports.push(` '${iconName}': ${toCamelCase(iconName)}Node,`);
@@ -47,19 +43,16 @@ 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',
);
+5 -26
View File
@@ -40,21 +40,7 @@ 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];
@@ -164,26 +150,19 @@ try {
const iconName = path.basename(iconJsonFile, '.json');
const metaDir = path.resolve(releaseMetaDataDirectory, `${iconName}.json`);
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) {
if (!(iconName in newReleaseMetaData)) {
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)) {
-1
View File
@@ -7,7 +7,6 @@
"allowImportingTsExtensions": true,
"allowSyntheticDefaultImports": true,
"noEmit": true,
"skipLibCheck": true,
"paths": {
"~/.vitepress/*": ["./.vitepress/*"]
}
-39
View File
@@ -1,39 +0,0 @@
{
"$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"
]
}
-16
View File
@@ -1,16 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 392 B

+1 -2
View File
@@ -3,8 +3,7 @@
"contributors": [
"rrod497",
"jamiemlaw",
"karsa-mistmere",
"jguddas"
"karsa-mistmere"
],
"use-cases": [],
"tags": [
+4 -4
View File
@@ -9,8 +9,8 @@
stroke-linecap="round"
stroke-linejoin="round"
>
<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" />
<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" />
</svg>

Before

Width:  |  Height:  |  Size: 560 B

After

Width:  |  Height:  |  Size: 572 B

+1 -1
View File
@@ -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.5V5a2 2 0 00-2-2H5a2 2 0 00-2 2v14a2 2 0 002 2h4" />
<path d="M21 11.5V5.05a2 2 0 00-2-2H5a2 2 0 00-2 2v14a2 2 0 002 2L9 21" />
<path d="M3 9h18" />
<path d="M8 2v3" />
</svg>

Before

Width:  |  Height:  |  Size: 407 B

After

Width:  |  Height:  |  Size: 413 B

+1 -1
View File
@@ -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="2" />
<rect x="8" y="10" width="8" height="8" rx="1" />
</svg>

Before

Width:  |  Height:  |  Size: 433 B

After

Width:  |  Height:  |  Size: 433 B

-27
View File
@@ -1,27 +0,0 @@
{
"$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"
]
}
-19
View File
@@ -1,19 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 571 B

-25
View File
@@ -1,25 +0,0 @@
{
"$schema": "../icon.schema.json",
"contributors": [
"karsa-mistmere"
],
"use-cases": [
"Indicating package delivery to a home or residence",
"Representing packages left at a recipient's door",
"Tracking doorstep or unattended deliveries",
"Selecting a delivery location or drop-off point"
],
"tags": [
"delivery",
"parcel",
"doorstep",
"shipping",
"drop-off",
"courier"
],
"categories": [
"home",
"food-beverage",
"transportation"
]
}
-17
View File
@@ -1,17 +0,0 @@
<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="M18 13v3" />
<path d="M19 9V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16" />
<path d="M2 21h8" />
<path d="M9 12h.01" />
<rect x="14" y="13" width="8" height="8" rx="1" />
</svg>

Before

Width:  |  Height:  |  Size: 387 B

-43
View File
@@ -1,43 +0,0 @@
{
"$schema": "../icon.schema.json",
"contributors": [
"danielbayley",
"karsa-mistmere"
],
"use-cases": [
"Indicating water sources on maps",
"Controlling faucets in smart home apps",
"Representing kitchen or bathroom plumbing fixtures",
"Showing drinking water amenities in travel apps"
],
"tags": [
"tap",
"spigot",
"water",
"drinking",
"fresh",
"source",
"pipes",
"plumbing",
"plumber",
"sink",
"basin",
"kitchen",
"bathroom",
"washroom",
"fixture",
"valve",
"flow",
"drinkable",
"potable",
"amenities",
"utilities",
"hydration"
],
"categories": [
"food-beverage",
"home",
"navigation",
"travel"
]
}
-19
View File
@@ -1,19 +0,0 @@
<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="M10.083 5.428 5.57 4.083a2 2 0 10.001 3.834l4.512-1.345" />
<path d="M12 8v3" />
<path d="m13.917 5.428 4.511-1.345a2 2 0 110 3.834l-4.51-1.345" />
<path d="M18 17v-4.006" />
<path d="M22 11v8" />
<path d="M22 12h-3a1 1 0 00-1 .994h-2.539a4 4 0 00-6.915-.012L7 13a5 5 0 00-5 5v1a1 1 0 001 1h2a1 1 0 001-1v-1a1 1 0 01.995-1l1.552.018a4 4 0 006.907 0L18 17a1 1 0 001 1h3" />
<circle cx="12" cy="6" r="2" />
</svg>

Before

Width:  |  Height:  |  Size: 636 B

-30
View File
@@ -1,30 +0,0 @@
{
"$schema": "../icon.schema.json",
"contributors": [
"jguddas",
"karsa-mistmere",
"danielbayley",
"UsamaKhan",
"ajaxjiang96"
],
"use-cases": [
"Opening household settings in smart home applications",
"Managing property configuration in real estate dashboards",
"Configuring home automation rules in IoT device management interfaces",
"Accessing home security settings in residential alarm systems"
],
"tags": [
"home",
"building",
"residence",
"settings",
"gear",
"configuration",
"property",
"automation"
],
"categories": [
"buildings",
"home"
]
}

Some files were not shown because too many files have changed in this diff Show More