Merge branch 'main' into vitepress-2-upgrade

This commit is contained in:
Eric Fennis
2026-11-06 12:45:18 +01:00
committed by GitHub
71 changed files with 1006 additions and 610 deletions
@@ -56,7 +56,7 @@ When checking or adding a `$group:` marker:
- A marker must be exactly `"$group:"` followed by the name of an existing file in `groups/`, without the `.json` extension, in lowercase kebab-case.
- `$group:` is allowed **only** inside `tags`. It is not valid in `categories` or `contributors`.
- Groups are named after the variant suffix: `file-plus` uses `$group:plus`, `file-question-mark` uses `$group:question-mark`. Suffixes with the same meaning share a group: `mail-warning` uses `$group:exclamation-mark`, and every currency suffix uses `$group:currency`.
- Groups are named after the variant suffix: `file-plus` uses `$group:plus`, `file-question-mark` uses `$group:question-mark`. Suffixes with the same meaning share a group: `mail-exclamation-point` uses `$group:exclamation-mark`, and every currency suffix uses `$group:currency`.
- The same suffix can mean different things in different contexts — `-off` means "disabled" on `wifi-off` but "allergy free" on `wheat-off` — so only use the group when the icon carries the group's meaning, and keep context-specific tags as literals.
- Do not repeat a tag a group already provides, and do not use the same group twice in one array.
- Conventional order: the `$extends:` base icon first, then `$group:` markers, then the icon's own literal tags.
+1 -1
View File
@@ -2,5 +2,5 @@
"$schema": "../category.schema.json",
"title": "Notifications",
"description": "Icons for alerts, status updates, reminders, approvals, and attention states.",
"icon": "triangle-alert"
"icon": "triangle-exclamation-point"
}
@@ -1,8 +1,9 @@
<script setup lang="ts">
import { ref, shallowRef } from 'vue';
import { onMounted, ref, shallowRef, type DefineComponent } from 'vue';
import { data } from './HomeHeroIconsCard.data';
import { useRouter } from 'vitepress';
import { motion, useScroll, useTransform, type Options as MotionOptions } from 'motion-v';
import { useMediaQuery } from '@vueuse/core';
import LucideIcon from '../base/LucideIcon.vue';
// motion-v re-exports framer-motion's `Variants`, which doesn't match the `variants` prop of its components
@@ -10,7 +11,8 @@ type Variants = MotionOptions['variants'];
const emit = defineEmits(['animation-complete']);
const MotionLucideIcon = motion.create(LucideIcon);
// motion.create only accepts a props-less DefineComponent, so the SFC's typed props need widening
const MotionLucideIcon = motion.create(LucideIcon as unknown as DefineComponent);
const preventFocusProps = {
tabindex: '-1',
@@ -21,6 +23,89 @@ const COLUMNS = 8;
const SIZE = 2;
const GAP = 1;
/** The two spirals of the Lucide logo, drawn the way the SVG editor previews them. */
const LOGO_PATH_OUTER =
'M14 12C14 9.79086 12.2091 8 10 8C7.79086 8 6 9.79086 6 12C6 16.4183 9.58172 20 14 20C18.4183 20 22 16.4183 22 12C22 8.446 20.455 5.25285 18 3.05557';
const LOGO_PATH_INNER =
'M10 12C10 14.2091 11.7909 16 14 16C16.2091 16 18 14.2091 18 12C18 7.58172 14.4183 4 10 4C5.58172 4 2 7.58172 2 12C2 15.5841 3.57127 18.8012 6.06253 21';
const VERTICES =
'M14 12h.01M10 8h.01M6 12h.01M14 20h.01M22 12h.01M18 3.05557h.01M10 12h.01M14 16h.01M18 12h.01M10 4h.01M2 12h.01M6.06253 21h.01';
const MARKERS = [
[14, 12],
[18, 3.05557],
[10, 12],
[6.06253, 21],
];
/** Bezier handles as [x1, y1, x2, y2] segments, grouped by when they appear. */
const HANDLE_GROUPS = [
// Outer spiral
{ delay: 0.2, segments: [[14, 12, 14, 9.79086]] },
{
delay: 0.4,
segments: [
[10, 8, 12.2091, 8],
[10, 8, 7.79086, 8],
],
},
{
delay: 0.6,
segments: [
[6, 12, 6, 9.79086],
[6, 12, 6, 16.4183],
],
},
{
delay: 0.8,
segments: [
[14, 20, 9.58172, 20],
[14, 20, 18.4183, 20],
],
},
{
delay: 1,
segments: [
[22, 12, 22, 16.4183],
[22, 12, 22, 8.446],
],
},
{ delay: 1.2, segments: [[18, 3.05557, 20.455, 5.25285]] },
// Inner spiral
{ delay: 0.2, segments: [[10, 12, 10, 14.2091]] },
{
delay: 0.4,
segments: [
[14, 16, 11.7909, 16],
[14, 16, 16.2091, 16],
],
},
{
delay: 0.6,
segments: [
[18, 12, 18, 14.2091],
[18, 12, 18, 7.58172],
],
},
{
delay: 0.8,
segments: [
[10, 4, 14.4183, 4],
[10, 4, 5.58172, 4],
],
},
{
delay: 1,
segments: [
[2, 12, 2, 7.58172],
[2, 12, 2, 15.5841],
],
},
{ delay: 1.2, segments: [[6.06253, 21, 3.57127, 18.8012]] },
];
const { scrollYProgress } = useScroll();
const opacity = useTransform(() => 1 - scrollYProgress.get() * 8);
@@ -101,6 +186,9 @@ const draw: Variants = {
// motion-v passes the variant label that finished, not the resolved variant
const onAnimationComplete = (variant: unknown) => {
// Reduced motion stops at the icon grid, so don't shrink the icons or loop
if (reducedMotion.value) return;
if (variant === 'visible') {
drawAnimation.value = 'exit';
return;
@@ -155,6 +243,24 @@ const shrinkIconVariants: Variants = {
transition: { delay: 1.8, duration: 1.5, ease: 'easeInOut' },
}),
};
// Only rendered on the client, so reduced motion is known before the first frame
const mounted = ref(false);
const reducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)');
onMounted(() => {
if (reducedMotion.value) {
drawAnimation.value = 'exit';
showHandles.value = false;
scaleDownAnimation.value = 'small';
iconGridAnimation.value = 'showIcons';
}
mounted.value = true;
});
/** Skips straight to the end state when the user prefers reduced motion. */
const initial = <T,>(value: T) => (reducedMotion.value ? false : value);
</script>
<template>
@@ -167,6 +273,7 @@ const shrinkIconVariants: Variants = {
v-bind="preventFocusProps"
>
<motion.svg
v-if="mounted"
xmlns="http://www.w3.org/2000/svg"
viewBox="-12 -12 48 48"
fill="none"
@@ -209,7 +316,7 @@ const shrinkIconVariants: Variants = {
<MotionLucideIcon
v-for="(icon, index) in icons"
size="2"
initial="initial"
:initial="initial('initial')"
:key="icon.name"
:variants="iconAnimationVariants"
:animate="iconGridAnimation"
@@ -223,15 +330,15 @@ const shrinkIconVariants: Variants = {
<motion.g
:variants="scaleDownVariants"
:animate="scaleDownAnimation"
initial="hidden"
:initial="initial('hidden')"
@animation-complete="onAnimationComplete"
v-bind="preventFocusProps"
>
<motion.path
d="M14 12C14 9.79086 12.2091 8 10 8C7.79086 8 6 9.79086 6 12C6 16.4183 9.58172 20 14 20C18.4183 20 22 16.4183 22 12C22 8.446 20.455 5.25285 18 3.05557"
:style="{ stroke: 'var(--vp-c-gray-1)' }"
:d="LOGO_PATH_OUTER"
:style="{ stroke: reducedMotion ? 'var(--vp-c-text-1)' : 'var(--vp-c-gray-1)' }"
:animate="drawAnimation"
initial="hidden"
:initial="initial('hidden')"
:variants="draw"
:custom="1"
@animation-complete="onAnimationComplete"
@@ -239,10 +346,10 @@ const shrinkIconVariants: Variants = {
/>
<motion.path
d="M10 12C10 14.2091 11.7909 16 14 16C16.2091 16 18 14.2091 18 12C18 7.58172 14.4183 4 10 4C5.58172 4 2 7.58172 2 12C2 15.5841 3.57127 18.8012 6.06253 21"
:style="{ stroke: 'var(--vp-c-gray-1)' }"
:d="LOGO_PATH_INNER"
:style="{ stroke: reducedMotion ? 'var(--vp-c-brand)' : 'var(--vp-c-gray-1)' }"
:animate="drawAnimation"
initial="hidden"
:initial="initial('hidden')"
:variants="draw"
:custom="0"
v-bind="preventFocusProps"
@@ -253,80 +360,36 @@ const shrinkIconVariants: Variants = {
class="svg-preview-control-path-marker-group"
:style="{ stroke: 'var(--vp-c-text-1)' }"
stroke-width="0.125"
:initial="{ opacity: 1 }"
:animate="showHandles ? { opacity: 1 } : { opacity: 0 }"
:transition="{ delay: 0, duration: 0.2 }"
:initial="initial({ opacity: 1 })"
:animate="{ opacity: showHandles ? 1 : 0 }"
:transition="{ duration: 0.2 }"
v-bind="preventFocusProps"
>
<motion.path
d="M14 12C14 9.79086 12.2091 8 10 8C7.79086 8 6 9.79086 6 12C6 16.4183 9.58172 20 14 20C18.4183 20 22 16.4183 22 12C22 8.446 20.455 5.25285 18 3.05557"
:initial="{ opacity: 0 }"
v-for="d in [LOGO_PATH_OUTER, LOGO_PATH_INNER]"
:key="d"
:d="d"
:initial="initial({ opacity: 0 })"
:animate="{ opacity: 1 }"
:transition="{ delay: 1.6, duration: 1.5 }"
v-bind="preventFocusProps"
/>
<motion.path
d="M10 12C10 14.2091 11.7909 16 14 16C16.2091 16 18 14.2091 18 12C18 7.58172 14.4183 4 10 4C5.58172 4 2 7.58172 2 12C2 15.5841 3.57127 18.8012 6.06253 21"
:initial="{ opacity: 0 }"
:animate="{ opacity: 1 }"
:transition="{ delay: 1.6, duration: 1.5 }"
v-bind="preventFocusProps"
/>
<motion.g
:initial="{ opacity: 0 }"
:d="VERTICES"
:initial="initial({ opacity: 0 })"
:animate="{ opacity: 1 }"
:transition="{ delay: 0.2, duration: 0.3 }"
v-bind="preventFocusProps"
>
<path
d="M14 12h.01M10 8h.01M10 8h.01M6 12h.01M6 12h.01M14 20h.01M14 20h.01M22 12h.01M22 12h.01M18 3.05557h.01M10 12h.01M14 16h.01M14 16h.01M18 12h.01M18 12h.01M10 4h.01M10 4h.01M2 12h.01M2 12h.01M6.06253 21h.01"
>
</path>
</motion.g>
<motion.circle
:initial="{ opacity: 0, scale: 0.2 }"
:animate="{ opacity: 1, scale: 1 }"
:transition="{ delay: 0, duration: 0.8 }"
cx="14"
cy="12"
r="0.5"
v-bind="preventFocusProps"
/>
<motion.circle
:initial="{ opacity: 0, scale: 0.2 }"
:animate="{ opacity: 1, scale: 1 }"
:transition="{ delay: 0, duration: 0.8 }"
cx="14"
cy="12"
v-for="[cx, cy] in MARKERS"
:key="`${cx},${cy}`"
:cx="cx"
:cy="cy"
r="0.5"
v-bind="preventFocusProps"
/>
<motion.circle
:initial="{ opacity: 0, scale: 0.2 }"
:initial="initial({ opacity: 0, scale: 0.2 })"
:animate="{ opacity: 1, scale: 1 }"
:transition="{ delay: 0, duration: 0.8 }"
cx="18"
cy="3.05557"
r="0.5"
v-bind="preventFocusProps"
/>
<motion.circle
:initial="{ opacity: 0, scale: 0.2 }"
:animate="{ opacity: 1, scale: 1 }"
:transition="{ delay: 0, duration: 0.8 }"
cx="10"
cy="12"
r="0.5"
v-bind="preventFocusProps"
/>
<motion.circle
:initial="{ opacity: 0, scale: 0.2 }"
:animate="{ opacity: 1, scale: 1 }"
:transition="{ delay: 0, duration: 0.8 }"
cx="6.06253"
cy="21"
r="0.5"
:transition="{ duration: 0.8 }"
v-bind="preventFocusProps"
/>
</motion.g>
@@ -335,221 +398,30 @@ const shrinkIconVariants: Variants = {
stroke-width="0.12"
stroke="#FFF"
stroke-opacity="0.3"
:initial="{ opacity: 1 }"
:animate="showHandles ? { opacity: 1 } : { opacity: 0 }"
:transition="{ delay: 0, duration: 0.6 }"
:initial="initial({ opacity: 1 })"
:animate="{ opacity: showHandles ? 1 : 0 }"
:transition="{ duration: 0.6 }"
v-bind="preventFocusProps"
>
<motion.g
:initial="{ opacity: 0, scale: 0.2 }"
v-for="(group, index) in HANDLE_GROUPS"
:key="index"
:initial="initial({ opacity: 0, scale: 0.2 })"
:animate="{ opacity: 1, scale: 1 }"
:transition="{ delay: 0.2, duration: 0.3 }"
:transition="{ delay: group.delay, duration: 0.3 }"
v-bind="preventFocusProps"
>
<path d="M14 12 14 9.79086"></path>
<circle
cy="9.79086"
cx="14"
r="0.25"
></circle>
</motion.g>
<motion.g
:initial="{ opacity: 0, scale: 0.2 }"
:animate="{ opacity: 1, scale: 1 }"
:transition="{ delay: 0.4, duration: 0.3 }"
v-bind="preventFocusProps"
>
<path d="M10 8 12.2091 8"></path>
<circle
cy="8"
cx="12.2091"
r="0.25"
></circle>
<path d="M10 8 7.79086 8"></path>
<circle
cy="8"
cx="7.79086"
r="0.25"
></circle>
</motion.g>
<motion.g
:initial="{ opacity: 0, scale: 0.2 }"
:animate="{ opacity: 1, scale: 1 }"
:transition="{ delay: 0.6, duration: 0.3 }"
v-bind="preventFocusProps"
>
<path d="M6 12 6 9.79086"></path>
<circle
cy="9.79086"
cx="6"
r="0.25"
></circle>
<path d="M6 12 6 16.4183"></path>
<circle
cy="16.4183"
cx="6"
r="0.25"
></circle>
</motion.g>
<motion.g
:initial="{ opacity: 0, scale: 0.2 }"
:animate="{ opacity: 1, scale: 1 }"
:transition="{ delay: 0.8, duration: 0.3 }"
v-bind="preventFocusProps"
>
<path d="M14 20 9.58172 20"></path>
<circle
cy="20"
cx="9.58172"
r="0.25"
></circle>
<path d="M14 20 18.4183 20"></path>
<circle
cy="20"
cx="18.4183"
r="0.25"
></circle>
</motion.g>
<motion.g
:initial="{ opacity: 0, scale: 0.2 }"
:animate="{ opacity: 1, scale: 1 }"
:transition="{ delay: 1, duration: 0.3 }"
v-bind="preventFocusProps"
>
<path d="M22 12 22 16.4183"></path>
<circle
cy="16.4183"
cx="22"
r="0.25"
></circle>
<path d="M22 12 22 8.446"></path>
<circle
cy="8.446"
cx="22"
r="0.25"
></circle>
</motion.g>
<motion.g
:initial="{ opacity: 0, scale: 0.2 }"
:animate="{ opacity: 1, scale: 1 }"
:transition="{ delay: 1.2, duration: 0.3 }"
v-bind="preventFocusProps"
>
<path d="M18 3.05557 20.455 5.25285"></path>
<circle
cy="5.25285"
cx="20.455"
r="0.25"
></circle>
</motion.g>
<motion.g
:initial="{ opacity: 0, scale: 0.2 }"
:animate="{ opacity: 1, scale: 1 }"
:transition="{ delay: 0.2, duration: 0.3 }"
v-bind="preventFocusProps"
>
<path d="M10 12 10 14.2091"></path>
<circle
cy="14.2091"
cx="10"
r="0.25"
></circle>
</motion.g>
<motion.g
:initial="{ opacity: 0, scale: 0.2 }"
:animate="{ opacity: 1, scale: 1 }"
:transition="{ delay: 0.4, duration: 0.3 }"
v-bind="preventFocusProps"
>
<path d="M14 16 11.7909 16"></path>
<circle
cy="16"
cx="11.7909"
r="0.25"
></circle>
<path d="M14 16 16.2091 16"></path>
<circle
cy="16"
cx="16.2091"
r="0.25"
></circle>
</motion.g>
<motion.g
:initial="{ opacity: 0, scale: 0.2 }"
:animate="{ opacity: 1, scale: 1 }"
:transition="{ delay: 0.6, duration: 0.3 }"
v-bind="preventFocusProps"
>
<path d="M18 12 18 14.2091"></path>
<circle
cy="14.2091"
cx="18"
r="0.25"
></circle>
<path d="M18 12 18 7.58172"></path>
<circle
cy="7.58172"
cx="18"
r="0.25"
></circle>
</motion.g>
<motion.g
:initial="{ opacity: 0, scale: 0.2 }"
:animate="{ opacity: 1, scale: 1 }"
:transition="{ delay: 0.8, duration: 0.3 }"
v-bind="preventFocusProps"
>
<path d="M10 4 14.4183 4"></path>
<circle
cy="4"
cx="14.4183"
r="0.25"
></circle>
<path d="M10 4 5.58172 4"></path>
<circle
cy="4"
cx="5.58172"
r="0.25"
></circle>
</motion.g>
<motion.g
:initial="{ opacity: 0, scale: 0.2 }"
:animate="{ opacity: 1, scale: 1 }"
:transition="{ delay: 1, duration: 0.3 }"
v-bind="preventFocusProps"
>
<path d="M2 12 2 7.58172"></path>
<circle
cy="7.58172"
cx="2"
r="0.25"
></circle>
<path d="M2 12 2 15.5841"></path>
<circle
cy="15.5841"
cx="2"
r="0.25"
></circle>
</motion.g>
<motion.g
:initial="{ opacity: 0, scale: 0.2 }"
:animate="{ opacity: 1, scale: 1 }"
:transition="{ delay: 1.2, duration: 0.3 }"
v-bind="preventFocusProps"
>
<path d="M6.06253 21 3.57127 18.8012"></path>
<circle
cy="18.8012"
cx="3.57127"
r="0.25"
></circle>
<template
v-for="[x1, y1, x2, y2] in group.segments"
:key="`${x2},${y2}`"
>
<path :d="`M${x1} ${y1} ${x2} ${y2}`" />
<circle
:cx="x2"
:cy="y2"
r="0.25"
/>
</template>
</motion.g>
</motion.g>
</motion.svg>
@@ -6,7 +6,7 @@ import Calendar from '../../../data/iconDetails/calendar.ts';
import Clock from '../../../data/iconDetails/clock.ts';
import Bug from '../../../data/iconDetails/bug.ts';
import Rocket from '../../../data/iconDetails/rocket.ts';
import TriangleAlert from '../../../data/iconDetails/triangle-alert.ts';
import TriangleExclamationPoint from '../../../data/iconDetails/triangle-exclamation-point.ts';
import PartyPopper from '../../../data/iconDetails/party-popper.ts';
import Scissors from '../../../data/iconDetails/scissors.ts';
import Copy from '../../../data/iconDetails/copy.ts';
@@ -41,7 +41,7 @@ const CalendarIcon = createLucideIcon('calendar', Calendar.iconNode);
const ClockIcon = createLucideIcon('clock', Clock.iconNode);
const BugIcon = createLucideIcon('bug', Bug.iconNode);
const RocketIcon = createLucideIcon('rocket', Rocket.iconNode);
const AlertTriangleIcon = createLucideIcon('alert-triangle', TriangleAlert.iconNode);
const TriangleExclamationPointIcon = createLucideIcon('triangle-exclamation-point', TriangleExclamationPoint.iconNode);
const ScissorsIcon = createLucideIcon('scissors', Scissors.iconNode);
const CopyIcon = createLucideIcon('copy', Copy.iconNode);
const SaveIcon = createLucideIcon('save', Save.iconNode);
@@ -128,7 +128,7 @@ const prettyName = props.name
>
<div class="placeholder"></div>
<div class="badge badge-red">
<AlertTriangleIcon :size="20" />
<TriangleExclamationPointIcon :size="20" />
Alert
</div>
</div>
@@ -117,8 +117,6 @@ function scrollTrack(direction: 1 | -1) {
.tiers {
padding-top: 96px;
padding-bottom: 48px;
/* The track bleeds to the right edge of the viewport. */
overflow-x: clip;
}
.tiers-header {
@@ -165,7 +163,6 @@ function scrollTrack(direction: 1 | -1) {
.track-wrapper {
position: relative;
margin-right: min(-24px, calc((1152px - 100vw) / 2));
}
.track {
@@ -178,12 +175,16 @@ function scrollTrack(direction: 1 | -1) {
padding-bottom: 12px;
padding-right: 24px;
scroll-padding-right: 24px;
scrollbar-width: thin;
scrollbar-width: none;
scroll-snap-align: center;
scroll-padding-left: 24px;
scroll-padding-right: 24px;
}
.track::-webkit-scrollbar {
display: none;
}
.track:focus-visible {
outline: 2px solid var(--vp-c-brand-1);
outline-offset: 4px;
@@ -3,7 +3,7 @@ description: Learn the best practices for writing use cases for Lucide icons.
---
<script setup>
import { batteryLow, creditCard, crop, handshake, heading2, microchip, search, squareLetterP, triangleAlert, wifiOff } from '~/.vitepress/data/iconNodes';
import { batteryLow, creditCard, crop, handshake, heading2, microchip, search, squareLetterP, triangleExclamationPoint, wifiOff } from '~/.vitepress/data/iconNodes';
import LucideIcon from '~/.vitepress/theme/components/base/LucideIcon.vue';
</script>
@@ -120,10 +120,10 @@ The period adds **unnecessary punctuation** and makes entries inconsistent.
Use plain text only. Formatting belongs in documentation, not metadata values.
:::: example
::: do <LucideIcon name="triangle-alert" :iconNode="triangleAlert" /> Warning users about a destructive action
::: do <LucideIcon name="triangle-exclamation-point" :iconNode="triangleExclamationPoint" /> Warning users about a destructive action
This works in search, generated pages, and other places that use metadata.
:::
::: dont <LucideIcon name="triangle-alert" :iconNode="triangleAlert" /> <span>**Warning** users about a destructive action ⚠️</span>
::: dont <LucideIcon name="triangle-exclamation-point" :iconNode="triangleExclamationPoint" /> <span>**Warning** users about a destructive action ⚠️</span>
Markdown and emoji can **show up in generated UI** and make metadata **harder to reuse**.
:::
::::
+1 -1
View File
@@ -31,7 +31,7 @@ You can then reference the registered icons by name:
```html [app.html]
<svg lucideIcon="square-check"></svg>
<svg lucideIcon="circle-alert"></svg>
<svg lucideIcon="circle-exclamation-point"></svg>
```
## How names are resolved
+2 -2
View File
@@ -41,7 +41,7 @@
"h3": "^1.15.11",
"markdown-it": "^15.0.3",
"markdown-it-container": "^4.0.0",
"nitropack": "^2.13.4",
"nitropack": "^2.13.5",
"rollup-plugin-copy": "^3.5.0",
"semver": "^7.8.5",
"sharp": "^0.35.5",
@@ -71,7 +71,7 @@
"lodash-es": "^4.18.1",
"lucide": "workspace:*",
"lucide-react": "workspace:*",
"motion-v": "2.4.0",
"motion-v": "2.11.0",
"react": "19.3.1",
"react-dom": "19.3.1",
"sandpack-vue3": "3.1.12",
+62 -7
View File
@@ -12,8 +12,68 @@
"permanent": true
},
{
"source": "/docs/:slug(lucide-?.*)",
"destination": "/guide/:slug",
"source": "/docs/lucide-vue-next",
"destination": "/guide/vue",
"permanent": true
},
{
"source": "/guide/packages/lucide-vue-next",
"destination": "/guide/vue",
"permanent": true
},
{
"source": "/guide/vue-next",
"destination": "/guide/vue",
"permanent": true
},
{
"source": "/docs/lucide",
"destination": "/guide/lucide",
"permanent": true
},
{
"source": "/docs/lucide-:pkg",
"destination": "/guide/:pkg",
"permanent": true
},
{
"source": "/guide/lucide-:pkg",
"destination": "/guide/:pkg",
"permanent": true
},
{
"source": "/guide/packages/lucide-:pkg/",
"destination": "/guide/:pkg",
"permanent": true
},
{
"source": "/guide/advanced/accessibility",
"destination": "/how-to/accessibility",
"permanent": true
},
{
"source": "/guide/basics/:slug",
"destination": "/guide/react/basics/:slug",
"permanent": true
},
{
"source": "/guide/advanced/:slug",
"destination": "/guide/react/advanced/:slug",
"permanent": true
},
{
"source": "/guide/design/figma-guide",
"destination": "/contribute/icons/figma-guide",
"permanent": true
},
{
"source": "/guide/design/:slug(icon-design-guide|icon-properties)",
"destination": "/contribute/icons/design-principles",
"permanent": true
},
{
"source": "/icons/lab/:name(carton-off|briefcase-plus)",
"destination": "/icons/:name",
"permanent": true
},
{
@@ -47,11 +107,6 @@
"destination": "/how-to/accessibility",
"permanent": true
},
{
"source": "/guide/advanced/accessibility",
"destination": "/how-to/accessibility",
"permanent": true
},
{
"source": "/guide/advanced",
"destination": "/guide/react/advanced",
+3 -1
View File
@@ -20,6 +20,8 @@
"investigate",
"magnifying glass",
"lens",
"zoom"
"zoom",
"inspection",
"visual search"
]
}
@@ -15,5 +15,10 @@
"categories": [
"account",
"social"
],
"aliases": [
{
"name": "badge-alert"
}
]
}

Before

Width:  |  Height:  |  Size: 471 B

After

Width:  |  Height:  |  Size: 471 B

@@ -12,5 +12,10 @@
],
"categories": [
"$extends:battery"
],
"aliases": [
{
"name": "battery-warning"
}
]
}

Before

Width:  |  Height:  |  Size: 393 B

After

Width:  |  Height:  |  Size: 393 B

@@ -15,5 +15,10 @@
],
"categories": [
"$extends:book"
],
"aliases": [
{
"name": "book-alert"
}
]
}

Before

Width:  |  Height:  |  Size: 359 B

After

Width:  |  Height:  |  Size: 359 B

@@ -16,6 +16,9 @@
"name": "alert-circle",
"deprecationReason": "alias.name",
"deprecated": true
},
{
"name": "circle-alert"
}
]
}

Before

Width:  |  Height:  |  Size: 332 B

After

Width:  |  Height:  |  Size: 332 B

+23
View File
@@ -0,0 +1,23 @@
{
"$schema": "../icon.schema.json",
"contributors": [
"n2duc"
],
"use-cases": [
"Indicating a submission or payment that is still being verified",
"Showing a deployment, upload, or backup that is finishing up",
"Marking a task as partially completed or awaiting final approval",
"Displaying a step in a progress tracker that is in progress"
],
"tags": [
"$group:check",
"pending",
"progress",
"processing",
"verifying",
"loading"
],
"categories": [
"notifications"
]
}
+18
View File
@@ -0,0 +1,18 @@
<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 2a10 10 0 017.38 16.75" />
<path d="m16 9-5.5 5.5L8 12" />
<path d="M2.5 8.875a10 10 0 00-.5 3" />
<path d="M2.83 16a10 10 0 002.43 3.4" />
<path d="M4.636 5.235a10 10 0 01.891-.857" />
<path d="M8.644 21.42a10 10 0 007.631-.38" />
</svg>

After

Width:  |  Height:  |  Size: 465 B

@@ -13,5 +13,10 @@
],
"categories": [
"$extends:clock"
],
"aliases": [
{
"name": "clock-alert"
}
]
}

Before

Width:  |  Height:  |  Size: 332 B

After

Width:  |  Height:  |  Size: 332 B

@@ -19,5 +19,10 @@
"development",
"notifications",
"weather"
],
"aliases": [
{
"name": "cloud-alert"
}
]
}

Before

Width:  |  Height:  |  Size: 331 B

After

Width:  |  Height:  |  Size: 331 B

+27
View File
@@ -0,0 +1,27 @@
{
"$schema": "../icon.schema.json",
"contributors": [
"$extends:grid-3x3",
"gridalchemy"
],
"use-cases": [
"Hiding grid lines on a canvas or artboard",
"Indicating that snap to grid is turned off",
"Hiding the cell grid in spreadsheets"
],
"tags": [
"$extends:grid-3x3",
"$group:off",
"guides",
"snap",
"canvas",
"spreadsheet",
"cells",
"data table",
"diagram",
"overlay"
],
"categories": [
"$extends:grid-3x3"
]
}
+21
View File
@@ -0,0 +1,21 @@
<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="M15 9.344V3" />
<path d="m2 2 20 20" />
<path d="M20.414 20.414A2 2 0 0119 21H5a2 2 0 01-2-2V5a2 2 0 01.586-1.414" />
<path d="M20.656 15H21" />
<path d="M21 9h-6.344" />
<path d="M3 15h12v6" />
<path d="M3 9h6v12" />
<path d="M8.656 3H19a2 2 0 012 2v10.343" />
<path d="M9 3v.344" />
</svg>

After

Width:  |  Height:  |  Size: 520 B

@@ -13,5 +13,13 @@
"categories": [
"mail",
"notifications"
],
"aliases": [
{
"name": "mail-warning"
},
{
"name": "mail-alert"
}
]
}

Before

Width:  |  Height:  |  Size: 393 B

After

Width:  |  Height:  |  Size: 393 B

+30
View File
@@ -0,0 +1,30 @@
{
"$schema": "../icon.schema.json",
"contributors": [
"Prsenapati09",
"rrod497"
],
"use-cases": [
"Representing tropical fruit in food and grocery apps",
"Indicating produce sections in shopping interfaces",
"Labeling tropical fruit filters in recipe and nutrition apps",
"Highlighting seasonal fruit in marketplaces"
],
"tags": [
"fruit",
"food",
"tropical",
"summer",
"exotic",
"healthy",
"ripe",
"yellow",
"juice",
"smoothie",
"snack",
"produce"
],
"categories": [
"food-beverage"
]
}
+14
View File
@@ -0,0 +1,14 @@
<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="m17 6 3-3" />
<path d="M7 22a4 4 0 01-2.62-7.023A2 2 0 005 13.383V10a8 8 0 1116 0A12 12 0 019 22z" />
</svg>

After

Width:  |  Height:  |  Size: 323 B

@@ -17,5 +17,13 @@
"categories": [
"$extends:message-circle",
"notifications"
],
"aliases": [
{
"name": "message-circle-warning"
},
{
"name": "message-circle-alert"
}
]
}

Before

Width:  |  Height:  |  Size: 396 B

After

Width:  |  Height:  |  Size: 396 B

@@ -16,5 +16,13 @@
"categories": [
"$extends:message-square",
"notifications"
],
"aliases": [
{
"name": "message-square-warning"
},
{
"name": "message-square-alert"
}
]
}

Before

Width:  |  Height:  |  Size: 388 B

After

Width:  |  Height:  |  Size: 388 B

+38
View File
@@ -0,0 +1,38 @@
{
"$schema": "../icon.schema.json",
"contributors": [
"jamiemlaw",
"jguddas"
],
"use-cases": [
"Picking a transport type in journey planning or racing apps",
"Highlighting a delivery driver on a map when they ride a motorbike"
],
"tags": [
"scooter",
"two-wheeler",
"commute",
"urban",
"city",
"courier",
"motorbike",
"moto",
"motorcycle",
"transport",
"vehicle",
"drive",
"ride",
"race",
"racing",
"trip",
"journey",
"delivery",
"off-road",
"dirt bike",
"trail",
"enduro"
],
"categories": [
"transportation"
]
}
+17
View File
@@ -0,0 +1,17 @@
<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.16 14.12-2.927-8.447A1 1 0 0 0 14.288 5H13" />
<path d="M19 10a7 7 0 0 0-6.928 6" />
<path d="M2 10h5.5a3 3 0 0 1 2.747 1.795l1.669 3.803A1 1 0 0 1 11.01 17H8" />
<circle cx="19" cy="17" r="3" />
<circle cx="5" cy="17" r="3" />
</svg>

After

Width:  |  Height:  |  Size: 459 B

@@ -18,6 +18,9 @@
"name": "alert-octagon",
"deprecationReason": "alias.name",
"deprecated": true
},
{
"name": "octagon-alert"
}
]
}

Before

Width:  |  Height:  |  Size: 510 B

After

Width:  |  Height:  |  Size: 510 B

+32
View File
@@ -0,0 +1,32 @@
{
"$schema": "../icon.schema.json",
"contributors": [
"ilyavred",
"jamiemlaw"
],
"use-cases": [
"Setting the opacity of layers or objects in graphic editors",
"Adjusting the opacity of fills, strokes, or other object attributes",
"Adjusting the alpha channel in color pickers",
"Indicating that a layer or element is semi-transparent"
],
"tags": [
"opacity",
"opaque",
"transparency",
"visibility",
"fade",
"lens",
"glass",
"blur",
"filter",
"clear",
"mask",
"cover",
"gradient"
],
"categories": [
"design",
"photography"
]
}
+25
View File
@@ -0,0 +1,25 @@
<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 3h.01" />
<path d="M12 9h.01" />
<path d="M15 6h.01" />
<path d="M18 3h.01" />
<path d="M21 6v13a2 2 0 0 1-2 2H6" />
<path d="M3 12h.01" />
<path d="M3 18h.01" />
<path d="M3 6h.01" />
<path d="M6 15h.01" />
<path d="M6 3h.01" />
<path d="M6 9h.01" />
<path d="M9 12h.01" />
<path d="M9 6h.01" />
</svg>

After

Width:  |  Height:  |  Size: 544 B

+19
View File
@@ -0,0 +1,19 @@
{
"$schema": "../icon.schema.json",
"contributors": [
"$extends:package"
],
"use-cases": [
"Editing recipient addresses or package contents in shipping systems",
"Modifying customer orders before shipment",
"Updating product listings in inventory or e-commerce platforms",
"Editing software package manifests or metadata"
],
"tags": [
"$extends:package",
"$group:pen"
],
"categories": [
"$extends:package"
]
}
+17
View File
@@ -0,0 +1,17 @@
<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 14.34V12l8.71-5" />
<path d="M16.511 21.141a2 2 0 0 0 .854-.506l4.013-4.009a1 1 0 1 0-3.004-3.004l-4.01 4.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .599.626z" />
<path d="M21 9.114V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l5.087 2.907" />
<path d="M3.29 7 12 12" />
<path d="m7.5 4.27 9 5.15" />
</svg>

After

Width:  |  Height:  |  Size: 564 B

+24
View File
@@ -0,0 +1,24 @@
{
"$schema": "../icon.schema.json",
"contributors": [
"$extends:package"
],
"use-cases": [
"Marking an unknown or unrecognized package in a package registry",
"Indicating a dependency that is still being resolved in a package manager",
"Indicating a lost or missing delivery package",
"Showing a shipment with an unknown delivery status"
],
"tags": [
"$extends:package",
"$group:question-mark",
"unresolved",
"placeholder",
"pending",
"lost",
"dependency"
],
"categories": [
"$extends:package"
]
}
+18
View File
@@ -0,0 +1,18 @@
<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 22V12l8.71-5" />
<path d="M17 15.27c.2-.4.5-.8.9-1a2.1 2.1 0 0 1 2.6.4c.3.4.5.8.5 1.3 0 1.3-2 2-2 2" />
<path d="M19 21.99h.01" />
<path d="M21 10.342V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l2.33-1.332" />
<path d="M3.29 7 12 12" />
<path d="m7.5 4.27 9 5.15" />
</svg>

After

Width:  |  Height:  |  Size: 550 B

+8 -1
View File
@@ -21,7 +21,14 @@
"packed",
"archive",
"zip",
"module"
"module",
"shipping",
"recipient",
"inventory",
"logistics",
"tracking",
"order",
"product"
],
"categories": [
"files",
@@ -15,5 +15,10 @@
],
"categories": [
"$extends:search"
],
"aliases": [
{
"name": "search-alert"
}
]
}

Before

Width:  |  Height:  |  Size: 322 B

After

Width:  |  Height:  |  Size: 322 B

@@ -59,5 +59,10 @@
"development",
"notifications",
"gaming"
],
"aliases": [
{
"name": "shield-alert"
}
]
}

Before

Width:  |  Height:  |  Size: 435 B

After

Width:  |  Height:  |  Size: 435 B

+20
View File
@@ -0,0 +1,20 @@
{
"$schema": "../icon.schema.json",
"contributors": [
"$extends:square-dashed",
"domarku"
],
"use-cases": [
"Searching within a selected region",
"Finding content inside a placeholder area",
"Scanning a marquee selection",
"Zooming a selected region"
],
"tags": [
"$extends:square-dashed",
"$group:search"
],
"categories": [
"$extends:square-dashed"
]
}
+23
View File
@@ -0,0 +1,23 @@
<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="M14 3h1" />
<path d="M19 3a2 2 0 012 2" />
<path d="m21 21-1.875-1.875" />
<path d="M21 9v1" />
<path d="M3 14v1" />
<path d="M3 9v1" />
<path d="M5 21a2 2 0 01-2-2" />
<path d="M5 3a2 2 0 00-2 2" />
<path d="M9 21h1" />
<path d="M9 3h1" />
<circle cx="17" cy="17" r="3" />
</svg>

After

Width:  |  Height:  |  Size: 513 B

+4 -1
View File
@@ -15,7 +15,10 @@
"marquee",
"tool",
"dashed",
"box"
"box",
"region",
"area",
"region of interest"
],
"categories": [
"design",
@@ -21,6 +21,9 @@
"name": "alert-triangle",
"deprecationReason": "alias.name",
"deprecated": true
},
{
"name": "triangle-alert"
}
]
}

Before

Width:  |  Height:  |  Size: 345 B

After

Width:  |  Height:  |  Size: 345 B

+26
View File
@@ -0,0 +1,26 @@
{
"$schema": "../icon.schema.json",
"contributors": [
"$extends:user"
],
"use-cases": [
"Flagging an account as suspicious in moderation tools",
"Indicating a problem with a user's account or profile",
"Showing that a user has received a warning",
"Issuing a warning to a member in community management"
],
"tags": [
"$extends:user",
"$group:exclamation-mark",
"flagged",
"suspicious",
"reported",
"violation",
"moderation",
"account issue",
"review"
],
"categories": [
"$extends:user"
]
}
+16
View File
@@ -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="M15 15H9a4 4 0 00-4 4v2" />
<path d="M18.988 13v4" />
<path d="M18.988 21h.01" />
<circle cx="12" cy="7" r="4" />
</svg>

After

Width:  |  Height:  |  Size: 339 B

+16
View File
@@ -0,0 +1,16 @@
{
"$schema": "../icon.schema.json",
"contributors": [
"$extends:user-round"
],
"use-cases": [
"$extends:user-exclamation-point"
],
"tags": [
"$extends:user-round",
"$extends:user-exclamation-point"
],
"categories": [
"$extends:user-round"
]
}
+16
View File
@@ -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="M19.99 13v4" />
<path d="M19.99 21H20" />
<path d="M4 21a8 8 0 0111.99-6.934" />
<circle cx="12" cy="8" r="5" />
</svg>

After

Width:  |  Height:  |  Size: 338 B

@@ -15,7 +15,7 @@
"@solidjs/start": "2.0.5",
"lucide-solid": "workspace:*",
"nitro": "3.0.260903-beta",
"solid-js": "1.9.15"
"solid-js": "1.9.18"
},
"devDependencies": {
"@vitest/browser-playwright": "5.0.5",
+1 -1
View File
@@ -11,7 +11,7 @@
},
"dependencies": {
"lucide-solid": "workspace:*",
"solid-js": "1.9.15"
"solid-js": "1.9.18"
},
"devDependencies": {
"@vitest/browser-playwright": "5.0.5",
@@ -3,20 +3,20 @@ import { bootstrapApplication, provideClientHydration } from '@angular/platform-
import { provideServerRendering, renderApplication } from '@angular/platform-server';
import { LucideDynamicIcon } from './lucide-dynamic-icon';
import { provideLucideIcons } from './lucide-icons';
import { LucideBadgeAlert } from './icons/badge-alert';
import { LucideBadgeExclamationPoint } from './icons/badge-exclamation-point';
@Component({
// eslint-disable-next-line @angular-eslint/component-selector
selector: 'lucide-static-hydration-root',
template: `<svg lucideBadgeAlert [nonScalingStroke]="true"></svg>`,
imports: [LucideBadgeAlert],
template: `<svg lucideBadgeExclamationPoint [nonScalingStroke]="true"></svg>`,
imports: [LucideBadgeExclamationPoint],
})
class StaticHydrationRootComponent {}
@Component({
// eslint-disable-next-line @angular-eslint/component-selector
selector: 'lucide-dynamic-hydration-root',
template: `<svg [lucideIcon]="'badge-alert'"></svg>`,
template: `<svg [lucideIcon]="'badge-exclamation-point'"></svg>`,
imports: [LucideDynamicIcon],
})
class DynamicHydrationRootComponent {}
@@ -60,7 +60,7 @@ describe('Lucide hydration', () => {
);
(globalThis as { ngServerMode?: boolean }).ngServerMode = false;
const serverDocument = new DOMParser().parseFromString(html, 'text/html');
expectBadgeAlertShapeNodes(serverDocument.querySelector('svg') as SVGSVGElement);
expectBadgeExclamationPointShapeNodes(serverDocument.querySelector('svg') as SVGSVGElement);
document.head.innerHTML = serverDocument.head.innerHTML;
document.body.innerHTML = serverDocument.body.innerHTML;
@@ -75,7 +75,7 @@ describe('Lucide hydration', () => {
return document.querySelector('svg') as SVGSVGElement;
}
function expectBadgeAlertShapeNodes(svg: SVGSVGElement) {
function expectBadgeExclamationPointShapeNodes(svg: SVGSVGElement) {
expect(Array.from(svg.children).map((child) => child.tagName.toLowerCase())).toEqual([
'path',
'line',
@@ -89,7 +89,7 @@ describe('Lucide hydration', () => {
'lucide-static-hydration-root',
);
expectBadgeAlertShapeNodes(svg);
expectBadgeExclamationPointShapeNodes(svg);
for (const child of Array.from(svg.children)) {
expect(child.getAttribute('vector-effect')).toBe('non-scaling-stroke');
}
@@ -99,9 +99,9 @@ describe('Lucide hydration', () => {
const svg = await renderAndHydrate(
DynamicHydrationRootComponent,
'lucide-dynamic-hydration-root',
[provideLucideIcons(LucideBadgeAlert)],
[provideLucideIcons(LucideBadgeExclamationPoint)],
);
expectBadgeAlertShapeNodes(svg);
expectBadgeExclamationPointShapeNodes(svg);
});
});
+2 -1
View File
@@ -55,11 +55,12 @@
"dist"
],
"scripts": {
"build": "pnpm clean && pnpm copy:license && pnpm build:icons && pnpm build:bundles",
"build": "pnpm clean && pnpm copy:license && pnpm build:icons && pnpm build:bundles && pnpm check:icon-subpaths",
"copy:license": "cp ../../LICENSE ./LICENSE",
"clean": "rm -rf dist && rm -rf stats && rm -rf ./src/icons/*.ts",
"build:icons": "build-icons --output=./src --templateSrc=./scripts/exportTemplate.mts --renderUniqueKey --iconFileExtension=.ts --exportFileName=index.ts --withAliases --separateAliasesFile --separateAliasesFileIgnore=fingerprint --aliasesFileExtension=.ts",
"build:bundles": "rollup -c ./rollup.config.mjs",
"check:icon-subpaths": "node ./scripts/checkIconSubpaths.mts",
"test": "pnpm build:icons && vitest run",
"test:watch": "vitest watch",
"typecheck": "pnpm build:icons && tsc"
@@ -11,7 +11,14 @@ const dtsOptions = {
const packageName = 'LucideReact';
const outputFileName = 'lucide-react-native';
const outputDir = 'dist';
const inputs = ['src/lucide-react-native.ts', 'src/icons/index.ts'];
const iconAndAliasEntries = await getIconEntryNamesAndAliases();
// Alias modules (e.g. `icons/trash-2`) aren't imported by the index, so list them as entries
// to emit the runtime files the `./icons/*` export map points at.
const inputs = [
'src/lucide-react-native.ts',
'src/icons/index.ts',
...Object.values(iconAndAliasEntries),
];
const bundles = [
{
format: 'cjs',
@@ -28,7 +35,6 @@ const bundles = [
},
];
const iconAndAliasEntries = await getIconEntryNamesAndAliases();
const typesModuleMatcher = /[/\\]src[/\\]types\.(d\.)?ts$/;
const configs = bundles.map(({ inputs, outputDir, format, preserveModules, extension = 'js' }) => ({
@@ -0,0 +1,16 @@
// Fails the build when an `icons/*` subpath has type declarations but no runtime file.
import { existsSync, readdirSync } from 'node:fs';
const declarations = readdirSync('dist/types/icons').filter((file) => file.endsWith('.d.ts'));
const missing = declarations.flatMap((file) => {
const name = file.replace(/\.d\.ts$/, '');
return [`dist/esm/icons/${name}.mjs`, `dist/cjs/icons/${name}.js`].filter(
(runtimeFile) => !existsSync(runtimeFile),
);
});
if (missing.length > 0) {
console.error(`Missing runtime files for icon subpaths:\n${missing.join('\n')}`);
process.exit(1);
}
+1 -1
View File
@@ -82,7 +82,7 @@
"jest-serializer-html": "^7.1.0",
"rollup": "^4.65.1",
"rollup-plugin-dts": "^6.5.1",
"solid-js": "^1.9.15",
"solid-js": "^1.9.18",
"typescript": "^6.0.3",
"vite": "^8.3.5",
"vite-plugin-solid": "^2.11.15"
+161 -283
View File
File diff suppressed because it is too large Load Diff
+8 -6
View File
@@ -14,8 +14,14 @@ overrides:
webpack-dev-middleware: ^5.3.4
vitest: '5.0.5'
vite: '^8.3.3'
minimatch@>=9.0.0 <9.0.6: ^9.0.6
minimatch@>=9.0.0 <9.0.7: ^9.0.7
zod@<4.6.6: ^4.6.6
brace-expansion@<1.1.21: ^1.1.21
decode-uri-component@<=0.5.0: ^0.5.1
deepmerge@>=4.2.0 <=4.3.1: ^4.3.1
file-type@>=13.0.0 <21.3.1: ^21.3.1
'@jimp/core>file-type': ^16.5.4
glob@>=9.0.0 <=14.0.1: ^14.0.2
hono@>=4.12.4 <4.13.11: ^4.13.11
allowBuilds:
'@parcel/watcher': false
@@ -29,7 +35,3 @@ allowBuilds:
ttf2woff2: false
unrs-resolver: false
vue-demi: false
minimumReleaseAgeExclude:
- decode-uri-component@0.4.3
- deepmerge@4.3.2
+1 -1
View File
@@ -1,7 +1,7 @@
import path from 'path';
import fs from 'fs';
import { promisify } from 'util';
import simpleGit from 'simple-git';
import { simpleGit } from 'simple-git';
import { type IconMetadata } from '../../tools/build-icons/types.ts';
/**
+3
View File
@@ -5,6 +5,9 @@
"description": "A internal used package with helpers.",
"main": "helpers.ts",
"types": "helpers.ts",
"scripts": {
"test": "vitest run"
},
"author": "",
"license": "ISC",
"type": "module",
@@ -0,0 +1,42 @@
import { describe, expect, it } from 'vitest';
import { toCamelCase } from './toCamelCase.ts';
describe('toCamelCase', () => {
it('converts kebab-case icon names', () => {
expect(toCamelCase('arrow-up')).toBe('arrowUp');
expect(toCamelCase('circle-arrow-out-down-left')).toBe('circleArrowOutDownLeft');
expect(toCamelCase('arrow-up-0-1')).toBe('arrowUp01');
});
it('treats whitespace, hyphens and underscores as separators', () => {
expect(toCamelCase('foo_bar')).toBe('fooBar');
expect(toCamelCase('foo bar')).toBe('fooBar');
expect(toCamelCase('foo\tbar')).toBe('fooBar');
expect(toCamelCase('foo - _\tbar')).toBe('fooBar');
expect(toCamelCase('-foo')).toBe('Foo');
});
it('lowercases only a leading capital letter', () => {
expect(toCamelCase('Foo-bar')).toBe('fooBar');
expect(toCamelCase('FooBar')).toBe('fooBar');
expect(toCamelCase('alreadyCamelCase')).toBe('alreadyCamelCase');
});
it('keeps separators that are not followed by a word character', () => {
expect(toCamelCase('foo-')).toBe('foo-');
expect(toCamelCase('foo__')).toBe('foo__');
expect(toCamelCase('foo\t!')).toBe('foo\t!');
expect(toCamelCase('')).toBe('');
});
it('handles long runs of tabs in linear time', () => {
const input = `a${'\t'.repeat(100_000)}!`;
const start = performance.now();
const result = toCamelCase(input);
const duration = performance.now() - start;
expect(result).toBe(input);
expect(duration).toBeLessThan(100);
});
});
+2 -2
View File
@@ -5,6 +5,6 @@
* @returns {string} A camelized string
*/
export const toCamelCase = (string: string): string =>
string.replace(/^([A-Z])|[\s-_]+(\w)/g, (match, p1, p2) =>
p2 ? p2.toUpperCase() : p1.toLowerCase(),
string.replace(/^([A-Z])|[\s-_]+(\w)?/g, (match, p1, p2) =>
p1 ? p1.toLowerCase() : p2 ? p2.toUpperCase() : match,
);