mirror of
https://github.com/lucide-icons/lucide.git
synced 2026-11-13 13:48:26 +00:00
Add version filter
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
import { eventHandler, setResponseHeader } from 'h3';
|
||||
import releaseMeta from '../../data/releaseMetaData.json';
|
||||
|
||||
export default eventHandler((event) => {
|
||||
setResponseHeader(event, 'Cache-Control', 'public, max-age=86400');
|
||||
setResponseHeader(event, 'Access-Control-Allow-Origin', '*');
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(releaseMeta).map(
|
||||
([name, { createdRelease }]) => [name, createdRelease.version]
|
||||
)
|
||||
);
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
export default eventHandler(() => {
|
||||
return { nitro: 'Is Awesome! asda' };
|
||||
});
|
||||
@@ -29,16 +29,17 @@ defineExpose({
|
||||
|
||||
<template>
|
||||
<div class="input-wrapper">
|
||||
<slot name="icon" class="icon" />
|
||||
<slot name="startIcon" class="icon" />
|
||||
<input
|
||||
:type="type"
|
||||
class="input"
|
||||
:class="{'has-icon': $slots.icon}"
|
||||
:class="{'has-icon': $slots.startIcon }"
|
||||
ref="input"
|
||||
:value="modelValue"
|
||||
v-bind="$attrs"
|
||||
@input="$emit('update:modelValue', $event.target.value)"
|
||||
/>
|
||||
<slot name="endIcon" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -55,6 +56,7 @@ defineExpose({
|
||||
height: 40px;
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
font-size: 14px;
|
||||
transition: border-color .2s ease-in-out;
|
||||
}
|
||||
|
||||
.input:hover, .input:focus {
|
||||
@@ -70,7 +72,7 @@ defineExpose({
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.input-wrapper svg {
|
||||
.input-wrapper > svg {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 12px;
|
||||
|
||||
@@ -8,9 +8,10 @@ export default {
|
||||
import { computed, ref } from 'vue'
|
||||
import Input from './Input.vue'
|
||||
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon'
|
||||
import { search } from '../../../data/iconNodes'
|
||||
import { search, x } from '../../../data/iconNodes'
|
||||
|
||||
const SearchIcon = createLucideIcon('search', search)
|
||||
const XIcon = createLucideIcon('close', x)
|
||||
|
||||
interface Props {
|
||||
modelValue: string
|
||||
@@ -32,6 +33,10 @@ const value = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
const clear = () => {
|
||||
value.value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -43,9 +48,16 @@ const value = computed({
|
||||
v-model="value"
|
||||
class="input-wrapper"
|
||||
>
|
||||
<template #icon>
|
||||
<template #startIcon>
|
||||
<component :is="SearchIcon" class="search-icon" />
|
||||
</template>
|
||||
|
||||
<template #endIcon>
|
||||
<button class="clear-button" type="button" v-if="value">
|
||||
<component :is="XIcon" class="x-icon" @click="clear" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
</Input>
|
||||
</template>
|
||||
|
||||
@@ -57,7 +69,9 @@ const value = computed({
|
||||
padding: 0 10px 0 12px;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
color: var(--vp-c-text-1);
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
transition: border-color .15s ease-in-out;
|
||||
}
|
||||
|
||||
.input:hover, .input:focus {
|
||||
@@ -66,10 +80,30 @@ const value = computed({
|
||||
}
|
||||
|
||||
.input-wrapper:deep(.input) {
|
||||
/* padding: 12px 24px; */
|
||||
padding-block: 12px;
|
||||
font-size: 14px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.clear-button {
|
||||
position: absolute;
|
||||
padding: 4px;
|
||||
right: 8px;
|
||||
top: 8px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: transparent;
|
||||
border-radius: 6px;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.clear-button:hover {
|
||||
background: var(--vp-button-alt-bg);
|
||||
}
|
||||
|
||||
.input-wrapper:deep(.input)::-webkit-search-decoration,
|
||||
.input-wrapper:deep(.input)::-webkit-search-cancel-button,
|
||||
.input-wrapper:deep(.input)::-webkit-search-results-button,
|
||||
.input-wrapper:deep(.input)::-webkit-search-results-decoration { display: none; }
|
||||
|
||||
</style>
|
||||
|
||||
@@ -45,7 +45,6 @@
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
/* background-color: var(--vp-c-neutral); */
|
||||
box-shadow: var(--vp-shadow-1);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { useData } from 'vitepress';
|
||||
import { useSessionStorage } from '@vueuse/core';
|
||||
import IconButton from '../base/IconButton.vue';
|
||||
import VPDocAsideCarbonAds from 'vitepress/dist/client/theme-default/components/VPDocAsideCarbonAds.vue'
|
||||
// import VPDocAsideCarbonAds from 'vitepress/dist/client/theme-default/components/VPDocAsideCarbonAds.vue'
|
||||
import { x } from '../../../data/iconNodes'
|
||||
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, defineAsyncComponent, onMounted, watch } from 'vue';
|
||||
import { ref, computed, defineAsyncComponent, onMounted, watch, watchEffect } from 'vue';
|
||||
import type { IconEntity } from '../../types';
|
||||
import { useElementSize, useEventListener, useVirtualList } from '@vueuse/core';
|
||||
import IconGrid from './IconGrid.vue';
|
||||
@@ -9,8 +9,11 @@ import useSearchInput from '../../composables/useSearchInput';
|
||||
import StickyBar from './StickyBar.vue';
|
||||
import useFetchTags from '../../composables/useFetchTags';
|
||||
import useFetchCategories from '../../composables/useFetchCategories';
|
||||
import useFetchReleaseInfo from '../../composables/useFetchReleaseInfo';
|
||||
import chunkArray from '../../utils/chunkArray';
|
||||
import CarbonAdOverlay from './CarbonAdOverlay.vue';
|
||||
import VersionSelect from './VersionSelect.vue';
|
||||
import { sort, satisfies } from 'semver';
|
||||
|
||||
const ICON_SIZE = 56;
|
||||
const ICON_GRID_GAP = 8;
|
||||
@@ -21,9 +24,11 @@ const props = defineProps<{
|
||||
}>();
|
||||
|
||||
const activeIconName = ref(null);
|
||||
const selectedVersion = ref('Latest version');
|
||||
|
||||
const { execute: fetchTags, data: tags } = useFetchTags();
|
||||
const { execute: fetchCategories, data: categories } = useFetchCategories();
|
||||
const { execute: fetchReleaseInfo, data: releaseInfo } = useFetchReleaseInfo();
|
||||
|
||||
const overviewEl = ref<HTMLElement | null>(null);
|
||||
const { width: containerWidth } = useElementSize(overviewEl)
|
||||
@@ -33,22 +38,48 @@ const columnSize = computed(() => {
|
||||
});
|
||||
|
||||
const mappedIcons = computed(() => {
|
||||
if (tags.value == null) {
|
||||
return props.icons;
|
||||
let icons = props.icons;
|
||||
|
||||
|
||||
if (tags.value != null && categories.value != null) {
|
||||
icons = props.icons.map((icon) => {
|
||||
const iconTags = tags.value[icon.name];
|
||||
const iconCategories = categories.value?.[icon.name] ?? [];
|
||||
|
||||
return {
|
||||
...icon,
|
||||
tags: iconTags,
|
||||
categories: iconCategories,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return props.icons.map((icon) => {
|
||||
const iconTags = tags.value[icon.name];
|
||||
const iconCategories = categories.value?.[icon.name] ?? [];
|
||||
if (selectedVersion.value === 'Latest version' || releaseInfo.value == null) {
|
||||
console.log('no release info');
|
||||
|
||||
return {
|
||||
...icon,
|
||||
tags: iconTags,
|
||||
categories: iconCategories,
|
||||
};
|
||||
return icons;
|
||||
}
|
||||
|
||||
return icons.filter((icon) => {
|
||||
return satisfies(
|
||||
releaseInfo.value[icon.name], `<=${selectedVersion.value}`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const versions = computed(() => {
|
||||
if (releaseInfo.value == null) {
|
||||
return [];
|
||||
}
|
||||
const allVersions = Array.from<string>(
|
||||
new Set(Object.values<string>(releaseInfo.value)).values()
|
||||
)
|
||||
|
||||
return sort(allVersions, {
|
||||
loose: true,
|
||||
}).reverse();
|
||||
});
|
||||
|
||||
const { searchInput, searchQuery, searchQueryDebounced } = useSearchInput();
|
||||
const searchResults = useSearch(searchQueryDebounced, mappedIcons, [
|
||||
{ name: 'name', weight: 3 },
|
||||
@@ -88,6 +119,12 @@ function onFocusSearchInput() {
|
||||
}
|
||||
}
|
||||
|
||||
function onFocusVersionSelect() {
|
||||
if (releaseInfo.value == null) {
|
||||
fetchReleaseInfo();
|
||||
}
|
||||
}
|
||||
|
||||
const NoResults = defineAsyncComponent(() => import('./NoResults.vue'));
|
||||
|
||||
const IconDetailOverlay = defineAsyncComponent(() => import('./IconDetailOverlay.vue'));
|
||||
@@ -107,12 +144,17 @@ function handleCloseDrawer() {
|
||||
<div ref="overviewEl" class="overview-container">
|
||||
<StickyBar>
|
||||
<InputSearch
|
||||
:placeholder="`Search ${icons.length} icons ...`"
|
||||
:placeholder="`Search ${mappedIcons.length} icons ...`"
|
||||
v-model="searchQuery"
|
||||
ref="searchInput"
|
||||
class="input-wrapper"
|
||||
@focus="onFocusSearchInput"
|
||||
/>
|
||||
<VersionSelect
|
||||
v-model="selectedVersion"
|
||||
:versions="versions"
|
||||
@focus="onFocusVersionSelect"
|
||||
/>
|
||||
</StickyBar>
|
||||
<NoResults
|
||||
v-if="list.length === 0 && searchQuery !== ''"
|
||||
@@ -121,7 +163,6 @@ function handleCloseDrawer() {
|
||||
/>
|
||||
<IconGrid
|
||||
v-else-if="list.length === 0"
|
||||
:key="index"
|
||||
overlayMode
|
||||
:icons="[...searchResults].splice(0, DEFAULT_GRID_ITEMS)"
|
||||
:activeIcon="activeIconName"
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
margin-top: -32px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 32px;
|
||||
background: var(--vp-c-bg);
|
||||
box-shadow: 0 16px 24px var(--vp-c-bg);
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxInput,
|
||||
ComboboxButton,
|
||||
ComboboxOptions,
|
||||
ComboboxOption,
|
||||
} from '@headlessui/vue'
|
||||
|
||||
import { chevronsUpDown, check } from '../../../data/iconNodes'
|
||||
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon'
|
||||
|
||||
const model = defineModel<string>()
|
||||
const props = defineProps<{
|
||||
versions: string[]
|
||||
}>()
|
||||
|
||||
let query = ref('')
|
||||
|
||||
let filteredVersions = computed(() =>
|
||||
query.value === ''
|
||||
? ['Latest version', ...props.versions]
|
||||
: props.versions.filter((version) =>
|
||||
version
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, '')
|
||||
.includes(query.value.toLowerCase().replace(/\s+/g, ''))
|
||||
)
|
||||
)
|
||||
|
||||
const emit = defineEmits(['focus'])
|
||||
|
||||
const ChevronUpDown = createLucideIcon('ChevronUpDown', chevronsUpDown)
|
||||
const Check = createLucideIcon('Check', check)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Combobox v-model="model">
|
||||
<div class="combobox-container">
|
||||
<div class="combobox-input-container">
|
||||
<ComboboxInput
|
||||
class="combobox-input"
|
||||
placeholder="Latest version"
|
||||
@change="query = $event.target.value"
|
||||
@focus="emit('focus')"
|
||||
/>
|
||||
<ComboboxButton class="combobox-button">
|
||||
<ChevronUpDown class="icon-chevron" aria-hidden="true" />
|
||||
</ComboboxButton>
|
||||
</div>
|
||||
|
||||
|
||||
<ComboboxOptions class="combobox-options">
|
||||
<div
|
||||
v-if="versions.length === 0 && query !== ''"
|
||||
class="combobox-no-results"
|
||||
>
|
||||
No match!
|
||||
</div>
|
||||
|
||||
<ComboboxOption
|
||||
v-for="version in filteredVersions"
|
||||
as="template"
|
||||
:key="version"
|
||||
:value="version"
|
||||
v-slot="{ selected, active }"
|
||||
>
|
||||
<li
|
||||
class="combobox-option"
|
||||
:class="{ 'active': active }"
|
||||
>
|
||||
<span
|
||||
class="combobox-option-text"
|
||||
:class="{ 'selected': selected }"
|
||||
>
|
||||
{{ version }}
|
||||
</span>
|
||||
<span
|
||||
v-if="selected"
|
||||
class="combobox-option-icon"
|
||||
:class="{ 'active': active }"
|
||||
>
|
||||
<Check class="icon-check" aria-hidden="true" />
|
||||
</span>
|
||||
</li>
|
||||
</ComboboxOption>
|
||||
</ComboboxOptions>
|
||||
</div>
|
||||
</Combobox>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.combobox-input-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
min-width: 160px;
|
||||
cursor: default;
|
||||
overflow: hidden;
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
text-align: left;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.combobox-input {
|
||||
width: 100%;
|
||||
border: none;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
color: var(--vp-c-text-1);
|
||||
border: 1px solid transparent;
|
||||
transition: border-color .15s ease-in-out;
|
||||
}
|
||||
|
||||
.combobox-input:hover, .combobox-input:focus {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-bg-alt);
|
||||
}
|
||||
|
||||
.combobox-button {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
padding: 9px 8px;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: 0.5rem;
|
||||
z-index: 10;;
|
||||
}
|
||||
|
||||
.icon-chevron {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.combobox-options {
|
||||
position: absolute;
|
||||
margin-top: 0.25rem;
|
||||
width: 100%;
|
||||
max-width: 160px;
|
||||
padding: 12px;
|
||||
min-width: 128px;
|
||||
overflow-y: auto;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background-color: var(--vp-c-bg-elv);
|
||||
box-shadow: var(--vp-shadow-3);
|
||||
transition: background-color 0.5s;
|
||||
max-height: 240px;
|
||||
outline: none;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.combobox-no-results {
|
||||
position: relative;
|
||||
cursor: default;
|
||||
padding: 0.5rem 1rem;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.combobox-option {
|
||||
position: relative;
|
||||
cursor: default;
|
||||
padding: 2px 8px;
|
||||
border-radius: 6px;
|
||||
color: var(--vp-c-text-1);
|
||||
line-height: 32px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
transition: background-color .25s,color .25s;
|
||||
}
|
||||
|
||||
.combobox-option.active {
|
||||
color: var(--vp-c-brand);
|
||||
background-color: var(--vp-c-default-soft);
|
||||
}
|
||||
|
||||
.combobox-option-text {
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.combobox-option-text.selected {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.combobox-option-icon {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 0.75rem;
|
||||
color: var(--vp-c-brand) !important;
|
||||
}
|
||||
|
||||
.combobox-option-icon.active {
|
||||
color: var(--vp-c-brand) !important;
|
||||
}
|
||||
|
||||
.icon-check {
|
||||
height:20px;
|
||||
width:20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,12 @@
|
||||
import { useFetch } from '@vueuse/core';
|
||||
|
||||
const useFetchTags = () =>
|
||||
useFetch<Record<string, string>>(
|
||||
`${import.meta.env.DEV ? 'http://localhost:3000' : ''}/api/release-info`,
|
||||
{
|
||||
immediate:
|
||||
typeof window !== 'undefined' && new URLSearchParams(window.location.search).has('version'),
|
||||
},
|
||||
).json();
|
||||
|
||||
export default useFetchTags;
|
||||
+1
-1
@@ -33,7 +33,7 @@
|
||||
"h3": "^1.8.0",
|
||||
"nitropack": "2.8.1",
|
||||
"rollup-plugin-copy": "^3.4.0",
|
||||
"vitepress": "^1.3.1"
|
||||
"vitepress": "^1.6.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@floating-ui/vue": "^1.0.3",
|
||||
|
||||
Generated
+338
-307
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user