mirror of
https://github.com/lucide-icons/lucide.git
synced 2026-11-10 13:18:25 +00:00
refactor(docs): virtualize icon overviews with @tanstack/vue-virtual
Replace @vueuse/core's useVirtualList, which assumed a fixed 64px row height, with useWindowVirtualizer from @tanstack/vue-virtual in the icons and categories overviews. - Rows are measured after render, so stretched grid cells and category headings no longer make the content jump while scrolling. - Scroll against the window with the list's page offset as scroll margin, instead of forwarding window scroll events by hand. - Match the column count to the CSS auto-fill grid. - Scroll to a category below the sticky search bar. Shared logic lives in composables/useIconGridVirtualizer.ts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G9u8FiKAWTfN2uj8sqzqSi
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, defineAsyncComponent, onMounted } from 'vue';
|
||||
import { ref, computed, watch, defineAsyncComponent } from 'vue';
|
||||
import type { IconEntity, Category } from '../../types';
|
||||
import useSearch from '../../composables/useSearch';
|
||||
import InputSearch from '../base/InputSearch.vue';
|
||||
@@ -9,15 +9,17 @@ import StickyBar from './StickyBar.vue';
|
||||
import IconsCategory, { CategoryRow } 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 useScrollToCategory from '../../composables/useScrollToCategory';
|
||||
import useIconGridVirtualizer, {
|
||||
useIconGridColumns,
|
||||
} from '../../composables/useIconGridVirtualizer';
|
||||
import { useCategoryView } from '../../composables/useCategoryView';
|
||||
import CarbonAdOverlay from './CarbonAdOverlay.vue';
|
||||
import useSearchPlaceholder from '../../utils/useSearchPlaceholder.ts';
|
||||
|
||||
const ICON_SIZE = 56;
|
||||
const ICON_GRID_GAP = 8;
|
||||
/** Estimated height of a category heading row. Rows are measured once rendered. */
|
||||
const CATEGORY_HEADING_HEIGHT = 64;
|
||||
|
||||
const props = defineProps<{
|
||||
icons: IconEntity[];
|
||||
@@ -52,11 +54,10 @@ const {
|
||||
} = 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 listEl = ref<HTMLElement | null>(null);
|
||||
const stickyBar = ref<InstanceType<typeof StickyBar> | null>(null);
|
||||
const gridColumns = useIconGridColumns(overviewEl);
|
||||
const { columnSize } = gridColumns;
|
||||
|
||||
const mappedIcons = computed(() => {
|
||||
if (tags.value == null) {
|
||||
@@ -125,23 +126,25 @@ const isSearchMetadataLoading = computed(
|
||||
() => searchQuery.value.length > 0 && !isFetchingTags && !isFetchingCategories,
|
||||
);
|
||||
|
||||
const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(categoriesList, {
|
||||
itemHeight: ICON_SIZE + ICON_GRID_GAP,
|
||||
overscan: 10,
|
||||
});
|
||||
const { virtualRows, totalSize, offsetTop, measureElement, scrollToIndex, scrollToTop } =
|
||||
useIconGridVirtualizer({
|
||||
columns: gridColumns,
|
||||
listEl,
|
||||
stickyEl: stickyBar,
|
||||
rows: categoriesList,
|
||||
estimateRowSize: (row, iconRowHeight) =>
|
||||
row.type === 'category' ? CATEGORY_HEADING_HEIGHT : iconRowHeight,
|
||||
getRowKey: (row, index) => (row.type === 'category' ? `category-${row.name}` : index),
|
||||
});
|
||||
|
||||
useScrollToCategory({
|
||||
categories,
|
||||
categoriesList,
|
||||
scrollTo,
|
||||
scrollToIndex,
|
||||
scrollToTop,
|
||||
searchQueryDebounced,
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
containerProps.ref.value = document.documentElement;
|
||||
useEventListener(window, 'scroll', containerProps.onScroll);
|
||||
});
|
||||
|
||||
function loadSearchMetadata() {
|
||||
if (tags.value == null && !isFetchingTags.value) {
|
||||
void fetchTags();
|
||||
@@ -184,7 +187,10 @@ function handleCloseDrawer() {
|
||||
class="overview-container"
|
||||
:class="{ 'icon-drawer-open': activeIconName }"
|
||||
>
|
||||
<StickyBar class="category-search">
|
||||
<StickyBar
|
||||
ref="stickyBar"
|
||||
class="category-search"
|
||||
>
|
||||
<InputSearch
|
||||
:placeholder="`Search ${icons.length} icons…`"
|
||||
v-model="searchQuery"
|
||||
@@ -200,14 +206,29 @@ function handleCloseDrawer() {
|
||||
:isBrandSearch="searchPlaceholder.isBrand"
|
||||
@clear="searchQuery = ''"
|
||||
/>
|
||||
<div v-bind="wrapperProps">
|
||||
<IconsCategory
|
||||
v-for="{ index, data } in list"
|
||||
:categoryRow="data"
|
||||
:activeIconName="activeIconName"
|
||||
@setActiveIcon="setActiveIconName"
|
||||
:key="index"
|
||||
/>
|
||||
<div
|
||||
ref="listEl"
|
||||
class="virtual-list"
|
||||
:style="{ height: `${totalSize}px` }"
|
||||
>
|
||||
<div
|
||||
class="virtual-list-rows"
|
||||
:style="{ transform: `translateY(${offsetTop}px)` }"
|
||||
>
|
||||
<div
|
||||
v-for="row in virtualRows"
|
||||
:key="String(row.key)"
|
||||
:ref="measureElement"
|
||||
:data-index="row.index"
|
||||
class="virtual-row"
|
||||
>
|
||||
<IconsCategory
|
||||
:categoryRow="categoriesList[row.index]"
|
||||
:activeIconName="activeIconName"
|
||||
@setActiveIcon="setActiveIconName"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<IconDetailOverlay
|
||||
@@ -220,6 +241,23 @@ function handleCloseDrawer() {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.virtual-list {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.virtual-list-rows {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Contain the row's margins, so the measured height includes them. */
|
||||
.virtual-row {
|
||||
display: flow-root;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, defineAsyncComponent, onMounted, watch } from 'vue';
|
||||
import type { IconEntity } from '../../types';
|
||||
import { useElementSize, useEventListener, useVirtualList } from '@vueuse/core';
|
||||
import { useRoute } from 'vitepress';
|
||||
import IconGrid from './IconGrid.vue';
|
||||
import Select from '../base/Select.vue';
|
||||
import InputSearch from '../base/InputSearch.vue';
|
||||
import useSearch from '../../composables/useSearch';
|
||||
import useIconGridVirtualizer, {
|
||||
ICON_GRID_GAP,
|
||||
ICON_SIZE,
|
||||
useIconGridColumns,
|
||||
} from '../../composables/useIconGridVirtualizer';
|
||||
import useSearchInput from '../../composables/useSearchInput';
|
||||
import useSearchShortcut from '../../utils/useSearchShortcut';
|
||||
import StickyBar from './StickyBar.vue';
|
||||
@@ -19,8 +23,6 @@ import Icon from '@lucide/vue/src/Icon';
|
||||
import { listSortDescending } from '~/.vitepress/data/iconNodes';
|
||||
import { track, trackDebounced } from '../../utils/openpanel';
|
||||
|
||||
const ICON_SIZE = 56;
|
||||
const ICON_GRID_GAP = 8;
|
||||
const SORTING = [
|
||||
{
|
||||
name: 'Popularity',
|
||||
@@ -60,11 +62,9 @@ const {
|
||||
} = 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 listEl = ref<HTMLElement | null>(null);
|
||||
const gridColumns = useIconGridColumns(overviewEl);
|
||||
const { containerWidth, columnSize } = gridColumns;
|
||||
|
||||
const sortedIcons = computed(() => {
|
||||
switch (selectedSort.value.value) {
|
||||
@@ -128,15 +128,13 @@ const chunkedIcons = computed(() => {
|
||||
return chunkArray(searchResults.value, columnSize.value);
|
||||
});
|
||||
|
||||
const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(chunkedIcons, {
|
||||
itemHeight: ICON_SIZE + ICON_GRID_GAP,
|
||||
overscan: 10,
|
||||
const { virtualRows, totalSize, offsetTop, measureElement, scrollToTop } = useIconGridVirtualizer({
|
||||
columns: gridColumns,
|
||||
listEl,
|
||||
rows: chunkedIcons,
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
containerProps.ref.value = document.documentElement;
|
||||
useEventListener(window, 'scroll', containerProps.onScroll);
|
||||
|
||||
// Check if we should focus the search input from URL parameter
|
||||
const route = useRoute();
|
||||
if (route.data?.relativePath && window.location.search.includes('focus')) {
|
||||
@@ -168,7 +166,7 @@ const NoResults = defineAsyncComponent(() => import('./NoResults.vue'));
|
||||
const IconDetailOverlay = defineAsyncComponent(() => import('./IconDetailOverlay.vue'));
|
||||
|
||||
watch(searchQueryDebounced, () => {
|
||||
scrollTo(0);
|
||||
scrollToTop();
|
||||
});
|
||||
|
||||
let hasStartedSearching = false;
|
||||
@@ -248,25 +246,37 @@ function handleCloseDrawer() {
|
||||
@clear="searchQuery = ''"
|
||||
/>
|
||||
<IconGrid
|
||||
v-else-if="list.length === 0"
|
||||
v-else-if="virtualRows.length === 0"
|
||||
overlayMode
|
||||
:icons="searchResults.slice(0, initialGridItems)"
|
||||
:activeIcon="activeIconName"
|
||||
@setActiveIcon="setActiveIconName"
|
||||
/>
|
||||
<div
|
||||
v-bind="wrapperProps"
|
||||
class="icon"
|
||||
v-else
|
||||
ref="listEl"
|
||||
class="virtual-list"
|
||||
:style="{ height: `${totalSize}px` }"
|
||||
>
|
||||
<IconGrid
|
||||
v-for="{ index, data: icons } in list"
|
||||
:key="index"
|
||||
overlayMode
|
||||
:icons="icons"
|
||||
:activeIcon="activeIconName"
|
||||
@setActiveIcon="setActiveIconName"
|
||||
/>
|
||||
<div
|
||||
class="virtual-list-rows"
|
||||
:style="{ transform: `translateY(${offsetTop}px)` }"
|
||||
>
|
||||
<div
|
||||
v-for="row in virtualRows"
|
||||
:key="String(row.key)"
|
||||
:ref="measureElement"
|
||||
:data-index="row.index"
|
||||
class="virtual-row"
|
||||
>
|
||||
<IconGrid
|
||||
overlayMode
|
||||
:icons="chunkedIcons[row.index]"
|
||||
:activeIcon="activeIconName"
|
||||
@setActiveIcon="setActiveIconName"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -287,6 +297,23 @@ function handleCloseDrawer() {
|
||||
aspect-ratio: 1/1;
|
||||
}
|
||||
|
||||
.virtual-list {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.virtual-list-rows {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Contain the row's bottom margin, so the measured height includes it. */
|
||||
.virtual-row {
|
||||
display: flow-root;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
width: 100%;
|
||||
/* view-transition-name: icons-search-box; */
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import { computed, onMounted, ref, watch, type ComponentPublicInstance, type Ref } from 'vue';
|
||||
import { useWindowVirtualizer } from '@tanstack/vue-virtual';
|
||||
import { useElementSize, useEventListener, useResizeObserver } from '@vueuse/core';
|
||||
|
||||
export const ICON_SIZE = 56;
|
||||
export const ICON_GRID_GAP = 8;
|
||||
|
||||
interface UseIconGridVirtualizerOptions<Row> {
|
||||
/** Grid columns of the overview, from `useIconGridColumns`. */
|
||||
columns: ReturnType<typeof useIconGridColumns>;
|
||||
/** Element the virtual rows are rendered in. Its offset in the page is the scroll margin. */
|
||||
listEl: Ref<HTMLElement | null>;
|
||||
/** Sticky element at the top of the list. Its height is kept clear when scrolling to a row. */
|
||||
stickyEl?: Ref<HTMLElement | ComponentPublicInstance | null>;
|
||||
/** Rows to virtualize, usually icons chunked by `columns.columnSize`. */
|
||||
rows: Ref<Row[]>;
|
||||
/** Estimated row height. Rows are measured once rendered, so this only needs to be close. */
|
||||
estimateRowSize?: (row: Row, iconRowHeight: number) => number;
|
||||
getRowKey?: (row: Row, index: number) => string | number;
|
||||
overscan?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of icon columns that matches the CSS grid used by `IconGrid`:
|
||||
* `grid-template-columns: repeat(auto-fill, minmax(ICON_SIZE, 1fr))` with `gap: ICON_GRID_GAP`.
|
||||
*/
|
||||
export function getColumnCount(width: number) {
|
||||
if (width <= 0) return 0;
|
||||
|
||||
return Math.max(1, Math.floor((width + ICON_GRID_GAP) / (ICON_SIZE + ICON_GRID_GAP)));
|
||||
}
|
||||
|
||||
export function useIconGridColumns(containerEl: Ref<HTMLElement | null>) {
|
||||
const { width: containerWidth } = useElementSize(containerEl);
|
||||
const columnSize = computed(() => getColumnCount(containerWidth.value));
|
||||
|
||||
/** Height of one row of icons: square cells that stretch to fill the row, plus the row gap. */
|
||||
const iconRowHeight = computed(() => {
|
||||
const columns = columnSize.value;
|
||||
if (!columns) return ICON_SIZE + ICON_GRID_GAP;
|
||||
|
||||
const cellSize = (containerWidth.value - ICON_GRID_GAP * (columns - 1)) / columns;
|
||||
return cellSize + ICON_GRID_GAP;
|
||||
});
|
||||
|
||||
return { containerWidth, columnSize, iconRowHeight };
|
||||
}
|
||||
|
||||
function resolveElement(target: HTMLElement | ComponentPublicInstance | null | undefined) {
|
||||
if (target == null) return null;
|
||||
if (target instanceof HTMLElement) return target;
|
||||
|
||||
return (target.$el as HTMLElement | null) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Virtualizes rows of icons against the window scroll, using `@tanstack/vue-virtual`.
|
||||
* Rows are measured after rendering, so rows of different heights (like category headings)
|
||||
* are positioned correctly.
|
||||
*/
|
||||
export default function useIconGridVirtualizer<Row>({
|
||||
columns,
|
||||
listEl,
|
||||
stickyEl,
|
||||
rows,
|
||||
estimateRowSize,
|
||||
getRowKey,
|
||||
overscan = 5,
|
||||
}: UseIconGridVirtualizerOptions<Row>) {
|
||||
const { containerWidth, iconRowHeight } = columns;
|
||||
|
||||
const scrollMargin = ref(0);
|
||||
const scrollPaddingStart = ref(0);
|
||||
|
||||
function updateLayoutMetrics() {
|
||||
const list = listEl.value;
|
||||
if (list) {
|
||||
scrollMargin.value = Math.round(list.getBoundingClientRect().top + window.scrollY);
|
||||
}
|
||||
|
||||
const sticky = resolveElement(stickyEl?.value);
|
||||
if (sticky) {
|
||||
const stickyTop = parseFloat(getComputedStyle(sticky).top) || 0;
|
||||
scrollPaddingStart.value = Math.round(stickyTop + sticky.offsetHeight);
|
||||
}
|
||||
}
|
||||
|
||||
const virtualizer = useWindowVirtualizer<HTMLElement>(
|
||||
computed(() => {
|
||||
const rowList = rows.value;
|
||||
const rowHeight = iconRowHeight.value;
|
||||
|
||||
return {
|
||||
count: rowList.length,
|
||||
estimateSize: (index: number) =>
|
||||
estimateRowSize ? estimateRowSize(rowList[index], rowHeight) : rowHeight,
|
||||
getItemKey: getRowKey ? (index: number) => getRowKey(rowList[index], index) : undefined,
|
||||
overscan,
|
||||
scrollMargin: scrollMargin.value,
|
||||
scrollPaddingStart: scrollPaddingStart.value,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const virtualRows = computed(() => virtualizer.value.getVirtualItems());
|
||||
const totalSize = computed(() => virtualizer.value.getTotalSize());
|
||||
|
||||
/** Offset of the first rendered row, relative to the top of the list element. */
|
||||
const offsetTop = computed(() => (virtualRows.value[0]?.start ?? 0) - scrollMargin.value);
|
||||
|
||||
function measureElement(el: Element | ComponentPublicInstance | null) {
|
||||
if (el instanceof HTMLElement) {
|
||||
virtualizer.value.measureElement(el);
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToIndex(index: number) {
|
||||
updateLayoutMetrics();
|
||||
virtualizer.value.scrollToIndex(index, { align: 'start' });
|
||||
}
|
||||
|
||||
function scrollToTop() {
|
||||
window.scrollTo({ top: 0 });
|
||||
}
|
||||
|
||||
// Rows change completely when the width or the list changes, so drop cached row sizes.
|
||||
watch([containerWidth, rows], () => {
|
||||
updateLayoutMetrics();
|
||||
virtualizer.value.measure();
|
||||
});
|
||||
|
||||
// The list element is only rendered once there are rows to show.
|
||||
watch(listEl, updateLayoutMetrics, { flush: 'post' });
|
||||
|
||||
useEventListener('resize', updateLayoutMetrics, { passive: true });
|
||||
// Content above the list (page header, banners) can change height after mount.
|
||||
useResizeObserver(
|
||||
() => (typeof document !== 'undefined' ? document.body : null),
|
||||
updateLayoutMetrics,
|
||||
);
|
||||
|
||||
onMounted(updateLayoutMetrics);
|
||||
|
||||
return {
|
||||
virtualizer,
|
||||
virtualRows,
|
||||
totalSize,
|
||||
offsetTop,
|
||||
measureElement,
|
||||
scrollToIndex,
|
||||
scrollToTop,
|
||||
};
|
||||
}
|
||||
@@ -6,14 +6,16 @@ import { useCategoryView } from './useCategoryView';
|
||||
interface UseScrollToCategory {
|
||||
categories: Ref<Pick<Category, 'name' | 'icons'>[]>;
|
||||
categoriesList: Ref<CategoryRow[]>;
|
||||
scrollTo: (index: number) => void;
|
||||
scrollToIndex: (index: number) => void;
|
||||
scrollToTop: () => void;
|
||||
searchQueryDebounced: Ref<string>;
|
||||
}
|
||||
|
||||
export default function useScrollToCategory({
|
||||
categories,
|
||||
categoriesList,
|
||||
scrollTo,
|
||||
scrollToIndex,
|
||||
scrollToTop,
|
||||
searchQueryDebounced,
|
||||
}: UseScrollToCategory) {
|
||||
const { selectedCategory, categoryCounts } = useCategoryView();
|
||||
@@ -28,7 +30,7 @@ export default function useScrollToCategory({
|
||||
|
||||
if (categoryRowIndex !== -1) {
|
||||
setTimeout(() => {
|
||||
scrollTo(categoryRowIndex);
|
||||
scrollToIndex(categoryRowIndex);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
@@ -38,7 +40,7 @@ export default function useScrollToCategory({
|
||||
if (value) {
|
||||
scrollToSelectedCategory(value);
|
||||
} else {
|
||||
scrollTo(0);
|
||||
scrollToTop();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -52,7 +54,7 @@ export default function useScrollToCategory({
|
||||
if (selectedCategory.value) {
|
||||
scrollToSelectedCategory(selectedCategory.value);
|
||||
} else {
|
||||
scrollTo(0);
|
||||
scrollToTop();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
"@headlessui/vue": "^1.7.23",
|
||||
"@lucide/vue": "workspace:*",
|
||||
"@openpanel/web": "^1.4.2",
|
||||
"@tanstack/vue-virtual": "3.13.47",
|
||||
"@vueuse/components": "^14.4.0",
|
||||
"@vueuse/core": "^14.4.0",
|
||||
"@vueuse/integrations": "^14.4.0",
|
||||
|
||||
Generated
+11
-8
@@ -164,6 +164,9 @@ importers:
|
||||
'@openpanel/web':
|
||||
specifier: ^1.4.2
|
||||
version: 1.4.2
|
||||
'@tanstack/vue-virtual':
|
||||
specifier: 3.13.47
|
||||
version: 3.13.47(vue@3.5.44(typescript@6.0.3))
|
||||
'@vueuse/components':
|
||||
specifier: ^14.4.0
|
||||
version: 14.4.0(vue@3.5.44(typescript@6.0.3))
|
||||
@@ -6221,15 +6224,15 @@ packages:
|
||||
'@tanstack/store@0.9.3':
|
||||
resolution: {integrity: sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==}
|
||||
|
||||
'@tanstack/virtual-core@3.17.8':
|
||||
resolution: {integrity: sha512-BfEvehNpOT75r5Ksc5xW6NZuXujTfb7nlSEyVu4XHG3gdxNg1KqXruWbDewXOUaUYIo4oRbSfkjIajz4MAT8tA==}
|
||||
'@tanstack/virtual-core@3.19.6':
|
||||
resolution: {integrity: sha512-CsTW892z23gy7IyxYTTorVu1uZWB+0N349rSMgKz4rFLPpLza7F7TpcCKeI5iUAwYQCt8QUlOU67J+KXCxtpGw==}
|
||||
|
||||
'@tanstack/virtual-file-routes@1.162.0':
|
||||
resolution: {integrity: sha512-uhOeFyxLcU41HzvrxsGpiWdcMbScY1EDgbZ5K7DVRMYInbLYWAC0EA/kx9wXAoSM8q82bUG2hRl8+EAjE6XAbA==}
|
||||
engines: {node: '>=20.19'}
|
||||
|
||||
'@tanstack/vue-virtual@3.13.36':
|
||||
resolution: {integrity: sha512-gKpExv4RbB9luVG+SucTXoqPZv/gzu/Yvz6BNO+8kpNxJ2x+I/ulryzl5W9BRciahZGp5Tls3Dp5XP1ztVGbMw==}
|
||||
'@tanstack/vue-virtual@3.13.47':
|
||||
resolution: {integrity: sha512-6HzdYK9bDyDoD9Q/NjepnC4wGDaZ7mFl5zz8tbn3Fmy7FDicI98ZJ+R37ztFssnoTMoCQhY3GfUhlz+pW/vOTg==}
|
||||
peerDependencies:
|
||||
vue: ^2.7.0 || ^3.0.0
|
||||
|
||||
@@ -16027,7 +16030,7 @@ snapshots:
|
||||
|
||||
'@headlessui/vue@1.7.23(vue@3.5.44(typescript@6.0.3))':
|
||||
dependencies:
|
||||
'@tanstack/vue-virtual': 3.13.36(vue@3.5.44(typescript@6.0.3))
|
||||
'@tanstack/vue-virtual': 3.13.47(vue@3.5.44(typescript@6.0.3))
|
||||
vue: 3.5.44(typescript@6.0.3)
|
||||
|
||||
'@html-eslint/core@0.65.0':
|
||||
@@ -18921,13 +18924,13 @@ snapshots:
|
||||
|
||||
'@tanstack/store@0.9.3': {}
|
||||
|
||||
'@tanstack/virtual-core@3.17.8': {}
|
||||
'@tanstack/virtual-core@3.19.6': {}
|
||||
|
||||
'@tanstack/virtual-file-routes@1.162.0': {}
|
||||
|
||||
'@tanstack/vue-virtual@3.13.36(vue@3.5.44(typescript@6.0.3))':
|
||||
'@tanstack/vue-virtual@3.13.47(vue@3.5.44(typescript@6.0.3))':
|
||||
dependencies:
|
||||
'@tanstack/virtual-core': 3.17.8
|
||||
'@tanstack/virtual-core': 3.19.6
|
||||
vue: 3.5.44(typescript@6.0.3)
|
||||
|
||||
'@testing-library/dom@10.4.2':
|
||||
|
||||
Reference in New Issue
Block a user