Compare commits

..
6 Commits
Author SHA1 Message Date
Daniel Bayley 024f21e896 Add bot-message-square (chatbot) icon (#1709) 2024-02-28 16:35:25 +01:00
Jakob Guddas c30ccd472e fix(icons): update goal icon (#1924)
* Updated icons/goal.svg

* Updated icons/goal.json
2024-02-27 10:17:59 +01:00
Wojciech Maj 28ec03ebc8 Add support for react-native-svg ^15.0.0 (#1929) 2024-02-27 10:13:42 +01:00
Eric Fennis ab3a31367a feat(site): adds virtual scrolling to icons grid and categories (#1911)
* Add virtual scrolling icons page

* improve scrolling

* Small fixes in the routing

* Cleanup

* Fix routing issue

* Remove log

* Cleanup

* Cleanup

* Show search results in sidebar

* Improve alphabetical list

* Add whitespace

* Extract logic into a composable and fix icon selection

* Formatting
2024-02-27 10:09:52 +01:00
148bae88d8 feat(icons): added globe-lock icon (#1587)
* Added globe-lock icon

* feat(icons): Added globe-lock, renamed globe-2 to earth, and added earth-lock

* feat(icons): Added globe as a tag to earth

---------

Co-authored-by: Eric Fennis <eric.fennis@gmail.com>
Co-authored-by: Karsa <karsa@sztaki.hu>
2024-02-26 09:53:04 +01:00
Chaoying ca4a38ba85 Fix import statements (#1922) 2024-02-25 19:04:23 +01:00
28 changed files with 503 additions and 183 deletions
@@ -1,13 +1,15 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ref, computed, watch } from 'vue'
import { useData } from 'vitepress'
import VPLink from 'vitepress/dist/client/theme-default/components/VPLink.vue'
import { isActive } from 'vitepress/dist/client/shared'
import { useActiveAnchor } from '../../composables/useActiveAnchor'
import { data } from './CategoryList.data'
import CategoryListItem from './CategoryListItem.vue'
import { useCategoryView } from '../../composables/useCategoryView'
const { page } = useData()
const { categoryCounts } = useCategoryView();
const categoriesIsActive = computed(() => {
return isActive(page.value.relativePath, '/icons/categories');
@@ -25,14 +27,21 @@ const headers = computed(() => {
level: 2,
link: `${linkPrefix}#${name}`,
title,
iconCount
iconCount: categoryCounts.value[name] ?? iconCount,
name
}))
})
const container = ref()
const marker = ref()
useActiveAnchor(container, marker)
const { setActiveLinkDebounced } = useActiveAnchor(container, marker)
watch(headers, () => {
setTimeout(() => {
setActiveLinkDebounced()
}, 200)
})
</script>
<template>
@@ -7,6 +7,7 @@ interface Header {
slug: string;
iconCount: number;
link: string;
name: string;
children: Header[];
}
@@ -14,36 +15,35 @@ type MenuItem = Omit<Header, 'slug' | 'children'> & {
children?: MenuItem[];
};
const props = defineProps<{
defineProps<{
headers: MenuItem[];
root?: boolean;
}>();
const { selectedCategory } = useCategoryView();
function onClick(event: Event) {
const target =
(event.target as HTMLElement).nodeName === 'span'
? (event.target as HTMLElement).parentNode
: (event.target as HTMLElement);
const href = (target as HTMLAnchorElement)?.href;
function onClick(categoryName: string) {
selectedCategory.value = categoryName;
if (href) {
const id = '#' + href.split('#')[1];
const decodedId = decodeURIComponent(id);
const heading = document.querySelector<HTMLAnchorElement>(categoryName);
heading?.focus();
selectedCategory.value = decodedId.replace('#', '');
const heading = document.querySelector<HTMLAnchorElement>(decodedId);
heading?.focus();
}
window.history.pushState({}, '', `/icons/categories#${categoryName}`)
}
</script>
<template>
<ul :class="root ? 'root' : 'nested'">
<li v-for="{ children, link, title, iconCount } in headers">
<a class="outline-link" :href="link" @click="onClick" :title="title">
<li v-for="{ children, link, title, iconCount, name } in headers">
<a
class="outline-link"
:href="link"
@click="onClick(name)"
:title="title"
:class="{
inactive: iconCount === 0,
}"
>
<span>
{{ title }}
</span>
@@ -83,6 +83,10 @@ function onClick(event: Event) {
transition: color 0.25s;
}
.outline-link.inactive {
color: var(--vp-c-text-4);
}
.outline-link.nested {
padding-left: 13px;
}
@@ -93,4 +97,8 @@ function onClick(event: Event) {
font-size: 11px;
font-weight: 400;
}
.outline-link.inactive .icon-count {
opacity: 0;
}
</style>
@@ -4,7 +4,7 @@ import IconItem from './IconItem.vue'
const emit = defineEmits(['setActiveIcon'])
const props = defineProps<{
defineProps<{
icons: IconEntity[]
activeIcon?: string
overlayMode?: boolean
@@ -40,7 +40,6 @@ function setActiveIcon(name: string) {
.icons {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(56px, 1fr));
/* padding: 32px 32px 96px; */
gap: 8px;
width: 100%;
}
@@ -1,56 +1,53 @@
<script lang="ts">
import { IconEntity } from '../../types';
type CategoryNameRow = {
type: 'category';
title: string;
name: string;
};
type CategoryIconsRow = {
type: 'icons';
icons: IconEntity[];
};
export type CategoryRow = CategoryNameRow | CategoryIconsRow;
</script>
<script setup lang="ts">
import { ref } from 'vue';
import { Category } from '../../types';
import IconGrid from './IconGrid.vue'
import { vIntersectionObserver } from '@vueuse/components'
defineProps<{
activeIconName: string
category: Category
categoryRow: CategoryRow
}>()
const emit = defineEmits(['setActiveIcon'])
const showIcons = ref(false)
// Added intersection observer to improve performance
const onIntersectionObserver: IntersectionObserverCallback = ([{ isIntersecting }]) => {
showIcons.value = isIntersecting
}
</script>
<template>
<section
class="category"
:key="category.name"
:id="category.name"
v-intersection-observer="onIntersectionObserver"
<h2
v-if="categoryRow.type === 'category'"
class="title"
>
<h2 class="title" >
<a class="header-anchor" :href="`#${category.name}`" :aria-label="`Permalink to &quot;${category.title}&quot;`">&ZeroWidthSpace;</a>
{{ category.title }}
</h2>
<IconGrid
:activeIcon="activeIconName"
:icons="category.icons"
@setActiveIcon="$event => $emit('setActiveIcon', $event)"
overlayMode
:hideIcons="!showIcons"
/>
</section>
<a class="header-anchor" :href="`#${categoryRow.name}`" :aria-label="`Permalink to &quot;${categoryRow.title}&quot;`">&ZeroWidthSpace;</a>
{{ categoryRow.title }}
</h2>
<IconGrid
v-else-if="categoryRow.type === 'icons'"
:activeIcon="activeIconName"
:icons="categoryRow.icons"
@setActiveIcon="$event => $emit('setActiveIcon', $event)"
overlayMode
/>
</template>
<style scoped>
.title {
margin-bottom: 24px;
margin-bottom: 8px;
font-size: 19px;
font-weight: 500;
padding-top: 86px;
/* scroll-padding-top: 240px; */
}
.category {
margin-bottom: calc(-86px + 32px);
padding: 24px 0 8px;
}
</style>
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, computed, defineAsyncComponent } from 'vue';
import { ref, computed, defineAsyncComponent, onMounted } from 'vue';
import type { IconEntity, Category } from '../../types';
import useSearch from '../../composables/useSearch';
import InputSearch from '../base/InputSearch.vue';
@@ -8,6 +8,13 @@ import StickyBar from './StickyBar.vue';
import IconsCategory from './IconsCategory.vue';
import useFetchTags from '../../composables/useFetchTags';
import useFetchCategories from '../../composables/useFetchCategories';
import { useElementSize, useEventListener, useVirtualList } from '@vueuse/core';
import chunkArray from '../../utils/chunkArray';
import { CategoryRow } from './IconsCategory.vue';
import useScrollToCategory from '../../composables/useScrollToCategory';
const ICON_SIZE = 56;
const ICON_GRID_GAP = 8;
const props = defineProps<{
icons: IconEntity[];
@@ -17,7 +24,6 @@ const props = defineProps<{
const activeIconName = ref(null);
const { searchInput, searchQuery, searchQueryDebounced } = useSearchInput();
const isSearching = computed(() => !!searchQuery.value);
function setActiveIconName(name: string) {
@@ -27,6 +33,13 @@ function setActiveIconName(name: string) {
const { execute: fetchTags, data: tags } = useFetchTags();
const { execute: fetchCategories, data: categoriesMap } = useFetchCategories();
const overviewEl = ref<HTMLElement | null>(null);
const { width: containerWidth } = useElementSize(overviewEl)
const columnSize = computed(() => {
return Math.floor((containerWidth.value) / ((ICON_SIZE + ICON_GRID_GAP)));
});
const mappedIcons = computed(() => {
if (tags.value == null) {
return props.icons;
@@ -43,9 +56,10 @@ const mappedIcons = computed(() => {
});
});
const searchResults = useSearch(searchQuery, mappedIcons, [
{ name: 'name', weight: 2 },
{ name: 'tags', weight: 1 },
const searchResults = useSearch(searchQueryDebounced, mappedIcons, [
{ name: 'name', weight: 3 },
{ name: 'aliases', weight: 3 },
{ name: 'tags', weight: 2 },
]);
const categories = computed(() => {
@@ -71,9 +85,43 @@ const categories = computed(() => {
icons: searchedCategoryIcons,
};
})
.filter(({ icons }) => icons.length);
});
const categoriesList = computed(() => {
return categories.value
.filter(({ icons }) => icons.length)
.reduce<CategoryRow[]>((acc, category) => {
acc.push({ type: 'category', title: category.title, name: category.name });
const categoryIcons = chunkArray(category.icons, columnSize.value);
categoryIcons.forEach((icons) => {
acc.push({ type: 'icons', icons });
});
return acc;
}, []);
});
const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(
categoriesList,
{
itemHeight: ICON_SIZE + ICON_GRID_GAP,
overscan: 10
},
)
useScrollToCategory({
categories,
categoriesList,
scrollTo,
searchQueryDebounced,
})
onMounted(() => {
containerProps.ref.value = document.documentElement;
useEventListener(window, 'scroll', containerProps.onScroll)
})
function onFocusSearchInput() {
if (tags.value == null) {
fetchTags();
@@ -84,28 +132,35 @@ function onFocusSearchInput() {
}
const NoResults = defineAsyncComponent(() => import('./NoResults.vue'));
const IconDetailOverlay = defineAsyncComponent(() => import('./IconDetailOverlay.vue'));
</script>
<template>
<StickyBar class="search-bar category-search">
<InputSearch
:placeholder="`Search ${icons.length} icons ...`"
v-model="searchQuery"
class="input-wrapper"
ref="searchInput"
@focus="onFocusSearchInput"
<div ref="overviewEl" class="overview-container">
<StickyBar class="category-search">
<InputSearch
:placeholder="`Search ${icons.length} icons ...`"
v-model="searchQuery"
class="input-wrapper"
ref="searchInput"
@focus="onFocusSearchInput"
/>
</StickyBar>
<NoResults
v-if="categories.length === 0"
:searchQuery="searchQuery"
@clear="searchQuery = ''"
/>
</StickyBar>
<NoResults v-if="categories.length === 0" :searchQuery="searchQuery" @clear="searchQuery = ''" />
<IconsCategory
v-for="category in categories"
:key="category.name"
:category="category"
:activeIconName="activeIconName"
@setActiveIcon="setActiveIconName"
/>
<div v-bind="wrapperProps">
<IconsCategory
v-for="{ index, data } in list"
:categoryRow="data"
:activeIconName="activeIconName"
@setActiveIcon="setActiveIconName"
:key="index"
/>
</div>
</div>
<IconDetailOverlay
v-if="activeIconName != null"
:iconName="activeIconName"
@@ -119,6 +174,21 @@ const IconDetailOverlay = defineAsyncComponent(() => import('./IconDetailOverlay
}
.search-bar.category-search {
margin-bottom: -54px;
margin-bottom: 10px;
}
.title {
margin-bottom: 8px;
font-size: 19px;
font-weight: 500;
padding: 24px 0 8px;
}
.icons {
margin-bottom: 8px;
}
.overview-container {
padding-bottom: 288px;
}
</style>
@@ -1,15 +1,18 @@
<script setup lang="ts">
import { ref, computed, watch, defineAsyncComponent } from 'vue';
import { ref, computed, defineAsyncComponent, onMounted, watch } from 'vue';
import type { IconEntity } from '../../types';
import { useMediaQuery, useOffsetPagination } from '@vueuse/core';
import { useElementSize, useEventListener, useVirtualList } from '@vueuse/core';
import IconGrid from './IconGrid.vue';
import InputSearch from '../base/InputSearch.vue';
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';
import chunkArray from '../../utils/chunkArray';
const ICON_SIZE = 56;
const ICON_GRID_GAP = 8;
const props = defineProps<{
icons: IconEntity[];
@@ -17,32 +20,16 @@ const props = defineProps<{
const activeIconName = ref(null);
const isExtraLargeScreen = useMediaQuery('(min-width: 1440px)');
const isLargeScreen = useMediaQuery('(min-width: 1280px)');
const isMediumScreen = useMediaQuery('(min-width: 960px)');
const isSmallScreen = useMediaQuery('(min-width: 640px)');
const pageSize = computed(() => {
if (isExtraLargeScreen.value) {
return 16 * 20;
}
if (isLargeScreen.value) {
return 16 * 12;
}
if (isMediumScreen.value) {
return 13 * 12;
}
if (isSmallScreen.value) {
return 10 * 10;
}
return 10 * 5;
});
const { execute: fetchTags, data: tags } = useFetchTags();
const { execute: fetchCategories, data: categories } = useFetchCategories();
const overviewEl = ref<HTMLElement | null>(null);
const { width: containerWidth } = useElementSize(overviewEl)
const columnSize = computed(() => {
return Math.floor((containerWidth.value) / ((ICON_SIZE + ICON_GRID_GAP)));
});
const mappedIcons = computed(() => {
if (tags.value == null) {
return props.icons;
@@ -63,26 +50,33 @@ const mappedIcons = computed(() => {
const { searchInput, searchQuery, searchQueryDebounced } = useSearchInput();
const searchResults = useSearch(searchQueryDebounced, mappedIcons, [
{ name: 'name', weight: 3 },
{ name: 'aliases', weight: 3 },
{ name: 'tags', weight: 2 },
{ name: 'categories', weight: 1 },
]);
const { next, currentPage } = useOffsetPagination({ pageSize });
const paginatedIcons = computed(() => {
const end = pageSize.value * currentPage.value;
return searchResults.value.slice(0, end);
const chunkedIcons = computed(() => {
return chunkArray(searchResults.value, columnSize.value);
});
const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(
chunkedIcons,
{
itemHeight: ICON_SIZE + ICON_GRID_GAP,
overscan: 10
},
)
onMounted(() => {
containerProps.ref.value = document.documentElement;
useEventListener(window, 'scroll', containerProps.onScroll)
})
function setActiveIconName(name: string) {
activeIconName.value = name;
}
watch(searchQueryDebounced, (searchString) => {
currentPage.value = 1;
});
function onFocusSearchInput() {
if (tags.value == null) {
fetchTags();
@@ -95,30 +89,40 @@ function onFocusSearchInput() {
const NoResults = defineAsyncComponent(() => import('./NoResults.vue'));
const IconDetailOverlay = defineAsyncComponent(() => import('./IconDetailOverlay.vue'));
watch(searchQueryDebounced, () => {
scrollTo(0)
})
</script>
<template>
<StickyBar>
<InputSearch
:placeholder="`Search ${icons.length} icons ...`"
v-model="searchQuery"
ref="searchInput"
class="input-wrapper"
@focus="onFocusSearchInput"
<div ref="overviewEl" class="overview-container">
<StickyBar>
<InputSearch
:placeholder="`Search ${icons.length} icons ...`"
v-model="searchQuery"
ref="searchInput"
class="input-wrapper"
@focus="onFocusSearchInput"
/>
</StickyBar>
<NoResults
v-if="list.length === 0"
:searchQuery="searchQuery"
@clear="searchQuery = ''"
/>
</StickyBar>
<NoResults
v-if="paginatedIcons.length === 0"
:searchQuery="searchQuery"
@clear="searchQuery = ''"
/>
<IconGrid
overlayMode
:activeIcon="activeIconName"
:icons="paginatedIcons"
@setActiveIcon="setActiveIconName"
/>
<EndOfPage @end-of-page="next" class="bottom-page" />
<div v-bind="wrapperProps" class="icon">
<IconGrid
v-for="{ index, data: icons } in list"
:key="index"
overlayMode
:icons="icons"
:activeIcon="activeIconName"
@setActiveIcon="setActiveIconName"
/>
</div>
</div>
<IconDetailOverlay
v-if="activeIconName != null"
:iconName="activeIconName"
@@ -128,10 +132,7 @@ const IconDetailOverlay = defineAsyncComponent(() => import('./IconDetailOverlay
<style>
.icons {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(56px, 1fr));
gap: 8px;
width: 100%;
margin-bottom: 8px;
}
.icon {
@@ -142,7 +143,7 @@ const IconDetailOverlay = defineAsyncComponent(() => import('./IconDetailOverlay
width: 100%;
}
.bottom-page {
height: 288px;
.overview-container {
padding-bottom: 288px;
}
</style>
@@ -1,9 +1,10 @@
<script setup lang="ts">
import {ref} from 'vue'
import {bird} from '../../../data/iconNodes'
import { ref, onMounted, computed } from 'vue'
import { bird, squirrel, rabbit } from '../../../data/iconNodes'
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon'
import {useEventListener} from '@vueuse/core'
import VPButton from 'vitepress/dist/client/theme-default/components/VPButton.vue'
import { IconNode } from '../../types'
defineProps<{
searchQuery: string
@@ -11,32 +12,48 @@ defineProps<{
defineEmits(['clear'])
const birdIcon = ref<HTMLElement>()
const Bird = createLucideIcon('bird', bird)
const animalIcon = ref<HTMLElement>()
const randomAnimal = computed<IconNode>(() => {
return Math.random() > 0.5 ? squirrel : Math.random() > 0.5 ? rabbit : bird
})
const animalComponent = computed(() => createLucideIcon('animal', randomAnimal.value))
const flip = ref(false)
useEventListener(document, 'mousemove', (mouseEvent) => {
const {width, height, x, y} = birdIcon.value.getBoundingClientRect()
onMounted(() => {
useEventListener(document, 'mousemove', (mouseEvent) => {
const {width, height, x, y} = animalIcon.value.getBoundingClientRect()
const centerX = (width / 2) + x
const centerX = (width / 2) + x
flip.value = mouseEvent.x < centerX
flip.value = mouseEvent.x < centerX
})
})
</script>
<template>
<div class="no-results">
<Bird class="bird-icon" ref="birdIcon" :class="{ flip }" :strokeWidth="1"/>
<component
:is="animalComponent"
class="animal-icon"
ref="animalIcon"
:class="{ flip }"
:strokeWidth="1"
/>
<h2 class="no-results-text">
No icons found for '{{ searchQuery }}'
</h2>
<VPButton text="Clear your search and try again" theme="alt" @click="$emit('clear')"/>
<VPButton
text="Clear your search and try again"
theme="alt"
@click="$emit('clear')"
/>
<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"
<VPButton
text="Search on Github issues"
theme="alt"
:href="`https://github.com/lucide-icons/lucide/issues?q=is%3Aopen+${searchQuery}`"
target="_blank"
/>
</div>
</template>
@@ -48,7 +65,7 @@ useEventListener(document, 'mousemove', (mouseEvent) => {
align-items: center;
}
.bird-icon {
.animal-icon {
width: 160px;
height: 160px;
color: var(--vp-c-neutral);
@@ -56,12 +73,12 @@ useEventListener(document, 'mousemove', (mouseEvent) => {
margin-top: 72px;
}
.bird-icon.flip {
.animal-icon.flip {
transform: rotateY(180deg);
}
@media (min-width: 960px) {
.bird-icon {
.animal-icon {
width: 240px;
height: 240px;
}
@@ -6,18 +6,18 @@ import { throttleAndDebounce } from 'vitepress/dist/client/theme-default/support
*/
export function useActiveAnchor(container, marker) {
const onScroll = throttleAndDebounce(setActiveLink, 100);
const setActiveLinkDebounced = throttleAndDebounce(setActiveLink, 100);
let prevActiveLink = null;
onMounted(() => {
requestAnimationFrame(setActiveLink);
window.addEventListener('scroll', onScroll);
window.addEventListener('scroll', setActiveLinkDebounced);
});
onUpdated(() => {
// sidebar update means a route change
activateLink(location.hash);
});
onUnmounted(() => {
window.removeEventListener('scroll', onScroll);
window.removeEventListener('scroll', setActiveLinkDebounced);
});
function setActiveLink() {
const links = [].slice.call(container.value.querySelectorAll('.outline-link'));
@@ -64,9 +64,13 @@ export function useActiveAnchor(container, marker) {
marker.value.style.opacity = '0';
}
}
return {
setActiveLinkDebounced,
};
}
const PAGE_OFFSET = 64;
const PAGE_OFFSET = 128;
function getAnchorTop(anchor) {
return anchor.parentElement.offsetTop - PAGE_OFFSET;
@@ -74,7 +78,7 @@ function getAnchorTop(anchor) {
function isAnchorActive(index, anchor, nextAnchor) {
const scrollTop = window.scrollY;
if (index === 0 && scrollTop === 0) {
return [true, null];
return [true, anchor.hash];
}
if (scrollTop < getAnchorTop(anchor)) {
return [false, null];
@@ -1,4 +1,5 @@
import { ref, inject, Ref } from 'vue';
import { useRoute } from 'vitepress';
import { ref, inject, Ref, onMounted, watch } from 'vue';
export const CATEGORY_VIEW_CONTEXT = Symbol('categoryView');
@@ -8,16 +9,30 @@ interface CategoryViewContext {
}
export const categoryViewContext = {
selectedCategory: ref(''),
selectedCategory: ref(),
categoryCounts: ref({}),
};
export function useCategoryView(): CategoryViewContext {
const context = inject<CategoryViewContext>(CATEGORY_VIEW_CONTEXT);
const route = useRoute();
if (!context) {
throw new Error('useCategoryView must be used with categoryView context');
}
onMounted(() => {
if (window.location.hash) {
context.selectedCategory.value = decodeURIComponent(window.location.hash.slice(1));
}
});
watch(route, (currentRoute) => {
if (currentRoute.path !== '/icons/categories') {
context.selectedCategory.value = '';
context.categoryCounts.value = {};
}
});
return context;
}
@@ -0,0 +1,52 @@
import { Ref, onMounted, watch } from 'vue';
import { CategoryRow } from '../components/icons/IconsCategory.vue';
import { Category } from '../types';
import { useCategoryView } from './useCategoryView';
interface UseScrollToCategory {
categories: Ref<Pick<Category, 'name' | 'icons'>[]>;
categoriesList: Ref<CategoryRow[]>;
scrollTo: (index: number) => void;
searchQueryDebounced: Ref<string>;
}
export default function useScrollToCategory({
categories,
categoriesList,
scrollTo,
searchQueryDebounced,
}: UseScrollToCategory) {
const { selectedCategory, categoryCounts } = useCategoryView();
function scrollToSelectedCategory(selectedCategory: string) {
const category = categories.value.find((category) => category.name === selectedCategory);
if (category != null) {
const categoryRowIndex = categoriesList.value.findIndex(
(row) => row.type === 'category' && row.name === selectedCategory,
);
if (categoryRowIndex !== -1) {
setTimeout(() => {
scrollTo(categoryRowIndex);
}, 0);
}
}
}
watch(selectedCategory, scrollToSelectedCategory);
onMounted(() => {
setTimeout(() => {
scrollToSelectedCategory(selectedCategory.value);
}, 0);
});
watch(searchQueryDebounced, () => {
scrollTo(0);
});
watch(categories, (items) => {
categoryCounts.value = Object.fromEntries(items.map(({ name, icons }) => [name, icons.length]));
});
}
+11 -1
View File
@@ -4,7 +4,7 @@ import { shallowRef, computed, Ref } from 'vue';
const useSearch = <T>(
query: Ref<string>,
collection: Ref<T[]>,
keys: Fuse.FuseOptionKey<T>[] = [],
keys: Fuse.FuseOptionKeyObject<T>[] = [],
) => {
const index = shallowRef(
new Fuse(collection.value, {
@@ -20,6 +20,16 @@ const useSearch = <T>(
return index.value.search(query.value).map((result) => result.item);
}
if (keys.length !== 0) {
const mainKey = keys[0].name;
return collection.value.sort((a, b) => {
const aString = a[mainKey as keyof T] as string;
const bString = b[mainKey as keyof T] as string;
return aString.localeCompare(bString);
});
}
return collection.value;
});
+4
View File
@@ -23,6 +23,8 @@
--vp-code-editor-property: #005cc5;
--vp-code-editor-static: #f78c6c;
--vp-code-editor-string: #032f62;
--vp-c-text-4: rgba(60, 60, 67, 0.32);
}
.dark {
@@ -38,6 +40,8 @@
--vp-code-editor-property: #79b8ff;
--vp-code-editor-static: #f78c6c;
--vp-code-editor-string: #9ecbff;
--vp-c-text-4: rgba(235, 235, 245, 0.16);
}
.VPNavBarTitle .logo {
@@ -0,0 +1,9 @@
const chunkArray = <ArrayType>(stream: ArrayType, size: number) => {
return stream.reduce<ArrayType[][]>(
(chunks, item, idx, arr) =>
idx % size == 0 ? [...chunks, arr.slice(idx, idx + size)] : chunks,
[],
);
};
export default chunkArray;
+1 -1
View File
@@ -4,7 +4,7 @@ Implementation of the lucide icon library for React Native applications
## Installation
First, ensure that you have `react-native-svg` (version between 12 and 14) installed. Then, install the package:
First, ensure that you have `react-native-svg` (version between 12 and 15) installed. Then, install the package:
::: code-group
+16
View File
@@ -0,0 +1,16 @@
{
"$schema": "../icon.schema.json",
"contributors": [
"danielbayley"
],
"tags": [
"robot",
"ai",
"chat",
"assistant"
],
"categories": [
"development",
"social"
]
}
+18
View File
@@ -0,0 +1,18 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 6V2H8" />
<path d="m8 18-4 4V8a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2Z" />
<path d="M2 12h2" />
<path d="M9 11v2" />
<path d="M15 11v2" />
<path d="M20 12h2" />
</svg>

After

Width:  |  Height:  |  Size: 402 B

+25
View File
@@ -0,0 +1,25 @@
{
"$schema": "../icon.schema.json",
"contributors": [
"mittalyashu",
"ericfennis",
"karsa-mistmere"
],
"tags": [
"vpn",
"private",
"privacy",
"network",
"world",
"browser",
"security",
"encryption",
"protection",
"connection"
],
"categories": [
"security",
"development",
"devices"
]
}
+18
View File
@@ -0,0 +1,18 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M7 3.34V5a3 3 0 0 0 3 3" />
<path d="M11 21.95V18a2 2 0 0 0-2-2 2 2 0 0 1-2-2v-1a2 2 0 0 0-2-2H2.05" />
<path d="M21.54 15H17a2 2 0 0 0-2 2v4.54" />
<path d="M12 2a10 10 0 1 0 9.54 13" />
<path d="M20 6V4a2 2 0 1 0-4 0v2" />
<rect width="8" height="5" x="14" y="6" rx="1" />
</svg>

After

Width:  |  Height:  |  Size: 504 B

+5 -1
View File
@@ -9,10 +9,14 @@
"world",
"browser",
"language",
"translate"
"translate",
"globe"
],
"categories": [
"maps",
"navigation"
],
"aliases": [
"globe-2"
]
}

Before

Width:  |  Height:  |  Size: 479 B

After

Width:  |  Height:  |  Size: 479 B

+26
View File
@@ -0,0 +1,26 @@
{
"$schema": "../icon.schema.json",
"contributors": [
"colebemis",
"csandman",
"ericfennis",
"karsa-mistmere"
],
"tags": [
"vpn",
"private",
"privacy",
"network",
"world",
"browser",
"security",
"encryption",
"protection",
"connection"
],
"categories": [
"security",
"development",
"devices"
]
}
+16
View File
@@ -0,0 +1,16 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M15.686 15A14.5 14.5 0 0 1 12 22a14.5 14.5 0 0 1 0-20 10 10 0 1 0 9.542 13" />
<path d="M2 12h8.5" />
<path d="M20 6V4a2 2 0 1 0-4 0v2" />
<rect width="8" height="5" x="14" y="6" rx="1" />
</svg>

After

Width:  |  Height:  |  Size: 414 B

+2 -1
View File
@@ -1,7 +1,8 @@
{
"$schema": "../icon.schema.json",
"contributors": [
"guillermo-angeles"
"guillermo-angeles",
"jguddas"
],
"tags": [
"flag",
+3 -3
View File
@@ -10,6 +10,6 @@
stroke-linejoin="round"
>
<path d="M12 13V2l8 4-8 4" />
<path d="M20.55 10.23A9 9 0 1 1 8 4.94" />
<path d="M8 10a5 5 0 1 0 8.9 2.02" />
</svg>
<path d="M20.561 10.222a9 9 0 1 1-12.55-5.29" />
<path d="M8.002 9.997a5 5 0 1 0 8.9 2.02" />
</svg>

Before

Width:  |  Height:  |  Size: 324 B

After

Width:  |  Height:  |  Size: 338 B

+1 -1
View File
@@ -6,7 +6,7 @@ Implementation of the lucide icon library for React Native applications.
## Installation
First, ensure that you have `react-native-svg` (version between 12 and 14) installed. Then, install the package:
First, ensure that you have `react-native-svg` (version between 12 and 15) installed. Then, install the package:
```sh
yarn add lucide-react-native
+2 -2
View File
@@ -54,7 +54,7 @@
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-native": "^0.73.1",
"react-native-svg": "^14.1.0",
"react-native-svg": "^15.0.0",
"rollup": "^4.9.2",
"rollup-plugin-dts": "^6.1.0",
"typescript": "^4.8.4",
@@ -64,6 +64,6 @@
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0",
"react-native": "*",
"react-native-svg": "^12.0.0 || ^13.0.0 || ^14.0.0"
"react-native-svg": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0"
}
}
+2 -1
View File
@@ -83,7 +83,8 @@ Example with React suspense:
```tsx
import React, { lazy, Suspense } from 'react';
import { dynamicIconImports, LucideProps } from 'lucide-react';
import { LucideProps } from 'lucide-react';
import dynamicIconImports from 'lucide-react/dynamicIconImports';
const fallback = <div style={{ background: '#ddd', width: 24, height: 24 }}/>
+4 -4
View File
@@ -476,8 +476,8 @@ importers:
specifier: ^0.73.1
version: 0.73.1(@babel/core@7.23.7)(@babel/preset-env@7.22.9)(react@18.2.0)
react-native-svg:
specifier: ^14.1.0
version: 14.1.0(react-native@0.73.1)(react@18.2.0)
specifier: ^15.0.0
version: 15.0.0(react-native@0.73.1)(react@18.2.0)
rollup:
specifier: ^4.9.2
version: 4.9.2
@@ -18488,8 +18488,8 @@ packages:
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
dev: true
/react-native-svg@14.1.0(react-native@0.73.1)(react@18.2.0):
resolution: {integrity: sha512-HeseElmEk+AXGwFZl3h56s0LtYD9HyGdrpg8yd9QM26X+d7kjETrRQ9vCjtxuT5dCZEIQ5uggU1dQhzasnsCWA==}
/react-native-svg@15.0.0(react-native@0.73.1)(react@18.2.0):
resolution: {integrity: sha512-ZUEXlzdU3cHjhOuc4BP7fbvabmz8yIuH4ocKSEr5V3P5skk2wnbEyZd3p7dzV9IoODgguCe7tcrNRGwr9pLRig==}
peerDependencies:
react: '*'
react-native: '*'