mirror of
https://github.com/lucide-icons/lucide.git
synced 2026-11-15 14:08:49 +00:00
Merge branch 'main' of https://github.com/lucide-icons/lucide into algolia-search
This commit is contained in:
@@ -41,7 +41,6 @@ export default eventHandler((event) => {
|
||||
<Diff
|
||||
oldSrc={oldSrc}
|
||||
newSrc={newSrc}
|
||||
showGrid
|
||||
height={height}
|
||||
width={width}
|
||||
>
|
||||
|
||||
@@ -48,7 +48,6 @@ export default eventHandler((event) => {
|
||||
<Diff
|
||||
oldSrc={oldSrc}
|
||||
newSrc={newSrc}
|
||||
showGrid
|
||||
height={height}
|
||||
width={width}
|
||||
>
|
||||
|
||||
@@ -18,11 +18,12 @@ export default eventHandler((event) => {
|
||||
return sendError(event, error);
|
||||
}
|
||||
|
||||
const width = getQuery(event).width || undefined;
|
||||
const height = getQuery(event).height || undefined;
|
||||
const color = getQuery(event).color || undefined;
|
||||
const strokeWidth = getQuery(event).strokeWidth || undefined;
|
||||
const background = getQuery(event).background || undefined;
|
||||
const query = getQuery<Record<string, string | undefined>>(event);
|
||||
const width = query.width || undefined;
|
||||
const height = query.height || undefined;
|
||||
const color = query.color || undefined;
|
||||
const strokeWidth = query.strokeWidth || undefined;
|
||||
const background = query.background || undefined;
|
||||
|
||||
const LucideIcon = createLucideIcon(params.iconName, iconNode);
|
||||
|
||||
|
||||
@@ -15,6 +15,10 @@ const defaultSandpackCSS = await readFile(
|
||||
fileURLToPath(new URL('./theme/sandpack-default.css', import.meta.url)),
|
||||
'utf-8',
|
||||
);
|
||||
const iconStyleHeadScript = await readFile(
|
||||
fileURLToPath(new URL('./theme/iconStyleHead.js', import.meta.url)),
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const title = 'Lucide';
|
||||
const socialTitle = 'Lucide Icons';
|
||||
@@ -90,7 +94,10 @@ export default defineConfig({
|
||||
}) as unknown as UserConfig['vite']['plugins'][0],
|
||||
],
|
||||
},
|
||||
head: getHeadConfig({ title, description, socialTitle }),
|
||||
head: [
|
||||
...getHeadConfig({ title, description, socialTitle }),
|
||||
['script', {}, iconStyleHeadScript],
|
||||
],
|
||||
transformPageData,
|
||||
themeConfig: {
|
||||
logo: {
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
},
|
||||
{
|
||||
"name": "notifications",
|
||||
"title": "Notification"
|
||||
"title": "Notifications"
|
||||
},
|
||||
{
|
||||
"name": "people",
|
||||
|
||||
@@ -85,8 +85,26 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"lucide-react-native": {
|
||||
"@lucide/solid": {
|
||||
"order": 5,
|
||||
"icon": "solid",
|
||||
"docsAlias": "solid",
|
||||
"packageDirname": "solid",
|
||||
"shields": [
|
||||
{
|
||||
"alt": "npm",
|
||||
"src": "https://img.shields.io/npm/v/@lucide/solid",
|
||||
"href": "https://www.npmjs.com/package/@lucide/solid"
|
||||
},
|
||||
{
|
||||
"alt": "npm",
|
||||
"src": "https://img.shields.io/npm/dw/@lucide/solid",
|
||||
"href": "https://www.npmjs.com/package/@lucide/solid"
|
||||
}
|
||||
]
|
||||
},
|
||||
"lucide-react-native": {
|
||||
"order": 6,
|
||||
"icon": "react-native",
|
||||
"docsAlias": "react-native",
|
||||
"shields": [
|
||||
@@ -103,7 +121,7 @@
|
||||
]
|
||||
},
|
||||
"@lucide/angular": {
|
||||
"order": 6,
|
||||
"order": 7,
|
||||
"icon": "angular",
|
||||
"docsAlias": "angular",
|
||||
"packageDirname": "angular",
|
||||
|
||||
@@ -228,5 +228,24 @@
|
||||
],
|
||||
"source": "https://github.com/GyeongHoKim/lucide-lit",
|
||||
"documentation": "https://github.com/GyeongHoKim/lucide-lit/blob/main/README.md"
|
||||
},
|
||||
{
|
||||
"name": "lucide-jte",
|
||||
"description": "Implementation of Lucide icons for jte (Java Template Engine).",
|
||||
"icon": "/framework-logos/java.svg",
|
||||
"shields": [
|
||||
{
|
||||
"alt": "Latest Stable Version",
|
||||
"src": "https://img.shields.io/github/v/release/sgruendel/lucide-jte",
|
||||
"href": "https://github.com/sgruendel/lucide-jte/releases"
|
||||
},
|
||||
{
|
||||
"alt": "Maven Central",
|
||||
"src": "https://img.shields.io/maven-central/v/io.github.sgruendel/lucide-jte.svg",
|
||||
"href": "https://central.sonatype.com/artifact/io.github.sgruendel/lucide-jte"
|
||||
}
|
||||
],
|
||||
"source": "https://github.com/sgruendel/lucide-jte",
|
||||
"documentation": "https://github.com/sgruendel/lucide-jte/blob/main/README.md"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { type JSX } from 'react';
|
||||
|
||||
interface BackdropProps {
|
||||
src: string;
|
||||
|
||||
@@ -397,10 +397,14 @@ interface SvgPreviewProps extends React.SVGProps<SVGSVGElement> {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const SvgPreview = (
|
||||
{ src, children, height = 24, width = 24, showGrid = false, ...props }: SvgPreviewProps,
|
||||
ref,
|
||||
) => {
|
||||
const SvgPreview = ({
|
||||
src,
|
||||
children,
|
||||
height = 24,
|
||||
width = 24,
|
||||
showGrid = false,
|
||||
...props
|
||||
}: SvgPreviewProps) => {
|
||||
const subGridSize =
|
||||
Math.max(height, width) % 3 === 0
|
||||
? Math.max(height, width) > 24
|
||||
@@ -413,7 +417,6 @@ const SvgPreview = (
|
||||
|
||||
return (
|
||||
<svg
|
||||
ref={ref}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={width}
|
||||
height={height}
|
||||
|
||||
@@ -16,6 +16,7 @@ export function getAllCategoryFiles(): Omit<Category, 'iconCount'>[] {
|
||||
return {
|
||||
name,
|
||||
title: parsedFileContent.title,
|
||||
description: parsedFileContent.description,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ function hidePopup() {
|
||||
:iconNode="x"
|
||||
name="x"
|
||||
:size="20"
|
||||
absoluteStrokeWidth
|
||||
nonScalingStroke
|
||||
/>
|
||||
</Button>
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import { useData } from 'vitepress';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -7,8 +6,6 @@ const props = defineProps<{
|
||||
id: string;
|
||||
}>();
|
||||
|
||||
const { isDark } = useData();
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
function limitHexInput(val: string) {
|
||||
@@ -22,7 +19,7 @@ function limitHexInput(val: string) {
|
||||
const value = computed({
|
||||
get: () => {
|
||||
if (props.modelValue == null || props.modelValue === 'currentColor') {
|
||||
return isDark.value ? '#ffffff' : '#000000';
|
||||
return null;
|
||||
}
|
||||
|
||||
return props.modelValue;
|
||||
|
||||
@@ -27,7 +27,7 @@ defineProps({
|
||||
|
||||
<style scoped>
|
||||
.fake-input {
|
||||
background: var(--vp-c-bg-soft);
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-radius: 8px;
|
||||
color: var(--vp-c-text-2);
|
||||
padding: 11px 16px;
|
||||
|
||||
@@ -5,7 +5,7 @@ import Button from './Button.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Button class="reset-button">
|
||||
<Button class="reset-button" aria-label="Reset style">
|
||||
<Icon
|
||||
:size="20"
|
||||
:iconNode="rotateCw"
|
||||
@@ -16,6 +16,7 @@ import Button from './Button.vue';
|
||||
|
||||
<style scoped>
|
||||
.reset-button {
|
||||
transition: ease-in-out 0.1s opacity;
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
@@ -29,6 +30,12 @@ import Button from './Button.vue';
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.reset-button:disabled {
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* a rotate css animation keyframes */
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
|
||||
@@ -1,22 +1,33 @@
|
||||
<script setup>
|
||||
import { Switch } from '@headlessui/vue'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const isHydrated = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
isHydrated.value = true
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Switch
|
||||
:model-value="modelValue"
|
||||
:model-value="isHydrated ? modelValue : false"
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
class="switch"
|
||||
:class="{ enabled: modelValue }"
|
||||
:class="{ enabled: isHydrated && modelValue }"
|
||||
:id="id"
|
||||
>
|
||||
<span class="thumb" />
|
||||
</Switch>
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { type FunctionalComponent } from 'vue';
|
||||
import { type LucideProps } from '@lucide/vue/src/types';
|
||||
import { Sandpack } from 'sandpack-vue3';
|
||||
import { Sandpack, type SandpackProps } from 'sandpack-vue3';
|
||||
|
||||
defineProps<FunctionalComponent<LucideProps>>();
|
||||
const props = defineProps<SandpackProps>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Sandpack />
|
||||
<Sandpack v-bind="props" />
|
||||
</template>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup lang="tsx">
|
||||
import VPSidebarGroup from 'vitepress/dist/client/theme-default/components/VPSidebarGroup.vue';
|
||||
import VPNavBarSearch from '../../overrides/VPNavBarSearch.vue';
|
||||
import sidebar, { guideSidebarTop } from '../../../sidebar';
|
||||
import sidebar from '../../../sidebar';
|
||||
import { useData, useRouter } from 'vitepress';
|
||||
import Select from '../base/Select.vue';
|
||||
import { computed } from 'vue';
|
||||
@@ -75,15 +74,7 @@ function onSelectFramework(item: Framework) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VPNavBarSearch v-if="page?.relativePath?.startsWith?.('guide')"/>
|
||||
<VPSidebarGroup
|
||||
:items="guideSidebarTop"
|
||||
v-if="page?.relativePath?.startsWith?.('guide')"
|
||||
/>
|
||||
<div
|
||||
class="framework-select"
|
||||
v-if="page?.relativePath?.startsWith?.('guide')"
|
||||
>
|
||||
<div class="framework-select">
|
||||
<label for="framework-select">Framework</label>
|
||||
<Select
|
||||
id="framework-select"
|
||||
@@ -94,10 +85,7 @@ function onSelectFramework(item: Framework) {
|
||||
</div>
|
||||
<VPSidebarGroup
|
||||
:key="selected.value"
|
||||
v-if="
|
||||
page?.relativePath?.startsWith?.('guide') &&
|
||||
!page?.relativePath?.startsWith?.(selected.value.substring(1))
|
||||
"
|
||||
v-if="!page?.relativePath?.startsWith?.(selected.value.substring(1))"
|
||||
:items="sidebar[selected.value]"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -56,7 +56,6 @@ const scaleDownVariants: Variants = {
|
||||
riseUp: {
|
||||
x: 0.5,
|
||||
y: -0.5,
|
||||
animationName: 'riseUp',
|
||||
scale: 1,
|
||||
transition: {
|
||||
delay: 0.5,
|
||||
@@ -68,7 +67,6 @@ const scaleDownVariants: Variants = {
|
||||
x: -10.5,
|
||||
y: -10.5,
|
||||
scale: 0.1,
|
||||
animationName: 'small',
|
||||
transition: {
|
||||
delay: 1,
|
||||
duration: 1,
|
||||
@@ -84,7 +82,6 @@ const drawAnimation = ref('visible');
|
||||
const draw: Variants = {
|
||||
hidden: { pathLength: 0, opacity: 0 },
|
||||
visible: {
|
||||
animationName: 'visible',
|
||||
pathLength: 1.02,
|
||||
opacity: 1,
|
||||
transition: {
|
||||
@@ -93,7 +90,6 @@ const draw: Variants = {
|
||||
},
|
||||
},
|
||||
exit: (path) => ({
|
||||
animationName: 'exit',
|
||||
stroke: path ? 'var(--vp-c-text-1)' : 'var(--vp-c-brand)',
|
||||
pathLength: 1.02,
|
||||
opacity: 1,
|
||||
@@ -103,25 +99,26 @@ const draw: Variants = {
|
||||
}),
|
||||
};
|
||||
|
||||
const onAnimationComplete = (item) => {
|
||||
if (item.animationName === 'visible') {
|
||||
// motion-v passes the variant label that finished, not the resolved variant
|
||||
const onAnimationComplete = (variant: unknown) => {
|
||||
if (variant === 'visible') {
|
||||
drawAnimation.value = 'exit';
|
||||
return;
|
||||
}
|
||||
if (item.animationName === 'exit') {
|
||||
if (variant === 'exit') {
|
||||
showHandles.value = false;
|
||||
scaleDownAnimation.value = 'small';
|
||||
}
|
||||
if (item.animationName === 'small') {
|
||||
if (variant === 'small') {
|
||||
iconGridAnimation.value = 'showIcons';
|
||||
}
|
||||
if (item.animationName === 'riseUp') {
|
||||
if (variant === 'riseUp') {
|
||||
scaleDownAnimation.value = 'small';
|
||||
}
|
||||
if (item.animationName === 'showIcons') {
|
||||
if (variant === 'showIcons') {
|
||||
shrinkIconAnimation.value = 'shrinkIcons';
|
||||
}
|
||||
if (item.animationName === 'shrinkIcons') {
|
||||
if (variant === 'shrinkIcons') {
|
||||
iconGridAnimation.value = 'initial';
|
||||
|
||||
setTimeout(() => {
|
||||
@@ -134,14 +131,12 @@ const randomIndex = ref(Math.floor(Math.random() * 64));
|
||||
|
||||
const iconAnimationVariants: Variants = {
|
||||
initial: {
|
||||
animationName: 'end',
|
||||
opacity: 0,
|
||||
x: 0,
|
||||
y: 0,
|
||||
transition: { duration: 1, delay: 1, ease: 'easeInOut' },
|
||||
},
|
||||
showIcons: (index) => ({
|
||||
animationName: 'showIcons',
|
||||
opacity: [0, 1, 1],
|
||||
x: [0.5, 0, 0],
|
||||
y: [-0.5, 0, 0],
|
||||
@@ -155,7 +150,6 @@ const shrinkIconAnimation = ref('initial');
|
||||
const shrinkIconVariants: Variants = {
|
||||
initial: { strokeWidth: 2 },
|
||||
shrinkIcons: (index) => ({
|
||||
animationName: 'shrinkIcons',
|
||||
opacity: 1,
|
||||
strokeWidth: 0,
|
||||
transition: { delay: 1.8, duration: 1.5, ease: 'easeInOut' },
|
||||
@@ -257,7 +251,7 @@ const shrinkIconVariants: Variants = {
|
||||
</motion.g>
|
||||
<motion.g
|
||||
class="svg-preview-control-path-marker-group"
|
||||
stroke="#fff"
|
||||
:style="{ stroke: 'var(--vp-c-text-1)' }"
|
||||
stroke-width="0.125"
|
||||
:initial="{ opacity: 1 }"
|
||||
:animate="showHandles ? { opacity: 1 } : { opacity: 0 }"
|
||||
|
||||
@@ -38,7 +38,7 @@ const pickIcons = [
|
||||
'map-pinned',
|
||||
'megaphone',
|
||||
'moon',
|
||||
'music',
|
||||
'sixteenth-notes',
|
||||
'phone',
|
||||
'power-off',
|
||||
'radio',
|
||||
|
||||
@@ -11,18 +11,6 @@ import RocketDuotoneIcon from './RocketDuotoneIcon.ts';
|
||||
<Badge :href="`https://github.com/lucide-icons/lucide/releases/tag/${data.version}`">
|
||||
v{{ data.version }}
|
||||
</Badge>
|
||||
<Badge
|
||||
class="badge-special confetti-button animate"
|
||||
href="/guide/version-1"
|
||||
>
|
||||
<Icon :iconNode="RocketDuotoneIcon" name="rocket-duotone" />
|
||||
Version 1
|
||||
<Icon
|
||||
:iconNode="moveRight"
|
||||
name="move-right"
|
||||
class=""
|
||||
/>
|
||||
</Badge>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -45,22 +33,6 @@ import RocketDuotoneIcon from './RocketDuotoneIcon.ts';
|
||||
gap: 8px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.badge-special {
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-brand-1);
|
||||
text-underline-offset: 2px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.badge-special::before,
|
||||
.badge-special::after {
|
||||
animation-delay: 0.5s !important;
|
||||
animation-duration: 5s !important;
|
||||
animation-iteration-count: infinite !important;
|
||||
animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1) !important;
|
||||
transform: translateX(32px);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -1,59 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { syncRef, useCssVar } from '@vueuse/core'
|
||||
import HomeContainer from './HomeContainer.vue'
|
||||
import RangeSlider from '../base/RangeSlider.vue'
|
||||
import InputField from '../base/InputField.vue'
|
||||
import ColorPicker from '../base/ColorPicker.vue'
|
||||
import ResetButton from '../base/ResetButton.vue'
|
||||
import HomeIconCustomizerIcons from './HomeIconCustomizerIcons.vue'
|
||||
import Switch from '../base/Switch.vue'
|
||||
import { computed } from 'vue';
|
||||
import { useIconStyle } from '../../composables/useIconStyle';
|
||||
import HomeContainer from './HomeContainer.vue';
|
||||
import RangeSlider from '../base/RangeSlider.vue';
|
||||
import InputField from '../base/InputField.vue';
|
||||
import ColorPicker from '../base/ColorPicker.vue';
|
||||
import ResetButton from '../base/ResetButton.vue';
|
||||
import HomeIconCustomizerIcons from './HomeIconCustomizerIcons.vue';
|
||||
import Switch from '../base/Switch.vue';
|
||||
|
||||
const { color, strokeWidth, size, nonScalingStroke, isCustomized, resetStyle } = useIconStyle();
|
||||
|
||||
const iconContainer = ref<HTMLElement | null>()
|
||||
const color = ref('currentColor')
|
||||
const strokeWidth = ref(2)
|
||||
const size = ref(24)
|
||||
const absoluteStrokeWidth = ref(false)
|
||||
|
||||
const colorCssVar = useCssVar(
|
||||
'--customize-color',
|
||||
iconContainer,
|
||||
{
|
||||
initialValue: 'default'
|
||||
}
|
||||
)
|
||||
|
||||
const strokeWidthCssVar = useCssVar(
|
||||
'--customize-strokeWidth',
|
||||
iconContainer,
|
||||
{
|
||||
initialValue: '2'
|
||||
}
|
||||
)
|
||||
|
||||
const sizeCssVar = useCssVar(
|
||||
'--customize-size',
|
||||
iconContainer,
|
||||
{
|
||||
initialValue: '24'
|
||||
}
|
||||
)
|
||||
|
||||
syncRef(color, colorCssVar)
|
||||
syncRef(strokeWidth, strokeWidthCssVar, { transform: { ltr: String, rtl: Number } })
|
||||
syncRef(size, sizeCssVar, { transform: { ltr: String, rtl: Number } })
|
||||
|
||||
function resetStyle () {
|
||||
color.value = 'currentColor'
|
||||
strokeWidth.value = 2
|
||||
size.value = 24
|
||||
absoluteStrokeWidth.value = false
|
||||
}
|
||||
|
||||
watch(absoluteStrokeWidth, (enabled) => {
|
||||
iconContainer.value?.classList.toggle('absolute-stroke-width', enabled)
|
||||
})
|
||||
const iconStyle = computed(() => ({
|
||||
'--home-icon-color': color.value,
|
||||
'--home-icon-stroke-width': String(strokeWidth.value),
|
||||
'--home-icon-size': String(size.value),
|
||||
}));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -62,7 +24,10 @@ watch(absoluteStrokeWidth, (enabled) => {
|
||||
<div class="card-column">
|
||||
<h2 class="title">
|
||||
Style as you please
|
||||
<ResetButton @click="resetStyle"></ResetButton>
|
||||
<ResetButton
|
||||
:disabled="!isCustomized"
|
||||
@click="resetStyle"
|
||||
/>
|
||||
</h2>
|
||||
<p class="copy">
|
||||
Lucide has a lot of customization options to match the icons with your UI.
|
||||
@@ -78,7 +43,10 @@ watch(absoluteStrokeWidth, (enabled) => {
|
||||
class="color-picker-field"
|
||||
>
|
||||
<template #display>
|
||||
<ColorPicker v-model="color" id="icon-color" />
|
||||
<ColorPicker
|
||||
v-model="color"
|
||||
id="icon-color-picker"
|
||||
/>
|
||||
</template>
|
||||
</InputField>
|
||||
|
||||
@@ -90,7 +58,7 @@ watch(absoluteStrokeWidth, (enabled) => {
|
||||
<span class="customize-label">{{ strokeWidth }}px</span>
|
||||
</template>
|
||||
<RangeSlider
|
||||
id="stroke-width"
|
||||
id="stroke-width-slider"
|
||||
name="stroke-width"
|
||||
v-model="strokeWidth"
|
||||
:min="1"
|
||||
@@ -107,7 +75,7 @@ watch(absoluteStrokeWidth, (enabled) => {
|
||||
<span class="customize-label">{{ size }}px</span>
|
||||
</template>
|
||||
<RangeSlider
|
||||
id="size"
|
||||
id="size-slider"
|
||||
name="size"
|
||||
v-model="size"
|
||||
:min="16"
|
||||
@@ -117,22 +85,25 @@ watch(absoluteStrokeWidth, (enabled) => {
|
||||
</InputField>
|
||||
|
||||
<InputField
|
||||
id="absolute-stroke-width"
|
||||
label="Absolute Stroke width"
|
||||
id="non-scaling-stroke"
|
||||
label="Non-scaling stroke"
|
||||
>
|
||||
<template #display>
|
||||
<Switch
|
||||
id="absolute-stroke-width"
|
||||
name="absolute-stroke-width"
|
||||
v-model="absoluteStrokeWidth"
|
||||
id="non-scaling-stroke-switch"
|
||||
name="non-scaling-stroke"
|
||||
v-model="nonScalingStroke"
|
||||
/>
|
||||
</template>
|
||||
</InputField>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="icons-container card-column" ref="iconContainer">
|
||||
<HomeIconCustomizerIcons />
|
||||
<div class="icons-container card-column">
|
||||
<HomeIconCustomizerIcons
|
||||
:class="{ 'non-scaling-stroke': nonScalingStroke }"
|
||||
:style="iconStyle"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</HomeContainer>
|
||||
@@ -179,10 +150,6 @@ watch(absoluteStrokeWidth, (enabled) => {
|
||||
display: grid;
|
||||
grid-template-columns: 8fr 10fr;
|
||||
}
|
||||
/*
|
||||
.card-column {
|
||||
flex: 1;
|
||||
} */
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
@@ -191,7 +158,7 @@ watch(absoluteStrokeWidth, (enabled) => {
|
||||
}
|
||||
}
|
||||
|
||||
.color-picker-field:deep(.display-value) {
|
||||
width: 138px;
|
||||
.color-picker-field:deep(.icon-color-picker-input) {
|
||||
width: 116px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,29 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { data } from './HomeHeroIconsCard.data'
|
||||
import LucideIcon from '../base/LucideIcon.vue'
|
||||
import { vIntersectionObserver } from '@vueuse/components'
|
||||
import { data } from './HomeHeroIconsCard.data';
|
||||
import LucideIcon from '../base/LucideIcon.vue';
|
||||
import { vIntersectionObserver } from '@vueuse/components';
|
||||
|
||||
const getInitialItems = () => data.icons.slice(0, 64)
|
||||
const items = ref(getInitialItems())
|
||||
const showIcons = ref(false)
|
||||
const getInitialItems = () => data.icons.slice(0, 64);
|
||||
const items = ref(getInitialItems());
|
||||
const showIcons = ref(false);
|
||||
|
||||
// Added intersection observer to improve performance
|
||||
const onIntersectionObserver: IntersectionObserverCallback = ([{ isIntersecting }]) => {
|
||||
if (isIntersecting) {
|
||||
showIcons.value = true
|
||||
showIcons.value = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="icon-grid" v-intersection-observer="onIntersectionObserver">
|
||||
<div
|
||||
class="icon-grid"
|
||||
v-intersection-observer="onIntersectionObserver"
|
||||
>
|
||||
<template v-if="showIcons">
|
||||
<div
|
||||
v-for="icon in items"
|
||||
class="icon-grid-item"
|
||||
>
|
||||
<LucideIcon v-bind="icon" class="lucide-icon"/>
|
||||
>
|
||||
<LucideIcon
|
||||
v-bind="icon"
|
||||
class="lucide-icon"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -36,7 +42,7 @@ const onIntersectionObserver: IntersectionObserverCallback = ([{ isIntersecting
|
||||
grid-template-columns: repeat(auto-fill, minmax(68px, 1fr));
|
||||
grid-template-rows: repeat(auto-fill, minmax(68px, 1fr));
|
||||
width: 100%;
|
||||
height:100%;
|
||||
height: 100%;
|
||||
max-height: 360px;
|
||||
gap: 1px;
|
||||
overflow: hidden;
|
||||
@@ -65,13 +71,15 @@ const onIntersectionObserver: IntersectionObserverCallback = ([{ isIntersecting
|
||||
|
||||
.lucide-icon {
|
||||
will-change: width, height, stroke-width, stroke;
|
||||
color: var(--customize-color, currentColor);
|
||||
stroke-width: var(--customize-strokeWidth, 2);
|
||||
width: calc(var(--customize-size, 24) * 1px);
|
||||
height: calc(var(--customize-size, 24) * 1px);
|
||||
color: var(--home-icon-color, currentColor);
|
||||
stroke-width: var(--home-icon-stroke-width, 2);
|
||||
width: calc(var(--home-icon-size, 24) * 1px);
|
||||
height: calc(var(--home-icon-size, 24) * 1px);
|
||||
max-width: 3rem;
|
||||
max-height: 3rem;
|
||||
}
|
||||
|
||||
.icons-container.absolute-stroke-width .lucide-icon {
|
||||
stroke-width: calc(var(--customize-strokeWidth, 2) * 24 / var(--customize-size, 24));
|
||||
.icon-grid.non-scaling-stroke .lucide-icon {
|
||||
stroke-width: calc(var(--home-icon-stroke-width, 2) * 24 / var(--home-icon-size, 24));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -48,7 +48,7 @@ onMounted(() => {
|
||||
:iconNode="x"
|
||||
name="x"
|
||||
:size="20"
|
||||
absoluteStrokeWidth
|
||||
nonScalingStroke
|
||||
/>
|
||||
</Button>
|
||||
<VPDocAsideCarbonAds :carbon-ads="theme.carbonAds" overlay />
|
||||
|
||||
@@ -9,7 +9,7 @@ const props = defineProps<{
|
||||
name: string;
|
||||
popoverPosition?: 'top' | 'bottom';
|
||||
}>();
|
||||
const { size, color, strokeWidth, absoluteStrokeWidth } = useIconStyleContext();
|
||||
const { size, color, strokeWidth, nonScalingStroke } = useIconStyleContext();
|
||||
const { animate, confetti } = useConfetti();
|
||||
const componentName = computed(() => {
|
||||
return (toPascalCase(props.name) as string).replace(/\s/g, '');
|
||||
@@ -30,8 +30,8 @@ function copyJSX() {
|
||||
attrs.push(`strokeWidth={${strokeWidth.value}}`);
|
||||
}
|
||||
|
||||
if (absoluteStrokeWidth.value) {
|
||||
attrs.push(`absoluteStrokeWidth`);
|
||||
if (nonScalingStroke.value) {
|
||||
attrs.push(`nonScalingStroke`);
|
||||
}
|
||||
|
||||
const code = `<${componentName.value}${attrs.join(' ')} />`;
|
||||
@@ -60,8 +60,8 @@ function copyVue() {
|
||||
attrs.push(`:stroke-width="${strokeWidth.value}"`);
|
||||
}
|
||||
|
||||
if (absoluteStrokeWidth.value) {
|
||||
attrs.push(`absoluteStrokeWidth`);
|
||||
if (nonScalingStroke.value) {
|
||||
attrs.push(`nonScalingStroke`);
|
||||
}
|
||||
|
||||
const code = `<${componentName.value}${attrs.join(' ')} />`;
|
||||
@@ -86,8 +86,8 @@ function copyAngular() {
|
||||
attrs.push(`[strokeWidth]="${strokeWidth.value}"`);
|
||||
}
|
||||
|
||||
if (absoluteStrokeWidth.value) {
|
||||
attrs.push(`[absoluteStrokeWidth]="true"`);
|
||||
if (nonScalingStroke.value) {
|
||||
attrs.push(`[nonScalingStroke]="true"`);
|
||||
}
|
||||
|
||||
const code = `<lucide-icon${attrs.join(' ')}></lucide-icon>`;
|
||||
|
||||
@@ -204,7 +204,7 @@ const DiamondIcon = createLucideIcon('Diamond', diamond);
|
||||
max-height: 3rem;
|
||||
}
|
||||
|
||||
html.absolute-stroke-width .lucide-icon.customizable {
|
||||
html.non-scaling-stroke .lucide-icon.customizable {
|
||||
stroke-width: calc(var(--customize-strokeWidth, 2) * 24 / var(--customize-size, 24));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { IconEntity } from '../../types';
|
||||
import { computed, ref } from 'vue';
|
||||
import createLucideIcon from '@lucide/vue/src/createLucideIcon';
|
||||
import Icon from '@lucide/vue/src/Icon';
|
||||
import { useIconStyleContext } from '../../composables/useIconStyle';
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -12,25 +12,23 @@ const props = defineProps<{
|
||||
|
||||
const activeSize = ref('1000%');
|
||||
|
||||
const { size, color, strokeWidth, absoluteStrokeWidth } = useIconStyleContext();
|
||||
const { size, color, strokeWidth, nonScalingStroke } = useIconStyleContext();
|
||||
const previewIcon = ref();
|
||||
|
||||
const gridLines = computed(() => Array.from({ length: size.value - 1 }));
|
||||
|
||||
const iconComponent = computed(() => {
|
||||
if (!props.name || !props.iconNode) return null;
|
||||
return createLucideIcon(props.name, props.iconNode);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="icon-container">
|
||||
<component
|
||||
<Icon
|
||||
v-if="name && iconNode"
|
||||
:name="name"
|
||||
:iconNode="iconNode"
|
||||
ref="previewIcon"
|
||||
:is="iconComponent"
|
||||
:size="size"
|
||||
:color="color"
|
||||
:strokeWidth="absoluteStrokeWidth ? (Number(strokeWidth) * 24) / Number(size) : strokeWidth"
|
||||
:strokeWidth="strokeWidth"
|
||||
:nonScalingStroke="nonScalingStroke"
|
||||
class="preview-icon"
|
||||
:data-size="activeSize"
|
||||
/>
|
||||
|
||||
@@ -71,7 +71,7 @@ const Icon = createLucideIcon(props.name, props.iconNode);
|
||||
height: calc(var(--customize-size, 24) * 1px); */
|
||||
}
|
||||
|
||||
html.absolute-stroke-width .lucide-icon {
|
||||
html.non-scaling-stroke .lucide-icon {
|
||||
stroke-width: calc(var(--customize-strokeWidth, 2) * 24 / var(--customize-size, 24));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,73 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import { shallowRef, type Ref, watch, computed } from 'vue';
|
||||
import { useCssVar, syncRef } from '@vueuse/core';
|
||||
import { STYLE_DEFAULTS, useIconStyleContext } from '../../composables/useIconStyle';
|
||||
import { computed } from 'vue';
|
||||
import { useMounted } from '@vueuse/core';
|
||||
import { useIconStyleContext } from '../../composables/useIconStyle';
|
||||
import RangeSlider from '../base/RangeSlider.vue';
|
||||
import InputField from '../base/InputField.vue';
|
||||
import ColorPicker from '../base/ColorPicker.vue';
|
||||
import ResetButton from '../base/ResetButton.vue';
|
||||
import Switch from '../base/Switch.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
rootEl?: Ref<HTMLElement>;
|
||||
}>();
|
||||
const { color, strokeWidth, size, nonScalingStroke, isCustomized, resetStyle } =
|
||||
useIconStyleContext();
|
||||
|
||||
const { color, strokeWidth, size, absoluteStrokeWidth } = useIconStyleContext();
|
||||
const documentRef = shallowRef<HTMLElement | undefined>(
|
||||
typeof document !== 'undefined' ? document?.documentElement : undefined,
|
||||
);
|
||||
|
||||
const colorCssVar = useCssVar('--customize-color', props.rootEl?.value ?? documentRef.value, {
|
||||
initialValue: `${STYLE_DEFAULTS.color}`,
|
||||
});
|
||||
|
||||
const strokeWidthCssVar = useCssVar(
|
||||
'--customize-strokeWidth',
|
||||
props.rootEl?.value ?? documentRef.value,
|
||||
{
|
||||
initialValue: `${STYLE_DEFAULTS.strokeWidth}`,
|
||||
},
|
||||
);
|
||||
|
||||
const sizeCssVar = useCssVar('--customize-size', props.rootEl?.value ?? documentRef.value, {
|
||||
initialValue: `${STYLE_DEFAULTS.size}`,
|
||||
});
|
||||
|
||||
syncRef(color, colorCssVar, { direction: 'ltr' });
|
||||
syncRef(strokeWidth, strokeWidthCssVar, { direction: 'ltr', transform: { ltr: String } });
|
||||
syncRef(size, sizeCssVar, { direction: 'ltr', transform: { ltr: String } });
|
||||
|
||||
function resetStyle() {
|
||||
color.value = STYLE_DEFAULTS.color;
|
||||
strokeWidth.value = STYLE_DEFAULTS.strokeWidth;
|
||||
size.value = STYLE_DEFAULTS.size;
|
||||
absoluteStrokeWidth.value = STYLE_DEFAULTS.absoluteStrokeWidth;
|
||||
}
|
||||
|
||||
watch(absoluteStrokeWidth, (enabled) => {
|
||||
const htmlEl = document.documentElement;
|
||||
|
||||
htmlEl.classList.toggle('absolute-stroke-width', enabled);
|
||||
});
|
||||
|
||||
const customizingActive = computed(() => {
|
||||
return (
|
||||
color.value !== STYLE_DEFAULTS.color ||
|
||||
strokeWidth.value !== STYLE_DEFAULTS.strokeWidth ||
|
||||
size.value !== STYLE_DEFAULTS.size ||
|
||||
absoluteStrokeWidth.value !== STYLE_DEFAULTS.absoluteStrokeWidth
|
||||
);
|
||||
});
|
||||
// The server always renders the default style. Keep the first client render identical,
|
||||
// otherwise the hydrated DOM keeps the stale `disabled` attribute on the reset button.
|
||||
const isMounted = useMounted();
|
||||
const showCustomized = computed(() => isMounted.value && isCustomized.value);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="customizer-card"
|
||||
:class="{ customized: customizingActive }"
|
||||
:class="{ customized: showCustomized }"
|
||||
>
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Customizer</h2>
|
||||
<ResetButton @click="resetStyle"></ResetButton>
|
||||
<ResetButton
|
||||
:disabled="!showCustomized"
|
||||
@click="resetStyle"
|
||||
/>
|
||||
</div>
|
||||
<InputField
|
||||
id="icon-color"
|
||||
@@ -75,7 +35,7 @@ const customizingActive = computed(() => {
|
||||
>
|
||||
<ColorPicker
|
||||
v-model="color"
|
||||
id="icon-color"
|
||||
id="icon-color-picker"
|
||||
class="color-picker"
|
||||
/>
|
||||
</InputField>
|
||||
@@ -88,7 +48,7 @@ const customizingActive = computed(() => {
|
||||
<span class="customize-label">{{ strokeWidth }}px</span>
|
||||
</template>
|
||||
<RangeSlider
|
||||
id="stroke-width"
|
||||
id="stroke-width-slider"
|
||||
name="stroke-width"
|
||||
v-model="strokeWidth"
|
||||
:min="0.5"
|
||||
@@ -105,7 +65,7 @@ const customizingActive = computed(() => {
|
||||
<span class="customize-label">{{ size }}px</span>
|
||||
</template>
|
||||
<RangeSlider
|
||||
id="size"
|
||||
id="size-slider"
|
||||
name="size"
|
||||
v-model="size"
|
||||
:min="16"
|
||||
@@ -115,13 +75,13 @@ const customizingActive = computed(() => {
|
||||
</InputField>
|
||||
|
||||
<InputField
|
||||
id="absolute-stroke-width"
|
||||
label="Absolute stroke width"
|
||||
id="non-scaling-stroke"
|
||||
label="Non-scaling stroke"
|
||||
>
|
||||
<Switch
|
||||
id="absolute-stroke-width"
|
||||
name="absolute-stroke-width"
|
||||
v-model="absoluteStrokeWidth"
|
||||
id="non-scaling-stroke-switch"
|
||||
name="non-scaling-stroke"
|
||||
v-model="nonScalingStroke"
|
||||
/>
|
||||
</InputField>
|
||||
</div>
|
||||
@@ -140,7 +100,6 @@ const customizingActive = computed(() => {
|
||||
color: var(--vp-c-text-1);
|
||||
line-height: 32px;
|
||||
font-size: 16px;
|
||||
/* margin-bottom: 12px; */
|
||||
}
|
||||
|
||||
.customizer-card {
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
sun,
|
||||
moon,
|
||||
camera,
|
||||
music,
|
||||
sixteenthNotes,
|
||||
video,
|
||||
globe,
|
||||
layers,
|
||||
@@ -44,7 +44,7 @@ import createElement from 'lucide/src/createElement';
|
||||
import { IconNode } from 'lucide/src/types';
|
||||
|
||||
const icons: Component[] = [
|
||||
heart, star, zap, code, feather, cloud, sun, moon, camera, music,
|
||||
heart, star, zap, code, feather, cloud, sun, moon, camera, sixteenthNotes,
|
||||
video, globe, layers, packageIcon, compass, command, terminal, database,
|
||||
server, cpu, lock, key, shield, wifi, download, upload, search,
|
||||
settings, users, mail, bell, calendar, clock, gitBranch, funnel,
|
||||
|
||||
@@ -8,20 +8,26 @@ const loaded = ref(false)
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import '@docsearch/css'
|
||||
import '@docsearch/css/dist/style.css'
|
||||
import {
|
||||
computed,
|
||||
defineAsyncComponent,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
} from 'vue'
|
||||
import type { DefaultTheme } from 'vitepress/theme'
|
||||
|
||||
import { useData } from 'vitepress/dist/client/theme-default/composables/data'
|
||||
import { useData } from 'vitepress'
|
||||
import FakeInput from '../base/FakeInput.vue'
|
||||
|
||||
const VPAlgoliaSearchBox = defineAsyncComponent(() => import('vitepress/dist/client/theme-default/components/VPAlgoliaSearchBox.vue'));
|
||||
|
||||
const { theme, page } = useData()
|
||||
const { theme } = useData<DefaultTheme.Config>()
|
||||
|
||||
const algolia = computed(
|
||||
() =>
|
||||
(theme.value.search?.options ?? theme.value.algolia) as DefaultTheme.AlgoliaSearchOptions,
|
||||
)
|
||||
|
||||
// whether this instance is the one rendering the (hidden) search box
|
||||
const isOwner = ref(false)
|
||||
@@ -39,10 +45,7 @@ const preconnect = () => {
|
||||
const preconnect = document.createElement('link')
|
||||
preconnect.id = id
|
||||
preconnect.rel = 'preconnect'
|
||||
preconnect.href = `https://${
|
||||
((theme.value.search?.options as DefaultTheme.AlgoliaSearchOptions) ??
|
||||
theme.value.algolia)!.appId
|
||||
}-dsn.algolia.net`
|
||||
preconnect.href = `https://${algolia.value.appId}-dsn.algolia.net`
|
||||
preconnect.crossOrigin = ''
|
||||
document.head.appendChild(preconnect)
|
||||
})
|
||||
@@ -126,7 +129,7 @@ function isEditingContent(event: KeyboardEvent): boolean {
|
||||
|
||||
<!-- docsearch renders its own button here; we only need its modal -->
|
||||
<div v-if="isOwner" class="docsearch-host">
|
||||
<VPAlgoliaSearchBox :algolia="theme.search?.options ?? theme.algolia" />
|
||||
<VPAlgoliaSearchBox :algolia="algolia" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { test } from 'node:test';
|
||||
import { nextTick } from 'vue';
|
||||
|
||||
const storedValues = new Map<string, string>([
|
||||
['icon-size', '240'],
|
||||
['icon-stroke-width', '1.5'],
|
||||
['icon-color', '#ff0000'],
|
||||
['icon-non-scaling-stroke', 'true'],
|
||||
]);
|
||||
const styleProperties = new Map<string, string>();
|
||||
const classes = new Set<string>();
|
||||
|
||||
class StorageMock {
|
||||
getItem(key: string) {
|
||||
return storedValues.get(key) ?? null;
|
||||
}
|
||||
|
||||
setItem(key: string, value: string) {
|
||||
storedValues.set(key, value);
|
||||
}
|
||||
|
||||
removeItem(key: string) {
|
||||
storedValues.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
class StorageEventMock {
|
||||
type: string;
|
||||
options: unknown;
|
||||
|
||||
constructor(type: string, options: unknown) {
|
||||
this.type = type;
|
||||
this.options = options;
|
||||
}
|
||||
}
|
||||
|
||||
const localStorage = new StorageMock();
|
||||
const document = {
|
||||
documentElement: {
|
||||
style: { setProperty: (key: string, value: string) => styleProperties.set(key, value) },
|
||||
classList: {
|
||||
toggle: (value: string, enabled: boolean) =>
|
||||
enabled ? classes.add(value) : classes.delete(value),
|
||||
},
|
||||
},
|
||||
};
|
||||
const window = {
|
||||
document,
|
||||
localStorage,
|
||||
addEventListener() {},
|
||||
removeEventListener() {},
|
||||
dispatchEvent() {},
|
||||
};
|
||||
|
||||
Object.assign(globalThis, {
|
||||
window,
|
||||
document,
|
||||
localStorage,
|
||||
Storage: StorageMock,
|
||||
StorageEvent: StorageEventMock,
|
||||
CSS: { supports: () => true },
|
||||
});
|
||||
|
||||
const { useIconStyle, usePersistedIconStyle } = await import('./useIconStyle.ts');
|
||||
|
||||
test('creates a non-persisted icon style from the defaults', () => {
|
||||
const style = useIconStyle();
|
||||
|
||||
assert.equal(style.size.value, 24);
|
||||
assert.equal(style.strokeWidth.value, 2);
|
||||
assert.equal(style.color.value, 'currentColor');
|
||||
assert.equal(style.nonScalingStroke.value, false);
|
||||
|
||||
style.size.value = 48;
|
||||
|
||||
assert.equal(storedValues.get('icon-size'), '240');
|
||||
assert.equal(styleProperties.get('--customize-size'), '240');
|
||||
});
|
||||
|
||||
test('restores persisted settings and applies the shared CSS variables', () => {
|
||||
const style = usePersistedIconStyle();
|
||||
|
||||
assert.equal(style.size.value, 240);
|
||||
assert.equal(style.strokeWidth.value, 1.5);
|
||||
assert.equal(style.color.value, '#ff0000');
|
||||
assert.equal(style.nonScalingStroke.value, true);
|
||||
assert.equal(style.isCustomized.value, true);
|
||||
assert.equal(styleProperties.get('--customize-size'), '240');
|
||||
assert.equal(styleProperties.get('--customize-strokeWidth'), '1.5');
|
||||
assert.equal(styleProperties.get('--customize-color'), '#ff0000');
|
||||
assert.ok(classes.has('non-scaling-stroke'));
|
||||
});
|
||||
|
||||
test('resetting updates refs, CSS variables, and persisted values', async () => {
|
||||
const style = usePersistedIconStyle();
|
||||
style.resetStyle();
|
||||
await nextTick();
|
||||
|
||||
assert.equal(style.size.value, 24);
|
||||
assert.equal(style.strokeWidth.value, 2);
|
||||
assert.equal(style.color.value, 'currentColor');
|
||||
assert.equal(style.nonScalingStroke.value, false);
|
||||
assert.equal(style.isCustomized.value, false);
|
||||
assert.equal(styleProperties.get('--customize-size'), '24');
|
||||
assert.equal(styleProperties.get('--customize-strokeWidth'), '2');
|
||||
assert.equal(styleProperties.get('--customize-color'), 'currentColor');
|
||||
assert.equal(classes.has('non-scaling-stroke'), false);
|
||||
assert.equal(storedValues.get('icon-size'), '24');
|
||||
assert.equal(storedValues.get('icon-stroke-width'), '2');
|
||||
assert.equal(storedValues.get('icon-color'), 'currentColor');
|
||||
assert.equal(storedValues.get('icon-non-scaling-stroke'), 'false');
|
||||
});
|
||||
@@ -1,30 +1,100 @@
|
||||
import { ref, inject, Ref } from 'vue';
|
||||
import { computed, inject, ref, watch, type ComputedRef, type Ref } from 'vue';
|
||||
import { useLocalStorage } from '@vueuse/core';
|
||||
|
||||
export const ICON_STYLE_CONTEXT = Symbol('style');
|
||||
|
||||
interface IconSizeContext {
|
||||
interface IconStyleContext {
|
||||
size: Ref<number>;
|
||||
strokeWidth: Ref<number>;
|
||||
color: Ref<string>;
|
||||
absoluteStrokeWidth: Ref<boolean>;
|
||||
nonScalingStroke: Ref<boolean>;
|
||||
isCustomized: ComputedRef<boolean>;
|
||||
resetStyle: () => void;
|
||||
}
|
||||
|
||||
type IconStyle = Pick<IconStyleContext, 'size' | 'strokeWidth' | 'color' | 'nonScalingStroke'>;
|
||||
|
||||
export const STYLE_DEFAULTS = {
|
||||
size: 24,
|
||||
strokeWidth: 2,
|
||||
color: 'currentColor',
|
||||
absoluteStrokeWidth: false,
|
||||
nonScalingStroke: false,
|
||||
};
|
||||
|
||||
export const iconStyleContext = {
|
||||
size: ref(24),
|
||||
strokeWidth: ref(2),
|
||||
color: ref('currentColor'),
|
||||
absoluteStrokeWidth: ref(false),
|
||||
const persistedIconStyle = {
|
||||
size: useLocalStorage('icon-size', STYLE_DEFAULTS.size, { initOnMounted: false }),
|
||||
strokeWidth: useLocalStorage('icon-stroke-width', STYLE_DEFAULTS.strokeWidth, {
|
||||
initOnMounted: false,
|
||||
}),
|
||||
color: useLocalStorage('icon-color', STYLE_DEFAULTS.color, { initOnMounted: false }),
|
||||
nonScalingStroke: useLocalStorage('icon-non-scaling-stroke', STYLE_DEFAULTS.nonScalingStroke, {
|
||||
initOnMounted: false,
|
||||
}),
|
||||
};
|
||||
|
||||
export function useIconStyleContext(): IconSizeContext {
|
||||
const context = inject<IconSizeContext>(ICON_STYLE_CONTEXT);
|
||||
function createIconStyleContext(style: IconStyle): IconStyleContext {
|
||||
const isCustomized = computed(() => {
|
||||
return (
|
||||
style.color.value !== STYLE_DEFAULTS.color ||
|
||||
style.strokeWidth.value !== STYLE_DEFAULTS.strokeWidth ||
|
||||
style.size.value !== STYLE_DEFAULTS.size ||
|
||||
style.nonScalingStroke.value !== STYLE_DEFAULTS.nonScalingStroke
|
||||
);
|
||||
});
|
||||
|
||||
function resetStyle() {
|
||||
style.color.value = STYLE_DEFAULTS.color;
|
||||
style.strokeWidth.value = STYLE_DEFAULTS.strokeWidth;
|
||||
style.size.value = STYLE_DEFAULTS.size;
|
||||
style.nonScalingStroke.value = STYLE_DEFAULTS.nonScalingStroke;
|
||||
}
|
||||
|
||||
return {
|
||||
...style,
|
||||
isCustomized,
|
||||
resetStyle,
|
||||
};
|
||||
}
|
||||
|
||||
export const iconStyleContext = createIconStyleContext(persistedIconStyle);
|
||||
|
||||
export function useIconStyle() {
|
||||
return createIconStyleContext({
|
||||
size: ref(STYLE_DEFAULTS.size),
|
||||
strokeWidth: ref(STYLE_DEFAULTS.strokeWidth),
|
||||
color: ref(STYLE_DEFAULTS.color),
|
||||
nonScalingStroke: ref(STYLE_DEFAULTS.nonScalingStroke),
|
||||
});
|
||||
}
|
||||
|
||||
export function usePersistedIconStyle() {
|
||||
return iconStyleContext;
|
||||
}
|
||||
|
||||
if (typeof document !== 'undefined') {
|
||||
watch(
|
||||
[
|
||||
persistedIconStyle.size,
|
||||
persistedIconStyle.strokeWidth,
|
||||
persistedIconStyle.color,
|
||||
persistedIconStyle.nonScalingStroke,
|
||||
],
|
||||
([size, strokeWidth, color, nonScalingStroke]) => {
|
||||
const root = document.documentElement;
|
||||
root.style.setProperty('--customize-size', String(Math.min(256, Math.max(16, size))));
|
||||
root.style.setProperty(
|
||||
'--customize-strokeWidth',
|
||||
String(Math.min(3, Math.max(0.5, strokeWidth))),
|
||||
);
|
||||
if (CSS.supports('color', color)) root.style.setProperty('--customize-color', color);
|
||||
root.classList.toggle('non-scaling-stroke', nonScalingStroke);
|
||||
},
|
||||
{ immediate: true, flush: 'sync' },
|
||||
);
|
||||
}
|
||||
|
||||
export function useIconStyleContext(): IconStyleContext {
|
||||
const context = inject<IconStyleContext>(ICON_STYLE_CONTEXT);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useIconStyleContext must be used with useIconStyleProvider');
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// Run before the body is painted: the generated HTML cannot know local preferences.
|
||||
(() => {
|
||||
try {
|
||||
const root = globalThis.document.documentElement;
|
||||
const size = globalThis.localStorage.getItem('icon-size');
|
||||
const stroke = globalThis.localStorage.getItem('icon-stroke-width');
|
||||
const color = globalThis.localStorage.getItem('icon-color');
|
||||
const nonScaling = globalThis.localStorage.getItem('icon-non-scaling-stroke');
|
||||
if ([size, stroke, color, nonScaling].every((value) => value === null)) return;
|
||||
if (size !== null && Number.isFinite(Number(size))) {
|
||||
root.style.setProperty('--customize-size', String(Math.min(256, Math.max(16, Number(size)))));
|
||||
}
|
||||
if (stroke !== null && Number.isFinite(Number(stroke)))
|
||||
root.style.setProperty(
|
||||
'--customize-strokeWidth',
|
||||
String(Math.min(3, Math.max(0.5, Number(stroke)))),
|
||||
);
|
||||
if (color !== null && globalThis.CSS.supports('color', color))
|
||||
root.style.setProperty('--customize-color', color);
|
||||
root.classList.toggle('non-scaling-stroke', nonScaling === 'true');
|
||||
} catch {
|
||||
// Storage can be unavailable, for example in private browsing mode.
|
||||
}
|
||||
})();
|
||||
@@ -10,14 +10,14 @@ import HomeHeroInfoBefore from './components/home/HomeHeroInfoBefore.vue';
|
||||
import { ICON_STYLE_CONTEXT, iconStyleContext } from './composables/useIconStyle';
|
||||
import { CATEGORY_VIEW_CONTEXT, categoryViewContext } from './composables/useCategoryView';
|
||||
import { EXTERNAL_LIBS_CONTEXT, externalLibContext } from './composables/useExternalLibs';
|
||||
import FrameworkSelect from './components/guide/FrameworkSelect.vue';
|
||||
import SideBarNavBefore from './layouts/SideBarNavBefore.vue';
|
||||
import SnackPlayer from './components/editors/SnackPlayer.vue';
|
||||
|
||||
const theme: Partial<Theme> = {
|
||||
extends: DefaultTheme,
|
||||
Layout() {
|
||||
return h(DefaultTheme.Layout, null, {
|
||||
'sidebar-nav-before': () => h(FrameworkSelect),
|
||||
'sidebar-nav-before': () => h(SideBarNavBefore),
|
||||
'home-hero-info-before': () => h(HomeHeroInfoBefore),
|
||||
'sidebar-nav-after': () => h(IconsSidebarNavAfter),
|
||||
'home-hero-image': () => h(HomeHeroIconsCard),
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useData } from 'vitepress';
|
||||
import VPSidebarGroup from 'vitepress/dist/client/theme-default/components/VPSidebarGroup.vue';
|
||||
import VPNavBarSearch from '../components/overrides/VPNavBarSearch.vue';
|
||||
import FrameworkSelect from '../components/guide/FrameworkSelect.vue';
|
||||
import { guideSidebarTop } from '../../sidebar';
|
||||
|
||||
const { page } = useData();
|
||||
|
||||
const isGuide = computed(() => page.value?.relativePath?.startsWith?.('guide'));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="isGuide">
|
||||
<VPNavBarSearch />
|
||||
<VPSidebarGroup :items="guideSidebarTop" />
|
||||
<FrameworkSelect />
|
||||
</template>
|
||||
</template>
|
||||
@@ -32,6 +32,7 @@ export interface IconEntity extends IconMetaData {
|
||||
export interface Category {
|
||||
name: string;
|
||||
title: string;
|
||||
description: string;
|
||||
icon?: string;
|
||||
iconCount: number;
|
||||
icons?: IconEntity[];
|
||||
|
||||
@@ -15,7 +15,7 @@ In an effort to expand the icon set while remaining true to the original minimal
|
||||
|
||||
### Why should I choose Lucide over Feather Icons?
|
||||
|
||||
- Lucide has expanded its icon set by 500+ in the last few years. Lucide now has over 1000 icons, while Feather has around 287 icons.
|
||||
- Lucide has expanded its icon set by 1600+ in the last few years. Lucide now has over 1800 icons, while Feather has around 287 icons.
|
||||
- Well maintained code base.
|
||||
- Active community.
|
||||
|
||||
|
||||
@@ -153,6 +153,7 @@ deno add lucide-solid
|
||||
```
|
||||
|
||||
:::
|
||||
> `@lucide/solid` is for Solid 2, for Solid 1 use the `lucide-solid` package. Solid 2 is still a release candidate.
|
||||
|
||||
For more details, see the [documentation](./solid/index.md).
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
} from "lucide-solid";
|
||||
```
|
||||
|
||||
### Turn off autocomplete in your IDE
|
||||
## Turn off autocomplete in your IDE
|
||||
|
||||
```json [.vscode/settings.json]
|
||||
{
|
||||
|
||||
@@ -6,7 +6,7 @@ description: Learn about the different types exported by the `lucide-solid` pack
|
||||
# TypeScript Support
|
||||
|
||||
List of exported types from the `lucide-solid` package.
|
||||
These can be used to type your components when using Lucide icons in a TypeScript React project
|
||||
These can be used to type your components when using Lucide icons in a TypeScript Solid project.
|
||||
|
||||
## `LucideProps`
|
||||
|
||||
@@ -61,10 +61,10 @@ interface ButtonProps {
|
||||
label: string;
|
||||
}
|
||||
|
||||
const IconButton = ({ icon: Icon, label }) => {
|
||||
const IconButton = (props: ButtonProps) => {
|
||||
return (
|
||||
<button aria-label={label}>
|
||||
<Icon size={16} />
|
||||
<button aria-label={props.label}>
|
||||
<props.icon size={16} />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -36,6 +36,52 @@ bun add lucide-solid
|
||||
|
||||
:::
|
||||
|
||||
:::: info Using Solid 2?
|
||||
|
||||
Lucide ships one package per major Solid version. This guide covers **Solid 1** and the `lucide-solid` package. If your app is on Solid 2, install `@lucide/solid` instead:
|
||||
|
||||
::: code-group
|
||||
|
||||
```sh [pnpm]
|
||||
pnpm add @lucide/solid
|
||||
```
|
||||
|
||||
```sh [yarn]
|
||||
yarn add @lucide/solid
|
||||
```
|
||||
|
||||
```sh [npm]
|
||||
npm install @lucide/solid
|
||||
```
|
||||
|
||||
```sh [bun]
|
||||
bun add @lucide/solid
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
Both packages export the same icon components with the same props, so the rest of this guide applies unchanged — only the import specifier differs:
|
||||
|
||||
```jsx
|
||||
import { Camera } from '@lucide/solid';
|
||||
```
|
||||
|
||||
What does differ is Solid itself. The changes most likely to affect the code you write around Lucide:
|
||||
|
||||
| | `lucide-solid` | `@lucide/solid` |
|
||||
| -------------------- | ----------------------------- | ----------------------- |
|
||||
| Solid version | `^1.4.7` | `^2.0.0-rc.0` |
|
||||
| DOM runtime package | `solid-js/web` | `@solidjs/web` |
|
||||
| `jsxImportSource` | `solid-js` | `@solidjs/web` |
|
||||
| Splitting icon props | `splitProps(props, ['size'])` | `omit(props, 'size')` |
|
||||
| Providing context | `<Ctx.Provider value={…}>` | `<Ctx value={…}>` |
|
||||
|
||||
See the [Solid 2 migration guide](https://github.com/solidjs/solid/blob/next/documentation/solid-2.0/MIGRATION.md) for the full list.
|
||||
|
||||
Solid 2 is still a release candidate, so treat `@lucide/solid` as pre-release too. The live examples throughout this guide run on Solid 1.
|
||||
|
||||
::::
|
||||
|
||||
## Importing your first icon
|
||||
|
||||
Lucide is built with ES Modules, so it's completely tree-shakable.
|
||||
|
||||
@@ -25,7 +25,7 @@ This is useful if you want all icons to share the same size, color, or stroke wi
|
||||
You can call `setLucideProps` in your main entry file or in a top-level component to set the default properties for all icons.
|
||||
|
||||
```js
|
||||
import { setLucideProps } from '@lucide/svelte';
|
||||
import { setLucideProps } from '@lucide/svelte/context';
|
||||
|
||||
setLucideProps({
|
||||
size: 32,
|
||||
|
||||
+8
-5
@@ -30,10 +30,13 @@
|
||||
"author": "Eric Fennis",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@docsearch/js": "3.8.2",
|
||||
"@lucide/build-icons": "workspace:*",
|
||||
"@lucide/helpers": "workspace:*",
|
||||
"@lucide/shared": "workspace:*",
|
||||
"@rollup/plugin-replace": "^6.0.3",
|
||||
"@types/react": "^19.3.0",
|
||||
"@types/react-dom": "^19.3.0",
|
||||
"@types/semver": "^7.8.0",
|
||||
"h3": "^1.15.11",
|
||||
"markdown-it": "^15.0.1",
|
||||
@@ -41,9 +44,9 @@
|
||||
"nitropack": "^2.13.4",
|
||||
"rollup-plugin-copy": "^3.5.0",
|
||||
"semver": "^7.8.5",
|
||||
"sharp": "^0.35.4",
|
||||
"sharp": "^0.35.5",
|
||||
"shiki": "^4.4.3",
|
||||
"simple-git": "^3.36.0",
|
||||
"simple-git": "^4.0.1",
|
||||
"sitemap": "^9.0.1",
|
||||
"svg-path-commander": "^2.3.3",
|
||||
"svg-pathdata": "^9.0.0",
|
||||
@@ -69,9 +72,9 @@
|
||||
"lucide": "workspace:*",
|
||||
"lucide-react": "workspace:*",
|
||||
"motion-v": "2.4.0",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react": "19.3.0",
|
||||
"react-dom": "19.3.0",
|
||||
"sandpack-vue3": "3.1.12",
|
||||
"vue": "^3.5.42"
|
||||
"vue": "^3.5.43"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 32 32">
|
||||
<path d="M11.622 24.74s-1.23.748.855.962c2.51.32 3.847.267 6.625-.267a10.02 10.02 0 0 0 1.763.855c-6.25 2.672-14.16-.16-9.244-1.55zm-.8-3.473s-1.336 1.015.748 1.23c2.725.267 4.862.32 8.55-.427a3.26 3.26 0 0 0 1.282.801c-7.534 2.244-15.976.214-10.58-1.603zm14.747 6.09s.908.748-1.015 1.336c-3.58 1.07-15.014 1.39-18.22 0-1.122-.48 1.015-1.175 1.7-1.282.695-.16 1.07-.16 1.07-.16-1.23-.855-8.175 1.763-3.526 2.51 12.77 2.084 23.296-.908 19.983-2.404zM12.2 17.633s-5.824 1.39-2.084 1.87c1.603.214 4.755.16 7.694-.053 2.404-.214 4.81-.64 4.81-.64s-.855.374-1.443.748c-5.93 1.55-17.312.855-14.052-.748 2.778-1.336 5.076-1.175 5.076-1.175zm10.42 5.824c5.984-3.1 3.206-6.09 1.282-5.717-.48.107-.695.214-.695.214s.16-.32.534-.427c3.794-1.336 6.786 4.007-1.23 6.09 0 0 .053-.053.107-.16zm-9.83 8.442c5.77.374 14.587-.214 14.8-2.94 0 0-.427 1.07-4.755 1.87-4.916.908-11.007.8-14.587.214 0 0 .748.64 4.542.855z" fill="#4e7896"/>
|
||||
<path d="M18.996.001s3.313 3.366-3.152 8.442c-5.183 4.114-1.175 6.465 0 9.137-3.046-2.725-5.236-5.13-3.74-7.373C14.294 6.893 20.332 5.3 18.996.001zm-1.7 15.335c1.55 1.763-.427 3.366-.427 3.366s3.954-2.03 2.137-4.542c-1.656-2.404-2.94-3.58 4.007-7.587 0 0-10.953 2.725-5.717 8.763z" fill="#f58219"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
Reference in New Issue
Block a user