mirror of
https://github.com/lucide-icons/lucide.git
synced 2026-05-18 10:24:49 +02:00
Site improvements (#1366)
* write cions details * add details * Add icons details * update gitignore * Add node details * Move tags api to own composable * remove overridden var * remopve whitespace * log directory existence * Fix path name * Fix build * Fix tags fetching * Move max related icons to prebuild * Improve tags scroller * Add categories call * cleanup * Add active state
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -30,6 +30,7 @@ docs/.vitepress/data/iconNodes
|
||||
docs/.vitepress/data/iconMetaData.ts
|
||||
docs/.vitepress/data/releaseMetaData.json
|
||||
docs/.vitepress/data/releaseMetaData
|
||||
docs/.vitepress/data/iconDetails
|
||||
docs/.vitepress/data/relatedIcons.json
|
||||
docs/.vercel
|
||||
docs/.nitro
|
||||
|
||||
11
docs/.vitepress/api/categories/index.get.ts
Normal file
11
docs/.vitepress/api/categories/index.get.ts
Normal file
@@ -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
|
||||
|
||||
|
||||
@@ -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 ])
|
||||
|
||||
@@ -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', {
|
||||
@@ -130,10 +131,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'),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
12
docs/.vitepress/theme/composables/useFetchCategories.ts
Normal file
12
docs/.vitepress/theme/composables/useFetchCategories.ts
Normal file
@@ -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
|
||||
12
docs/.vitepress/theme/composables/useFetchTags.ts
Normal file
12
docs/.vitepress/theme/composables/useFetchTags.ts
Normal file
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 })),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
59
scripts/writeIconDetails.mjs
Normal file
59
scripts/writeIconDetails.mjs
Normal file
@@ -0,0 +1,59 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import renderIconsObject from './render/renderIconsObject.mjs';
|
||||
import { readSvgDirectory, toCamelCase } from './helpers.mjs';
|
||||
|
||||
const currentDir = process.cwd();
|
||||
const ICONS_DIR = path.resolve(currentDir, '../icons');
|
||||
const icons = readSvgDirectory(ICONS_DIR, '.json');
|
||||
|
||||
const iconDetailsDirectory = path.resolve(currentDir, '.vitepress/data', 'iconDetails');
|
||||
|
||||
if (fs.existsSync(iconDetailsDirectory)) {
|
||||
fs.rmSync(iconDetailsDirectory, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
if (!fs.existsSync(iconDetailsDirectory)) {
|
||||
fs.mkdirSync(iconDetailsDirectory);
|
||||
}
|
||||
|
||||
const indexFile = path.resolve(iconDetailsDirectory, `index.ts`);
|
||||
|
||||
const writeIconFiles = icons.map(async (iconFileName) => {
|
||||
const iconName = path.basename(iconFileName, '.json');
|
||||
const location = path.resolve(iconDetailsDirectory, `${iconName}.ts`);
|
||||
|
||||
const contents = `\
|
||||
import iconNode from '../iconNodes/${iconName}.node.json'
|
||||
import metaData from '../../../../icons/${iconName}.json'
|
||||
import releaseData from '../releaseMetadata/${iconName}.json'
|
||||
|
||||
const { tags, categories } = metaData
|
||||
|
||||
const iconDetails = {
|
||||
name: '${iconName}',
|
||||
iconNode,
|
||||
contributors: ['ericfennis', 'karsa-mistmere'],
|
||||
tags,
|
||||
categories,
|
||||
...releaseData,
|
||||
}
|
||||
|
||||
export default iconDetails
|
||||
`;
|
||||
|
||||
await fs.promises.writeFile(location, contents, 'utf-8');
|
||||
await fs.promises.appendFile(
|
||||
indexFile,
|
||||
`export { default as ${toCamelCase(iconName)} } from './${iconName}';\n`,
|
||||
'utf-8',
|
||||
);
|
||||
});
|
||||
|
||||
Promise.all(writeIconFiles)
|
||||
.then(() => {
|
||||
console.log('Successfully write', writeIconFiles.length, 'iconDetails files.');
|
||||
})
|
||||
.catch((error) => {
|
||||
throw new Error(`Something went wrong generating iconNode files,\n ${error}`);
|
||||
});
|
||||
@@ -31,7 +31,9 @@ const writeIconFiles = Object.entries(icons).map(async ([iconName, { children }]
|
||||
const output = JSON.stringify(iconNode, null, 2);
|
||||
await fs.promises.writeFile(location, output, 'utf-8');
|
||||
|
||||
iconIndexFileImports.push(`import ${toCamelCase(iconName)}Node from './${iconName}.node.json';`);
|
||||
iconIndexFileImports.push(
|
||||
`import ${toCamelCase(iconName)}Node from './${iconName}.node.json' assert { type: "json" };`,
|
||||
);
|
||||
iconIndexFileExports.push(` ${toCamelCase(iconName)}Node as ${toCamelCase(iconName)},`);
|
||||
iconIndexFileDefaultExports.push(` '${iconName}': ${toCamelCase(iconName)}Node,`);
|
||||
});
|
||||
|
||||
@@ -16,6 +16,8 @@ const nameWeight = 5;
|
||||
const tagWeight = 4;
|
||||
const categoryWeight = 3;
|
||||
|
||||
const MAX_RELATED_ICONS = 4 * 17 // grid of 4x17 icons, = 68 icons
|
||||
|
||||
const arrayMatches = (a, b) => {
|
||||
// let matches = 0;
|
||||
// for (let i = 0; i < a.length; ++i) {
|
||||
@@ -44,6 +46,7 @@ const getRelatedIcons = (currentIcon, icons) => {
|
||||
.filter(a => a.similarity > 0) // @todo: maybe require a minimal non-zero similarity
|
||||
.sort((a, b) => b.similarity - a.similarity)
|
||||
.map(i => i.icon)
|
||||
.slice(0, MAX_RELATED_ICONS)
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,11 +13,20 @@ const currentDir = process.cwd();
|
||||
const ICONS_DIR = path.resolve(currentDir, '../icons');
|
||||
const iconJsonFiles = readSvgDirectory(ICONS_DIR, '.json');
|
||||
const location = path.resolve(currentDir, '.vitepress/data', 'releaseMetaData.json');
|
||||
const releaseMetaDataDirectory = path.resolve(currentDir, '.vitepress/data', 'releaseMetadata');
|
||||
|
||||
if (fs.existsSync(location)) {
|
||||
fs.unlinkSync(location);
|
||||
}
|
||||
|
||||
if (fs.existsSync(releaseMetaDataDirectory)) {
|
||||
fs.rmSync(releaseMetaDataDirectory, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
if (!fs.existsSync(releaseMetaDataDirectory)) {
|
||||
fs.mkdirSync(releaseMetaDataDirectory);
|
||||
}
|
||||
|
||||
const fetchAllReleases = async () => {
|
||||
await git.fetch('https://github.com/lucide-icons/lucide.git', '--tags');
|
||||
|
||||
@@ -129,6 +138,7 @@ try {
|
||||
const releaseMetaData = await Promise.all(
|
||||
iconJsonFiles.map(async (iconJsonFile) => {
|
||||
const iconName = path.basename(iconJsonFile, '.json');
|
||||
const metaDir = path.resolve(releaseMetaDataDirectory, `${iconName}.json`);
|
||||
|
||||
if (iconName in newReleaseMetaData === false) {
|
||||
console.error(`Could not find release metadata for icon '${iconName}'.`);
|
||||
@@ -153,6 +163,9 @@ try {
|
||||
});
|
||||
}
|
||||
|
||||
const output = JSON.stringify(contents, null, 2);
|
||||
await fs.promises.writeFile(metaDir, output, 'utf-8');
|
||||
|
||||
return [iconName, contents];
|
||||
}),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user