mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 22:49:31 -07:00
fix: Change bg color and format
This commit is contained in:
@@ -1,48 +1,79 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="isOpen"
|
<div
|
||||||
|
v-if="isOpen"
|
||||||
class="search-dialog fixed inset-0 bg-black/90 z-[9999] flex items-start justify-center pt-[64px] animate-fade-in"
|
class="search-dialog fixed inset-0 bg-black/90 z-[9999] flex items-start justify-center pt-[64px] animate-fade-in"
|
||||||
@click="closeDialog">
|
@click="closeDialog"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="bg-[#060010] border border-[#2e4e3e] p-4 mx-4 pb-2 rounded-xl w-full max-w-[720px] overflow-hidden animate-slide-in"
|
class="bg-[#0b0b0b] border border-[#2e4e3e] p-4 mx-4 pb-2 rounded-xl w-full max-w-[720px] overflow-hidden animate-slide-in"
|
||||||
@click.stop>
|
@click.stop
|
||||||
|
>
|
||||||
<div class="relative flex items-center pb-2">
|
<div class="relative flex items-center pb-2">
|
||||||
<i class="pi pi-search text-[#9eefb0] py-2.5 px-3"></i>
|
<i class="pi pi-search text-[#9eefb0] py-2.5 px-3"></i>
|
||||||
<input ref="searchInputRef" v-model="inputValue" type="text" placeholder="Search the docs"
|
<input
|
||||||
|
ref="searchInputRef"
|
||||||
|
v-model="inputValue"
|
||||||
|
type="text"
|
||||||
|
placeholder="Search the docs"
|
||||||
class="flex-1 bg-transparent border-none outline-none text-white text-lg placeholder:text-[#182916] focus:outline-none"
|
class="flex-1 bg-transparent border-none outline-none text-white text-lg placeholder:text-[#182916] focus:outline-none"
|
||||||
@keydown="handleKeyDown" />
|
@keydown="handleKeyDown"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
<Motion v-if="searchValue" key="results" :initial="{ height: 0, opacity: 0 }"
|
<Motion
|
||||||
:animate="{ height: 'auto', opacity: 1 }" :exit="{ height: 0, opacity: 0 }" :transition="{ duration: 0.3 }"
|
v-if="searchValue"
|
||||||
class="overflow-hidden">
|
key="results"
|
||||||
|
:initial="{ height: 0, opacity: 0 }"
|
||||||
<div v-if="searchResults.length > 0"
|
:animate="{ height: 'auto', opacity: 1 }"
|
||||||
class="relative max-h-[400px] overflow-hidden border-t border-[#2e4e3e] pb-4">
|
:exit="{ height: 0, opacity: 0 }"
|
||||||
<div class="max-h-[400px] overflow-y-auto" :style="{
|
:transition="{ duration: 0.3 }"
|
||||||
scrollbarWidth: 'thin',
|
class="overflow-hidden"
|
||||||
scrollbarColor: '#1e3725 #0b0b0b'
|
>
|
||||||
}" ref="listRef" @scroll="handleScroll">
|
<div
|
||||||
|
v-if="searchResults.length > 0"
|
||||||
<Motion v-for="(result, index) in searchResults"
|
class="relative max-h-[400px] overflow-hidden border-t border-[#2e4e3e] pb-4"
|
||||||
:key="`${result.categoryName}-${result.componentName}-${index}`" :data-index="index"
|
>
|
||||||
class="cursor-pointer mr-2" :initial="{ scale: 0.7, opacity: 0 }"
|
<div
|
||||||
|
class="max-h-[400px] overflow-y-auto"
|
||||||
|
:style="{
|
||||||
|
scrollbarWidth: 'thin',
|
||||||
|
scrollbarColor: '#1e3725 #0b0b0b'
|
||||||
|
}"
|
||||||
|
ref="listRef"
|
||||||
|
@scroll="handleScroll"
|
||||||
|
>
|
||||||
|
<Motion
|
||||||
|
v-for="(result, index) in searchResults"
|
||||||
|
:key="`${result.categoryName}-${result.componentName}-${index}`"
|
||||||
|
:data-index="index"
|
||||||
|
class="cursor-pointer mr-2"
|
||||||
|
:initial="{ scale: 0.7, opacity: 0 }"
|
||||||
:animate="getItemInView(index) ? { scale: 1, opacity: 1 } : { scale: 0.7, opacity: 0 }"
|
:animate="getItemInView(index) ? { scale: 1, opacity: 1 } : { scale: 0.7, opacity: 0 }"
|
||||||
:transition="{ duration: 0.2, delay: 0.05 }" @mouseenter="() => setSelectedIndex(index)" @click="
|
:transition="{ duration: 0.2, delay: 0.05 }"
|
||||||
|
@mouseenter="() => setSelectedIndex(index)"
|
||||||
|
@click="
|
||||||
() => {
|
() => {
|
||||||
handleSelect(searchResults[index])
|
handleSelect(searchResults[index]);
|
||||||
}
|
}
|
||||||
">
|
"
|
||||||
<div class="flex items-center p-[14px] rounded-xl transition-all duration-200" :class="[
|
>
|
||||||
selectedIndex === index ? 'bg-[#2e4e31]' : 'hover:bg-[#2e4e31] bg-[#1e3725]',
|
<div
|
||||||
index === 0 ? 'mt-4' : 'mt-2'
|
class="flex items-center p-[14px] rounded-xl transition-all duration-200"
|
||||||
]">
|
:class="[
|
||||||
<i class="text-[#9eefb0] mr-4"
|
selectedIndex === index ? 'bg-[#2e4e31]' : 'hover:bg-[#2e4e31] bg-[#222]',
|
||||||
:class="categoryIconMapping[result.categoryName as keyof typeof categoryIconMapping] || 'pi pi-search'"></i>
|
index === 0 ? 'mt-4' : 'mt-2'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="text-[#9eefb0] mr-4"
|
||||||
|
:class="
|
||||||
|
categoryIconMapping[result.categoryName as keyof typeof categoryIconMapping] || 'pi pi-search'
|
||||||
|
"
|
||||||
|
></i>
|
||||||
<div class="flex flex-col flex-1">
|
<div class="flex flex-col flex-1">
|
||||||
<div class="text-white text-base font-bold">{{ result.componentName }}</div>
|
<div class="text-white text-base font-bold">{{ result.componentName }}</div>
|
||||||
<div class="text-[#9eefb0] text-sm ">in {{ result.categoryName }}</div>
|
<div class="text-[#9eefb0] text-sm">in {{ result.categoryName }}</div>
|
||||||
</div>
|
</div>
|
||||||
<i class="pi pi-reply rotate-180 text-[#9eefb0]"></i>
|
<i class="pi pi-reply rotate-180 text-[#9eefb0]"></i>
|
||||||
</div>
|
</div>
|
||||||
@@ -50,29 +81,34 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="absolute top-0 left-0 right-0 h-[50px] bg-gradient-to-b from-[#031000] to-transparent pointer-events-none transition-opacity duration-300 ease"
|
class="absolute top-0 left-0 right-0 h-[50px] bg-gradient-to-b from-[#0b0b0b] to-transparent pointer-events-none transition-opacity duration-300 ease"
|
||||||
:style="{ opacity: topGradientOpacity }"></div>
|
:style="{ opacity: topGradientOpacity }"
|
||||||
|
></div>
|
||||||
<div
|
<div
|
||||||
class="absolute bottom-0 left-0 right-0 h-[100px] bg-gradient-to-t from-[#031000] to-transparent pointer-events-none transition-opacity duration-300 ease"
|
class="absolute bottom-0 left-0 right-0 h-[100px] bg-gradient-to-t from-[#0b0b0b] to-transparent pointer-events-none transition-opacity duration-300 ease"
|
||||||
:style="{ opacity: bottomGradientOpacity }"></div>
|
:style="{ opacity: bottomGradientOpacity }"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="searchValue && searchResults.length === 0"
|
<div
|
||||||
class="p-4 pb-2 text-center border-t border-[#2e4e3e]">
|
v-else-if="searchValue && searchResults.length === 0"
|
||||||
<div class="text-[#9eefac] text-sm">No results found for "<span class="font-black">{{ searchValue }}</span>"
|
class="p-4 pb-2 text-center border-t border-[#2e4e3e]"
|
||||||
|
>
|
||||||
|
<div class="text-[#9eefac] text-sm">
|
||||||
|
No results found for "
|
||||||
|
<span class="font-black">{{ searchValue }}</span>
|
||||||
|
"
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</Motion>
|
</Motion>
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch, nextTick, onUnmounted, useTemplateRef } from 'vue'
|
import { ref, computed, watch, nextTick, onUnmounted, useTemplateRef } from 'vue';
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router';
|
||||||
import { CATEGORIES } from '../../constants/Categories';
|
import { CATEGORIES } from '../../constants/Categories';
|
||||||
import { Motion, AnimatePresence } from 'motion-v';
|
import { Motion, AnimatePresence } from 'motion-v';
|
||||||
|
|
||||||
@@ -81,47 +117,47 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['close', 'open'])
|
const emit = defineEmits(['close', 'open']);
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
const searchInputRef = ref<HTMLInputElement | null>(null)
|
const searchInputRef = ref<HTMLInputElement | null>(null);
|
||||||
const inputValue = ref('')
|
const inputValue = ref('');
|
||||||
const selectedIndex = ref(0)
|
const selectedIndex = ref(0);
|
||||||
const listRef = useTemplateRef<HTMLDivElement>('listRef');
|
const listRef = useTemplateRef<HTMLDivElement>('listRef');
|
||||||
const itemsInView = ref<boolean[]>([]);
|
const itemsInView = ref<boolean[]>([]);
|
||||||
const keyboardNav = ref(false);
|
const keyboardNav = ref(false);
|
||||||
const topGradientOpacity = ref(0);
|
const topGradientOpacity = ref(0);
|
||||||
const bottomGradientOpacity = ref(1);
|
const bottomGradientOpacity = ref(1);
|
||||||
const searchValue = ref('')
|
const searchValue = ref('');
|
||||||
let debounceTimer: any = null
|
let debounceTimer: any = null;
|
||||||
|
|
||||||
interface SearchResult {
|
interface SearchResult {
|
||||||
categoryName: string
|
categoryName: string;
|
||||||
componentName: string
|
componentName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const categoryIconMapping = {
|
const categoryIconMapping = {
|
||||||
"Get Started": 'pi pi-file',
|
'Get Started': 'pi pi-file',
|
||||||
"Text Animations": 'pi pi-hashtag',
|
'Text Animations': 'pi pi-hashtag',
|
||||||
"Animations": 'pi pi-circle',
|
Animations: 'pi pi-circle',
|
||||||
"Components": 'pi pi-box',
|
Components: 'pi pi-box',
|
||||||
"Backgrounds": 'pi pi-image',
|
Backgrounds: 'pi pi-image'
|
||||||
};
|
};
|
||||||
|
|
||||||
const searchResults = computed(() => {
|
const searchResults = computed(() => {
|
||||||
return searchComponents(searchValue.value)
|
return searchComponents(searchValue.value);
|
||||||
})
|
});
|
||||||
|
|
||||||
watch(inputValue, (newValue) => {
|
watch(inputValue, newValue => {
|
||||||
if (debounceTimer) clearTimeout(debounceTimer)
|
if (debounceTimer) clearTimeout(debounceTimer);
|
||||||
debounceTimer = setTimeout(() => {
|
debounceTimer = setTimeout(() => {
|
||||||
searchValue.value = newValue
|
searchValue.value = newValue;
|
||||||
selectedIndex.value = -1
|
selectedIndex.value = -1;
|
||||||
}, 500)
|
}, 500);
|
||||||
})
|
});
|
||||||
|
|
||||||
watch([selectedIndex, keyboardNav], () => {
|
watch([selectedIndex, keyboardNav], () => {
|
||||||
if (!keyboardNav.value || selectedIndex.value < 0 || !listRef.value) return;
|
if (!keyboardNav.value || selectedIndex.value < 0 || !listRef.value) return;
|
||||||
@@ -146,19 +182,19 @@ watch([selectedIndex, keyboardNav], () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const levenshtein = (a: string, b: string): number => {
|
const levenshtein = (a: string, b: string): number => {
|
||||||
const m = a.length, n = b.length;
|
const m = a.length,
|
||||||
|
n = b.length;
|
||||||
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
||||||
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
||||||
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
||||||
for (let i = 1; i <= m; i++) {
|
for (let i = 1; i <= m; i++) {
|
||||||
for (let j = 1; j <= n; j++) {
|
for (let j = 1; j <= n; j++) {
|
||||||
dp[i][j] = a[i - 1] === b[j - 1]
|
dp[i][j] =
|
||||||
? dp[i - 1][j - 1]
|
a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : Math.min(dp[i - 1][j - 1] + 1, dp[i][j - 1] + 1, dp[i - 1][j] + 1);
|
||||||
: Math.min(dp[i - 1][j - 1] + 1, dp[i][j - 1] + 1, dp[i - 1][j] + 1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return dp[m][n];
|
return dp[m][n];
|
||||||
}
|
};
|
||||||
|
|
||||||
const fuzzyMatch = (candidate: string, query: string): boolean => {
|
const fuzzyMatch = (candidate: string, query: string): boolean => {
|
||||||
const lowerCandidate = candidate.toLowerCase();
|
const lowerCandidate = candidate.toLowerCase();
|
||||||
@@ -173,7 +209,7 @@ const fuzzyMatch = (candidate: string, query: string): boolean => {
|
|||||||
return distance <= threshold;
|
return distance <= threshold;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const searchComponents = (query: string): SearchResult[] => {
|
const searchComponents = (query: string): SearchResult[] => {
|
||||||
if (!query || query.trim() === '') return [];
|
if (!query || query.trim() === '') return [];
|
||||||
@@ -181,18 +217,15 @@ const searchComponents = (query: string): SearchResult[] => {
|
|||||||
CATEGORIES.forEach(category => {
|
CATEGORIES.forEach(category => {
|
||||||
const { name: categoryName, subcategories } = category;
|
const { name: categoryName, subcategories } = category;
|
||||||
if (fuzzyMatch(categoryName, query)) {
|
if (fuzzyMatch(categoryName, query)) {
|
||||||
subcategories.forEach(component =>
|
subcategories.forEach(component => results.push({ categoryName, componentName: component }));
|
||||||
results.push({ categoryName, componentName: component })
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
subcategories.forEach(component => {
|
subcategories.forEach(component => {
|
||||||
if (fuzzyMatch(component, query))
|
if (fuzzyMatch(component, query)) results.push({ categoryName, componentName: component });
|
||||||
results.push({ categoryName, componentName: component });
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return results;
|
return results;
|
||||||
}
|
};
|
||||||
|
|
||||||
watch(searchResults, () => {
|
watch(searchResults, () => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
@@ -202,21 +235,21 @@ watch(searchResults, () => {
|
|||||||
const bottomDistance = scrollHeight - (scrollTop + clientHeight);
|
const bottomDistance = scrollHeight - (scrollTop + clientHeight);
|
||||||
bottomGradientOpacity.value = scrollHeight <= clientHeight ? 0 : Math.min(bottomDistance / 50, 1);
|
bottomGradientOpacity.value = scrollHeight <= clientHeight ? 0 : Math.min(bottomDistance / 50, 1);
|
||||||
});
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if (!searchValue.value) return;
|
if (!searchValue.value) return;
|
||||||
if (event.key === "ArrowDown" || (event.key === "Tab" && !event.shiftKey)) {
|
if (event.key === 'ArrowDown' || (event.key === 'Tab' && !event.shiftKey)) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
keyboardNav.value = true;
|
keyboardNav.value = true;
|
||||||
selectedIndex.value = Math.min(selectedIndex.value + 1, searchResults.value.length - 1)
|
selectedIndex.value = Math.min(selectedIndex.value + 1, searchResults.value.length - 1);
|
||||||
} else if (event.key === "ArrowUp" || (event.key === "Tab" && event.shiftKey)) {
|
} else if (event.key === 'ArrowUp' || (event.key === 'Tab' && event.shiftKey)) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
keyboardNav.value = true;
|
keyboardNav.value = true;
|
||||||
selectedIndex.value = Math.max(selectedIndex.value - 1, 0)
|
selectedIndex.value = Math.max(selectedIndex.value - 1, 0);
|
||||||
} else if (event.key === "Enter" && selectedIndex.value >= 0) {
|
} else if (event.key === 'Enter' && selectedIndex.value >= 0) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
handleSelect(searchResults.value[selectedIndex.value])
|
handleSelect(searchResults.value[selectedIndex.value]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -230,10 +263,10 @@ const handleScroll = (e: Event) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSelect = (result: SearchResult) => {
|
const handleSelect = (result: SearchResult) => {
|
||||||
const slug = (result: string) => result.replace(/\s+/g, "-").toLowerCase();
|
const slug = (result: string) => result.replace(/\s+/g, '-').toLowerCase();
|
||||||
router.push(`/${slug(result.categoryName)}/${slug(result.componentName)}`)
|
router.push(`/${slug(result.categoryName)}/${slug(result.componentName)}`);
|
||||||
closeDialog()
|
closeDialog();
|
||||||
}
|
};
|
||||||
|
|
||||||
const setSelectedIndex = (index: number) => {
|
const setSelectedIndex = (index: number) => {
|
||||||
selectedIndex.value = index;
|
selectedIndex.value = index;
|
||||||
@@ -263,33 +296,35 @@ const updateItemsInView = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const closeDialog = () => {
|
const closeDialog = () => {
|
||||||
document.body.classList.remove('overflow-hidden')
|
document.body.classList.remove('overflow-hidden');
|
||||||
emit('close')
|
emit('close');
|
||||||
inputValue.value = ''
|
inputValue.value = '';
|
||||||
selectedIndex.value = 0
|
selectedIndex.value = 0;
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleGlobalKeyDown = (event: KeyboardEvent) => {
|
const handleGlobalKeyDown = (event: KeyboardEvent) => {
|
||||||
if (event.key === '/' && !props.isOpen) {
|
if (event.key === '/' && !props.isOpen) {
|
||||||
event.preventDefault()
|
event.preventDefault();
|
||||||
emit('open')
|
emit('open');
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
watch(() => props.isOpen, (isOpen) => {
|
watch(
|
||||||
if (isOpen) {
|
() => props.isOpen,
|
||||||
document.body.classList.add('overflow-hidden')
|
isOpen => {
|
||||||
nextTick(() => {
|
if (isOpen) {
|
||||||
searchInputRef.value?.focus()
|
document.body.classList.add('overflow-hidden');
|
||||||
})
|
nextTick(() => {
|
||||||
|
searchInputRef.value?.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
);
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
document.removeEventListener('keydown', handleGlobalKeyDown)
|
document.removeEventListener('keydown', handleGlobalKeyDown);
|
||||||
if (debounceTimer) clearTimeout(debounceTimer)
|
if (debounceTimer) clearTimeout(debounceTimer);
|
||||||
})
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -324,7 +359,6 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
|
||||||
.animate-fade-in,
|
.animate-fade-in,
|
||||||
.animate-slide-in {
|
.animate-slide-in {
|
||||||
animation: none;
|
animation: none;
|
||||||
|
|||||||
@@ -380,11 +380,13 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
padding: 0 0.5rem;
|
|
||||||
border-radius: 50px;
|
border-radius: 50px;
|
||||||
background: #1a1a1a;
|
background: #1a1a1a;
|
||||||
color: #a6a6a6;
|
color: #a6a6a6;
|
||||||
border: 1px solid #333;
|
border: 1px solid #333;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.star-count {
|
.star-count {
|
||||||
|
|||||||
Reference in New Issue
Block a user