Merge branch 'main' of https://github.com/lucide-icons/lucide into add-angolia-search

This commit is contained in:
Eric Fennis
2023-07-12 13:01:09 +02:00
340 changed files with 5746 additions and 2158 deletions
@@ -0,0 +1,11 @@
import { eventHandler, setResponseHeader } from 'h3'
import iconMetaData from '../../data/iconMetaData'
export default eventHandler((event) => {
setResponseHeader(event, 'Cache-Control', 'public, max-age=86400')
setResponseHeader(event, 'Access-Control-Allow-Origin', '*')
return Object.fromEntries(
Object.entries(iconMetaData).map(([name, { categories }]) => [ name, categories ])
)
})
@@ -8,6 +8,7 @@ export default eventHandler((event) => {
const withUniqueKeys = query.withUniqueKeys === 'true'
setResponseHeader(event, 'Cache-Control', 'public, max-age=86400')
setResponseHeader(event, 'Access-Control-Allow-Origin', '*')
if (withUniqueKeys) {
return iconNodes
@@ -38,6 +38,7 @@ export default eventHandler((event) => {
defaultContentType(event, 'image/svg+xml')
setResponseHeader(event, 'Cache-Control', 'public,max-age=31536000')
setResponseHeader(event, 'Access-Control-Allow-Origin', '*')
return svg
+1
View File
@@ -3,6 +3,7 @@ import iconMetaData from '../../data/iconMetaData'
export default eventHandler((event) => {
setResponseHeader(event, 'Cache-Control', 'public, max-age=86400')
setResponseHeader(event, 'Access-Control-Allow-Origin', '*')
return Object.fromEntries(
Object.entries(iconMetaData).map(([name, { tags }]) => [ name, tags ])
+2 -5
View File
@@ -5,6 +5,7 @@ import { createWriteStream } from 'node:fs'
import { resolve } from 'node:path'
import { SitemapStream } from 'sitemap'
import sidebar from './sidebar';
import fs from 'fs';
const links = []
@@ -35,7 +36,7 @@ export default defineConfig({
)
}
]
}
},
},
head: [
[ 'script', {
@@ -138,10 +139,6 @@ export default defineConfig({
return
}
if (pageData.relativePath === 'index.md') {
console.log('Home!');
}
if (pageData.relativePath.startsWith('icons/')) {
links.push({
url: pageData.relativePath.replace(/((^|\/)index)?\.md$/, '$2'),
@@ -0,0 +1,55 @@
<script setup lang="ts">
import {onMounted, ref} from 'vue';
import {useFloating, offset, shift} from '@floating-ui/vue';
const reference = ref(null);
const tooltip = ref(null);
const middleware = ref([shift(), offset(8)]);
const { floatingStyles, update } = useFloating(reference, tooltip, {
middleware,
transform: false
});
defineProps<{
title: string
}>()
onMounted(() => {
update()
});
</script>
<template>
<span ref="reference" class="reference">
<slot/>
</span>
<div ref="tooltip" class="tooltip" :style="floatingStyles">
{{title}}
</div>
</template>
<style scoped>
.reference:hover + .tooltip{
opacity: 1;
transform: scale(1);
}
.tooltip {
display: block;
font-size: 12px;
line-height: 20px;
transform: scale(0.9);
font-weight: 400;
background: var(--vp-c-brand-dark);
color: white;
z-index: 10;
white-space: nowrap;
padding: 2px 8px;
border-radius: 4px;
box-shadow: var(--vp-shadow-1);
opacity: 0;
pointer-events: none;
transition: cubic-bezier(0.19, 1, 0.22, 1) .2s;
transition-property: opacity, transform;
}
</style>
@@ -3,19 +3,9 @@ import { ref } from 'vue';
import ButtonMenu from '../base/ButtonMenu.vue'
import { useIconStyleContext } from '../../composables/useIconStyle';
import useConfetti from '../../composables/useConfetti';
import getSVGIcon from '../../utils/getSVGIcon';
import downloadData from '../../utils/downloadData';
const allowedAttrs = [
'xmlns',
'width',
'height',
'viewBox',
'fill',
'stroke',
'stroke-width',
'stroke-linecap',
'stroke-linejoin',
'class',
]
const downloadText = 'Download!'
const copiedText = 'Copied!'
const confettiText = ref(copiedText)
@@ -28,24 +18,6 @@ const { size } = useIconStyleContext()
const { animate, confetti } = useConfetti()
function getSVGIcon() {
const svg = document.querySelector('#previewer svg')
if (!svg) return
const clonedSvg = svg.cloneNode(true) as SVGElement
// Filter out attributes that are not allowed in SVGs
for (const attr of Array.from(clonedSvg.attributes)) {
if (!allowedAttrs.includes(attr.name)) {
clonedSvg.removeAttribute(attr.name)
}
}
const svgString = new XMLSerializer().serializeToString(clonedSvg)
return svgString
}
function copySVG() {
confettiText.value = copiedText
const svgString = getSVGIcon()
@@ -70,11 +42,7 @@ function downloadSVG() {
confettiText.value = downloadText
const svgString = getSVGIcon()
const link = document.createElement('a');
link.download = `${props.name}.svg`;
link.href = `data:image/svg+xml;base64,${btoa(svgString)}`
link.click();
downloadData(`${props.name}.svg`, `data:image/svg+xml;base64,${btoa(svgString)}`)
confetti()
}
@@ -91,12 +59,7 @@ function downloadPNG() {
image.src = `data:image/svg+xml;base64,${btoa(svgString)}`;
image.onload = function() {
ctx.drawImage(image, 0, 0);
const link = document.createElement('a');
link.download = `${props.name}.png`;
link.href = canvas.toDataURL('image/png')
link.click();
downloadData(`${props.name}.png`, canvas.toDataURL('image/png'))
confetti()
}
}
@@ -1,30 +1,38 @@
<script setup lang="ts">
import type { IconEntity } from '../../types'
import { computed, ref, watch } from 'vue'
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon';
import IconButton from '../base/IconButton.vue';
import IconContributors from './IconContributors.vue';
import IconPreview from './IconPreview.vue';
import { x, expand } from '../../../data/iconNodes'
import { useRouter } from 'vitepress';
import IconInfo from './IconInfo.vue';
import Badge from '../base/Badge.vue';
import type { IconEntity } from '../../types'
import { computed, ref, watch } from 'vue'
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon';
import IconButton from '../base/IconButton.vue';
import IconContributors from './IconContributors.vue';
import IconPreview from './IconPreview.vue';
import { x, expand } from '../../../data/iconNodes'
import { useRouter } from 'vitepress';
import IconInfo from './IconInfo.vue';
import Badge from '../base/Badge.vue';
import { computedAsync } from '@vueuse/core';
const props = defineProps<{
icon: IconEntity
}>()
const props = defineProps<{
iconName: string
}>()
const emit = defineEmits(['close'])
const isOpen = computed(() => !!props.icon)
function onClose() {
emit('close')
const icon = computedAsync<IconEntity | null>(async () => {
if (props.iconName) {
return (await import(`../../../data/iconDetails/${props.iconName}.ts`)).default as IconEntity
}
return null
}, null)
const { go } = useRouter()
const emit = defineEmits(['close'])
const isOpen = computed(() => !!icon.value)
const CloseIcon = createLucideIcon('Close', x)
const Expand = createLucideIcon('Expand', expand)
function onClose() {
emit('close')
}
const { go } = useRouter()
const CloseIcon = createLucideIcon('Close', x)
const Expand = createLucideIcon('Expand', expand)
</script>
<template>
@@ -109,9 +117,8 @@
}
.icon-info {
padding: 0 24px;
padding-left: 24px;
flex-basis: 100%;
}
.icon-tags {
@@ -43,5 +43,6 @@ function setActiveIcon(name: string) {
.icon {
aspect-ratio: 1/1;
position: relative;
}
</style>
@@ -17,7 +17,7 @@ const { go } = useRouter()
const { page } = useData()
const tags = computed(() => {
if (!props.icon) return []
if (!props.icon || !props?.icon?.tags) return []
return props.icon.tags.join(' • ')
})
</script>
@@ -27,9 +27,11 @@ const tags = computed(() => {
<IconDetailName class="icon-name">
{{ icon.name }}
</IconDetailName>
<p class="icon-tags">
{{ tags }}
</p>
<div class="tags-scroller" v-if="tags.length">
<p class="icon-tags horizontal-scroller">
{{ tags }}
</p>
</div>
<div class="group">
<Badge
v-for="category in icon.categories"
@@ -72,9 +74,61 @@ const tags = computed(() => {
font-size: 16px;
color: var(--vp-c-text-2);
font-weight: 500;
margin-top: 0;;
margin-bottom: 16px;
line-height: 28px;
white-space: nowrap;
position: absolute;
top: 0;
left: 0;
right: 0;
margin-top: 0;
margin-bottom: 0;
}
.tags-scroller {
position: relative;
max-width: 100%;
width: 100%;
height: 28px;
padding: 8px 0 16px;
margin-bottom: 16px;
margin-top: 8px;
align-items: center;
--gradient-background: var(--tags-gradient-background, var(--vp-c-bg-elv))
}
.horizontal-scroller {
overflow-x: scroll;
/* Hide Scrollbar */
-ms-overflow-style: none;
scrollbar-width: none;
scrollbar-width: thin; /* can also be normal, or none, to not render scrollbar */
scrollbar-color: currentColor transparent; /* foreground background */
}
.horizontal-scroller::-webkit-scrollbar {
width: 0;
display: none
}
.horizontal-scroller::-webkit-scrollbar-track {
background: transparent
}
.horizontal-scroller::-webkit-scrollbar-thumb {
background: transparent;
border: none
}
.tags-scroller::after {
content: '';
position: absolute;
bottom: 0;
width: 32px;
height: 100%;
/* Background Gradient left to right */
background: linear-gradient(to right, rgba(255,255,255,0) 0%,var(--gradient-background) 100%);
right: 0;
pointer-events: none;
}
.buttons {
@@ -2,13 +2,17 @@
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon';
import { useMediaQuery } from '@vueuse/core';
import { useRouter } from 'vitepress';
import getSVGIcon from '../../utils/getSVGIcon';
import useConfetti from '../../composables/useConfetti';
import Tooltip from '../base/Tooltip.vue';
const downloadText = 'Download!'
const copiedText = 'Copied!'
export type IconNode = [elementName: string, attrs: Record<string, string>][]
const props = defineProps<{
name: string;
// tags: string[];
// categories: string[];
iconNode: IconNode;
active: boolean;
customizable?: boolean;
@@ -20,40 +24,65 @@ const emit = defineEmits(['setActiveIcon'])
const { go } = useRouter()
const showOverlay = useMediaQuery('(min-width: 860px)');
const { animate, confetti, confettiText } = useConfetti()
const icon = createLucideIcon(props.name, props.iconNode)
function navigateToIcon() {
async function navigateToIcon(event) {
if (event.shiftKey) {
event.preventDefault()
const svgString = getSVGIcon(event.target.firstChild, {
class: `lucide lucide-${props.name}`,
})
await navigator.clipboard.writeText(svgString)
confettiText.value = copiedText
confetti()
return
}
if(props.overlayMode && showOverlay.value) {
event.preventDefault()
window.history.pushState({}, '', `/icons/${props.name}`)
emit('setActiveIcon', props.name)
}
else {
event.preventDefault()
go(`/icons/${props.name}`)
}
}
</script>
<template>
<button
class="icon-button"
@click="navigateToIcon"
:class="{ 'active' : active }"
:data-title="name"
:aria-label="name"
:href="`/icons/${props.name}`"
>
<KeepAlive>
<component
v-if="!hideIcon"
:is="icon"
class="lucide-icon"
:class="{ customizable }"
/>
</KeepAlive>
</button>
<Tooltip :title="name">
<a
class="icon-button confetti-button vp-raw"
@click="navigateToIcon"
:class="{ active, animate }"
:aria-label="name"
:href="`/icons/${props.name}`"
:data-confetti-text="confettiText"
ref="ref"
>
<KeepAlive>
<component
v-if="!hideIcon"
:is="icon"
class="lucide-icon"
:class="{
customizable,
}"
/>
</KeepAlive>
</a>
</Tooltip>
</template>
<style src="./confetti.css" />
<style scoped>
.icon-button {
display: inline-block;
@@ -72,35 +101,13 @@ function navigateToIcon() {
color: var(--vp-c-text-1);
}
.icon-button:hover:before {
opacity: 1;
transform: translate(-50%, 48px) scale(1);
.confetti-button:before,
.confetti-button:after {
z-index: 100;
}
.icon-button:before {
content: attr(data-title);
display: block;
font-size: 12px;
line-height: 20px;
margin-left: 27px;
transform: translate(-50%, 48px) scale(0.9);
font-weight: 400;
position: absolute;
background: var(--vp-c-brand-dark);
color: white;
z-index: 10;
white-space: nowrap;
padding: 2px 8px;
border-radius: 4px;
box-shadow: var(--vp-shadow-1);
opacity: 0;
pointer-events: none;
transition: cubic-bezier(0.19, 1, 0.22, 1) .2s;
transition-property: opacity, transform;
/* max-width: calc((32px * 2) + 56px); */
overflow: hidden;
white-space: pre-wrap;
word-break: break-word;
.confetti-button:before {
line-height: 80px;
}
.icon-button:active {
@@ -139,6 +146,7 @@ function navigateToIcon() {
.lucide-icon {
margin: auto;
pointer-events: none;
}
.lucide-icon.customizable {
will-change: width, height, stroke-width, stroke;
@@ -6,6 +6,9 @@ import InputSearch from '../base/InputSearch.vue'
import useSearchInput from '../../composables/useSearchInput'
import StickyBar from './StickyBar.vue'
import IconsCategory from './IconsCategory.vue'
import { useFetch } from '@vueuse/core'
import useFetchTags from '../../composables/useFetchTags'
import useFetchCategories from '../../composables/useFetchCategories'
const props = defineProps<{
icons: IconEntity[]
@@ -22,7 +25,26 @@ function setActiveIconName(name: string) {
activeIconName.value = name
}
const searchResults = useSearch(searchQuery, props.icons, [
const { execute: fetchTags, data: tags } = useFetchTags()
const { execute: fetchCategories, data: categoriesMap } = useFetchCategories()
const mappedIcons = computed(() => {
if(tags.value == null) {
return props.icons
}
return props.icons.map((icon) => {
const iconTags = tags.value[icon.name]
const iconCategories = categoriesMap.value?.[icon.name] ?? []
return {
...icon,
tags: iconTags,
categories: iconCategories,
}
})
})
const searchResults = useSearch(searchQuery, mappedIcons, [
{ name: 'name', weight: 2 },
{ name: 'tags', weight: 1 },
])
@@ -37,7 +59,6 @@ const categories = computed(() => {
return iconCategories?.includes(name)
})
const searchedCategoryIcons = isSearching
? categoryIcons.filter(icon => searchResults.value.some((item) => item?.name === icon?.name))
: categoryIcons;
@@ -51,9 +72,14 @@ const categories = computed(() => {
.filter(({ icons }) => icons.length)
})
const activeIcon = computed(() =>
props.icons?.find((icon) => icon.name === activeIconName.value)
)
function onFocusSearchInput() {
if (tags.value == null) {
fetchTags()
}
if (categoriesMap.value == null) {
fetchCategories()
}
}
const NoResults = defineAsyncComponent(() =>
import('./NoResults.vue')
@@ -71,6 +97,7 @@ const IconDetailOverlay = defineAsyncComponent(() =>
v-model="searchQuery"
class="input-wrapper"
ref="searchInput"
@focus="onFocusSearchInput"
/>
</StickyBar>
<NoResults
@@ -87,7 +114,7 @@ const IconDetailOverlay = defineAsyncComponent(() =>
/>
<IconDetailOverlay
v-if="activeIconName != null"
:icon="activeIcon"
:iconName="activeIconName"
@close="setActiveIconName('')"
/>
</template>
@@ -8,6 +8,8 @@ import useSearch from '../../composables/useSearch'
import EndOfPage from '../base/EndOfPage.vue'
import useSearchInput from '../../composables/useSearchInput'
import StickyBar from './StickyBar.vue'
import useFetchTags from '../../composables/useFetchTags'
import useFetchCategories from '../../composables/useFetchCategories'
const props = defineProps<{
icons: IconEntity[]
@@ -22,7 +24,7 @@ const isSmallScreen = useMediaQuery('(min-width: 640px)');
const pageSize = computed(() => {
if(isExtraLargeScreen.value) {
return 16 * 16;
return 16 * 20;
}
if(isLargeScreen.value) {
return 16 * 12;
@@ -38,8 +40,28 @@ const pageSize = computed(() => {
return 10 * 5;
})
const { execute: fetchTags, data: tags } = useFetchTags()
const { execute: fetchCategories, data: categories } = useFetchCategories()
const mappedIcons = computed(() => {
if(tags.value == null) {
return props.icons
}
return props.icons.map((icon) => {
const iconTags = tags.value[icon.name]
const iconCategories = categories.value?.[icon.name] ?? []
return {
...icon,
tags: iconTags,
categories: iconCategories,
}
})
})
const { searchInput, searchQuery, searchQueryThrottled } = useSearchInput()
const searchResults = useSearch(searchQueryThrottled, props.icons, [
const searchResults = useSearch(searchQueryThrottled, mappedIcons, [
{ name: 'name', weight: 3 },
{ name: 'tags', weight: 2 },
{ name: 'categories', weight: 1 },
@@ -58,12 +80,19 @@ function setActiveIconName(name: string) {
activeIconName.value = name
}
const activeIcon = computed(() => props.icons.find((icon) => icon.name === activeIconName.value))
watch(searchQueryThrottled, (searchString) => {
currentPage.value = 1
})
function onFocusSearchInput() {
if (tags.value == null) {
fetchTags()
}
if (categories.value == null) {
fetchCategories()
}
}
const NoResults = defineAsyncComponent(() =>
import('./NoResults.vue')
)
@@ -81,6 +110,7 @@ const IconDetailOverlay = defineAsyncComponent(() =>
v-model="searchQuery"
ref="searchInput"
class="input-wrapper"
@focus="onFocusSearchInput"
/>
</StickyBar>
<NoResults
@@ -97,7 +127,7 @@ const IconDetailOverlay = defineAsyncComponent(() =>
<EndOfPage @end-of-page="next" class="bottom-page"/>
<IconDetailOverlay
v-if="activeIconName != null"
:icon="activeIcon"
:iconName="activeIconName"
@close="setActiveIconName('')"
/>
</template>
@@ -32,8 +32,8 @@ useEventListener(document, 'mousemove', (mouseEvent) => {
No icons found for '{{ searchQuery }}'
</h2>
<VPButton text="Clear your search and try again" theme="alt" @click="$emit('clear')"/>
or
<VPButton text="Check if someone has already requested this icon"
<span class="text-divider">or</span>
<VPButton text="Search on Github issues"
theme="alt"
:href="`https://github.com/lucide-icons/lucide/issues?q=is%3Aopen+${searchQuery}`"
target="_blank"
@@ -74,4 +74,10 @@ useEventListener(document, 'mousemove', (mouseEvent) => {
margin-bottom: 32px;
text-align: center;
}
.text-divider {
margin: 12px 0;
font-size: 16px;
color: var(--vp-c-neutral);
}
</style>
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { shallowRef, type Ref, watch } from 'vue'
import { shallowRef, type Ref, watch, computed } from 'vue'
import { useCssVar, syncRef } from '@vueuse/core'
import { useIconStyleContext } from '../../composables/useIconStyle'
import { STYLE_DEFAULTS, useIconStyleContext } from '../../composables/useIconStyle'
import RangeSlider from '../base/RangeSlider.vue'
import InputField from '../base/InputField.vue'
import ColorPicker from '../base/ColorPicker.vue'
@@ -19,7 +19,7 @@ const colorCssVar = useCssVar(
'--customize-color',
props.rootEl?.value ?? documentRef.value,
{
initialValue: 'default'
initialValue: `${STYLE_DEFAULTS.color}`
}
)
@@ -27,7 +27,7 @@ const strokeWidthCssVar = useCssVar(
'--customize-strokeWidth',
props.rootEl?.value ?? documentRef.value,
{
initialValue: '2'
initialValue: `${STYLE_DEFAULTS.strokeWidth}`
}
)
@@ -35,7 +35,7 @@ const sizeCssVar = useCssVar(
'--customize-size',
props.rootEl?.value ?? documentRef.value,
{
initialValue: '24'
initialValue: `${STYLE_DEFAULTS.size}`
}
)
@@ -44,9 +44,9 @@ syncRef(strokeWidth, strokeWidthCssVar, { direction: 'ltr' })
syncRef(size, sizeCssVar, { direction: 'ltr' })
function resetStyle () {
color.value = 'currentColor'
strokeWidth.value = 2
size.value = 24
color.value = STYLE_DEFAULTS.color
strokeWidth.value = STYLE_DEFAULTS.strokeWidth
size.value = STYLE_DEFAULTS.size
}
watch(absoluteStrokeWidth, (enabled) => {
@@ -54,10 +54,18 @@ watch(absoluteStrokeWidth, (enabled) => {
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
})
</script>
<template>
<div class="customizer-card">
<div class="customizer-card" :class="{ customized: customizingActive }">
<div class="card-header">
<h2 class="card-title">
Customizer
@@ -142,6 +150,12 @@ watch(absoluteStrokeWidth, (enabled) => {
margin-bottom: 24px;
position: relative;
z-index: 0;
border: 1px solid transparent;
transition: border-color .4s ease-in-out;
}
.customizer-card.customized {
border-color: var(--vp-c-brand);
}
.color-picker {
@@ -2,6 +2,7 @@ import { ref } from "vue";
export default function useConfetti() {
const animate = ref(false)
const confettiText = ref('confetti!')
function confetti() {
animate.value = true;
@@ -13,6 +14,7 @@ export default function useConfetti() {
return {
animate,
confetti
confetti,
confettiText
}
}
@@ -0,0 +1,12 @@
import { useFetch } from "@vueuse/core"
const useFetchCategories = () => useFetch<Record<string, string[]>>(
`${import.meta.env.DEV ? 'http://localhost:3000' : ''}/api/categories`,
{
immediate:
typeof window !== 'undefined'
&& new URLSearchParams(window.location.search).has('search'),
}
).json()
export default useFetchCategories
@@ -0,0 +1,12 @@
import { useFetch } from "@vueuse/core"
const useFetchTags = () => useFetch<Record<string, string[]>>(
`${import.meta.env.DEV ? 'http://localhost:3000' : ''}/api/tags`,
{
immediate:
typeof window !== 'undefined'
&& new URLSearchParams(window.location.search).has('search'),
}
).json()
export default useFetchTags
@@ -10,8 +10,16 @@ interface IconSizeContext {
size: Ref<number>
strokeWidth: Ref<number>
color: Ref<string>
absoluteStrokeWidth: Ref<boolean>
}
export const STYLE_DEFAULTS = {
size: 24,
strokeWidth: 2,
color: 'currentColor',
absoluteStrokeWidth: false,
};
export const iconStyleContext = {
size: ref(24),
strokeWidth: ref(2),
@@ -1,20 +1,22 @@
import Fuse from 'fuse.js';
import { shallowRef, computed, Ref } from 'vue';
const useSearch = <T>(query: Ref<string>, collection: T[], keys: Fuse.FuseOptionKey<T>[] = []) => {
const useSearch = <T>(query: Ref<string>, collection: Ref<T[]>, keys: Fuse.FuseOptionKey<T>[] = []) => {
const index = shallowRef(
new Fuse(collection, {
new Fuse(collection.value, {
threshold: 0.2,
keys,
})
)
const results = computed(() => {
index.value.setCollection(collection.value);
if (query.value) {
return index.value.search(query.value).map((result) => result.item);
}
return collection;
return collection.value;
});
return results;
@@ -11,12 +11,16 @@ const useSearchInput = () => {
|| ''
)
)
const searchQueryThrottled = refThrottled(searchQuery, 200)
const searchQueryThrottled = refThrottled(searchQuery, 400)
watch(searchQueryThrottled, (searchString) => {
const newUrl = new URL(window.location.href);
newUrl.searchParams.set('search', searchString);
if(searchString === '') {
newUrl.searchParams.delete('search');
} else {
newUrl.searchParams.set('search', searchString);
}
nextTick(() => {
window.history.replaceState({}, '', newUrl)
@@ -0,0 +1,6 @@
export default function downloadData(filename:string, data:string) {
const link = document.createElement('a');
link.download = filename;
link.href = data
link.click();
}
+34
View File
@@ -0,0 +1,34 @@
const allowedAttrs = [
'xmlns',
'width',
'height',
'viewBox',
'fill',
'stroke',
'stroke-width',
'stroke-linecap',
'stroke-linejoin',
'class',
]
export default function getSVGIcon(element?: HTMLElement, attrs?: Record<string, string>) {
const svg = element ?? document.querySelector('#previewer svg')
if (!svg) return
const clonedSvg = svg.cloneNode(true) as SVGElement
// Filter out attributes that are not allowed in SVGs
for (const attr of Array.from(clonedSvg.attributes)) {
if (!allowedAttrs.includes(attr.name)) {
clonedSvg.removeAttribute(attr.name)
}
}
for (const [key, value] of Object.entries(attrs ?? {})) {
clonedSvg.setAttribute(key, value)
}
const svgString = new XMLSerializer().serializeToString(clonedSvg)
return svgString
}
+1 -1
View File
@@ -89,7 +89,7 @@ export default Icon;
import Icon from './Icon';
const App = () => {
return <Icon name="home" />;
return <Icon name="Home" />;
};
export default App;
+17 -13
View File
@@ -41,47 +41,47 @@ const codeExample = computed(() => data.codeExamples?.map(
<div :class="$style.iconPreviews">
<IconPreview
id="previewer"
:name="$params.name"
:iconNode="$params.iconNode"
:name="params.name"
:iconNode="params.iconNode"
:class="$style.preview"
/>
<IconPreviewSmall
:name="$params.name"
:iconNode="$params.iconNode"
:name="params.name"
:iconNode="params.iconNode"
:class="$style.smallPreview"
/>
</div>
<div >
<div :class="$style.info">
<IconInfo :icon="$params" />
<IconInfo :icon="params" />
<div :class="$style.meta">
<div
v-if="$params.createdRelease?.version"
v-if="params.createdRelease?.version"
:class="$style.version"
>
<Label>Created:</Label>
<Badge
:href="`https://github.com/lucide-icons/lucide/releases/tag/v${$params.createdRelease.version}`"
:href="`https://github.com/lucide-icons/lucide/releases/tag/v${params.createdRelease.version}`"
target="_blank"
rel="noreferrer noopener"
>
v{{$params.createdRelease.version}}
v{{params.createdRelease.version}}
</Badge>
</div>
<div
v-if="$params.changedRelease?.version"
v-if="params.changedRelease?.version"
:class="$style.version"
>
<Label>Last changed:</Label>
<Badge
:href="`https://github.com/lucide-icons/lucide/releases/tag/v${$params.changedRelease.version}`"
:href="`https://github.com/lucide-icons/lucide/releases/tag/v${params.changedRelease.version}`"
target="_blank"
rel="noreferrer noopener"
>
v{{$params.changedRelease.version}}
v{{params.changedRelease.version}}
</Badge>
</div>
<IconContributors :icon="$params" :class="$style.contributors"/>
<IconContributors :icon="params" :class="$style.contributors"/>
</div>
</div>
<CodeGroup
@@ -97,7 +97,7 @@ const codeExample = computed(() => data.codeExamples?.map(
</div>
</div>
<RelatedIcons :icons="$params.relatedIcons" />
<RelatedIcons :icons="params.relatedIcons" />
<style module>
.preview {
@@ -117,6 +117,10 @@ const codeExample = computed(() => data.codeExamples?.map(
margin-top: 24px;
}
.info {
--tags-gradient-background: var(--vp-c-bg);
}
.version, .contributors {
display: flex;
flex-wrap: wrap;
+4 -4
View File
@@ -1,18 +1,18 @@
import { getAllData } from "../.vitepress/lib/icons";
import relatedIcons from '../.vitepress/data/relatedIcons.json'
import iconNodes from '../.vitepress/data/iconNodes'
import * as iconDetails from '../.vitepress/data/iconDetails'
import { IconEntity } from "../.vitepress/theme/types";
export default {
paths: async () => {
const icons = await getAllData()
return icons.map((iconEntity) => {
return (Object.values(iconDetails) as unknown as IconEntity[]).map((iconEntity) => {
const params = {
...iconEntity,
relatedIcons: relatedIcons[iconEntity.name].map((name: string) => ({
name,
iconNode: iconNodes[name],
})),
}))
}
return {
+2 -2
View File
@@ -1,9 +1,9 @@
import { getAllData } from '../.vitepress/lib/icons'
import iconNodes from '../.vitepress/data/iconNodes'
export default {
async load() {
return {
icons: await getAllData(),
icons: Object.entries(iconNodes).map(([name, iconNode]) => ({ name, iconNode })),
}
}
}
+2
View File
@@ -13,6 +13,7 @@
"prebuild:metaJson": "node ../scripts/writeIconMetaIndex.mjs",
"prebuild:releaseJson": "node ../scripts/writeReleaseMetadata.mjs",
"prebuild:relatedIcons": "node ../scripts/writeIconRelatedIcons.mjs",
"prebuild:iconDetails": "node ../scripts/writeIconDetails.mjs",
"postbuild:vercelJson": "node ../scripts/writeVercelOutput.mjs",
"dev": "npx nitropack dev",
"build:api": "npx nitropack build",
@@ -29,6 +30,7 @@
"vitepress": "1.0.0-beta.1"
},
"dependencies": {
"@floating-ui/vue": "^1.0.1",
"@headlessui/vue": "^1.7.13",
"@vueuse/components": "^10.1.0",
"@vueuse/core": "^10.1.0",