mirror of
https://github.com/lucide-icons/lucide.git
synced 2026-11-14 13:58:21 +00:00
Add search icon
This commit is contained in:
@@ -1,8 +1,14 @@
|
||||
<script setup>
|
||||
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon'
|
||||
import { search } from '../../iconNodes'
|
||||
import { computed } from 'vue';
|
||||
|
||||
const SearchIcon = createLucideIcon('search', search)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button class="fake-input">
|
||||
<component :is="SearchIcon" class="search-icon"/>
|
||||
<slot/>
|
||||
</button>
|
||||
</template>
|
||||
@@ -19,6 +25,8 @@
|
||||
text-align: left;
|
||||
border: 1px solid transparent;
|
||||
cursor: text;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.fake-input:hover, .fake-input:focus {
|
||||
|
||||
@@ -63,9 +63,9 @@ onBeforeUnmount(() => {
|
||||
</IconButton>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
<FakeInput @click="go('/icons/?search')" class="search-box">
|
||||
Search 964 icons...
|
||||
</FakeInput>
|
||||
<FakeInput @click="go('/icons/?focus')" class="search-box">
|
||||
Search 964 icons...
|
||||
</FakeInput>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -76,6 +76,7 @@ onBeforeUnmount(() => {
|
||||
/* padding: 0 24px; */
|
||||
margin-left: auto;
|
||||
margin-bottom: auto;
|
||||
margin-top: 48px;;
|
||||
}
|
||||
.icons-card {
|
||||
background: var(--vp-c-bg-alt);
|
||||
@@ -128,9 +129,20 @@ onBeforeUnmount(() => {
|
||||
|
||||
.search-box {
|
||||
position: absolute;
|
||||
bottom:-24px;
|
||||
width: 100%;
|
||||
left: -24px;
|
||||
left: 0;
|
||||
top: -48px;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.search-box {
|
||||
top: unset;
|
||||
bottom: -24px;
|
||||
left: -24px;
|
||||
}
|
||||
.card-wrapper {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* .search-box :deep(.input) {
|
||||
|
||||
@@ -7,24 +7,16 @@ import useSearch from '../composables/useSearch'
|
||||
import Input from './Input.vue'
|
||||
import EndOfPage from './EndOfPage.vue'
|
||||
import { useCategoryView } from '../composables/useCategoryView'
|
||||
import useSearchInput from '../composables/useSearchInput'
|
||||
|
||||
const props = defineProps<{
|
||||
icons: IconEntity[]
|
||||
categories: Category[]
|
||||
}>()
|
||||
|
||||
const { selectedCategory } = useCategoryView()
|
||||
const activeIconName = ref('')
|
||||
const searchQuery = ref(
|
||||
typeof window === 'undefined'
|
||||
? ''
|
||||
: (
|
||||
new URLSearchParams(window.location.search).get('search')
|
||||
|| ''
|
||||
)
|
||||
)
|
||||
const { searchInput, searchQuery, searchQueryThrottled } = useSearchInput()
|
||||
|
||||
const searchInput = ref()
|
||||
const isSearching = computed(() => !!searchQuery.value)
|
||||
|
||||
function setActiveIconName(name: string) {
|
||||
@@ -57,23 +49,6 @@ const categories = computed(() => {
|
||||
.filter(({ icons }) => icons.length)
|
||||
})
|
||||
|
||||
watch(searchQuery, (searchString) => {
|
||||
const newUrl = new URL(window.location.href);
|
||||
|
||||
newUrl.searchParams.set('search', searchString);
|
||||
|
||||
nextTick(() => {
|
||||
window.history.replaceState({}, '', newUrl)
|
||||
})
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
if(searchParams.has('search')) {
|
||||
searchInput.value.focus()
|
||||
}
|
||||
})
|
||||
|
||||
const activeIcon = computed(() =>
|
||||
props.icons?.find((icon) => icon.name === activeIconName.value)
|
||||
)
|
||||
|
||||
@@ -1,34 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, nextTick, onMounted } from 'vue'
|
||||
import type { IconEntity } from '../types'
|
||||
import { useMediaQuery, useOffsetPagination } from '@vueuse/core'
|
||||
import { refThrottled, useMediaQuery, useOffsetPagination } from '@vueuse/core'
|
||||
import IconItem from './IconItem.vue'
|
||||
import IconDetailOverlay from './IconDetailOverlay.vue'
|
||||
import IconGrid from './IconGrid.vue'
|
||||
import Input from './Input.vue'
|
||||
import InputSearch from './InputSearch.vue'
|
||||
import useSearch from '../composables/useSearch'
|
||||
import EndOfPage from './EndOfPage.vue'
|
||||
import useSearchInput from '../composables/useSearchInput'
|
||||
|
||||
const props = defineProps<{
|
||||
icons: IconEntity[]
|
||||
}>()
|
||||
|
||||
const activeIconName = ref('')
|
||||
const searchQuery = ref(
|
||||
typeof window === 'undefined'
|
||||
? ''
|
||||
: (
|
||||
new URLSearchParams(window.location.search).get('search')
|
||||
|| ''
|
||||
)
|
||||
)
|
||||
|
||||
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 searchInput = ref()
|
||||
const pageSize = computed(() => {
|
||||
if(isExtraLargeScreen.value) {
|
||||
return 16 * 16;
|
||||
@@ -47,7 +39,8 @@ const pageSize = computed(() => {
|
||||
return 10 * 5;
|
||||
})
|
||||
|
||||
const searchResults = useSearch(searchQuery, props.icons, [
|
||||
const { searchInput, searchQuery, searchQueryThrottled } = useSearchInput()
|
||||
const searchResults = useSearch(searchQueryThrottled, props.icons, [
|
||||
{ name: 'name', weight: 2 },
|
||||
{ name: 'tags', weight: 1 },
|
||||
])
|
||||
@@ -69,28 +62,18 @@ const activeIcon = computed(() =>
|
||||
props.icons.find((icon) => icon.name === activeIconName.value)
|
||||
)
|
||||
|
||||
watch(searchQuery, (searchString) => {
|
||||
const newUrl = new URL(window.location.href);
|
||||
|
||||
newUrl.searchParams.set('search', searchString);
|
||||
|
||||
nextTick(() => {
|
||||
window.history.replaceState({}, '', newUrl)
|
||||
})
|
||||
|
||||
watch(searchQueryThrottled, (searchString) => {
|
||||
currentPage.value = 1
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
if(searchParams.has('search')) {
|
||||
searchInput.value.focus()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Input type="search" placeholder="Search icons..." v-model="searchQuery" class="input-wrapper" ref="searchInput"/>
|
||||
<InputSearch
|
||||
placeholder="Search icons..."
|
||||
v-model="searchQuery"
|
||||
class="input-wrapper"
|
||||
ref="searchInput"
|
||||
/>
|
||||
<IconGrid :activeIcon="activeIconName" :icons="paginatedIcons" @setActiveIcon="setActiveIconName"/>
|
||||
<EndOfPage @end-of-page="next" class="bottom-page"/>
|
||||
<IconDetailOverlay :icon="activeIcon" @close="setActiveIconName('')"/>
|
||||
|
||||
@@ -2,23 +2,23 @@
|
||||
export default {
|
||||
inheritAttrs: false,
|
||||
}
|
||||
|
||||
export interface InputProps {
|
||||
type: string
|
||||
modelValue: string
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
interface Props {
|
||||
type: string
|
||||
modelValue: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
const props = withDefaults(defineProps<InputProps>(), {
|
||||
type: 'text'
|
||||
})
|
||||
|
||||
const input = ref()
|
||||
|
||||
defineEmits(['change', 'input'])
|
||||
defineEmits(['change', 'input', 'update:modelValue'])
|
||||
|
||||
defineExpose({
|
||||
focus: () => {
|
||||
@@ -29,9 +29,11 @@ defineExpose({
|
||||
|
||||
<template>
|
||||
<div class="input-wrapper">
|
||||
<slot name="icon" class="icon" />
|
||||
<input
|
||||
:type="type"
|
||||
class="input"
|
||||
:class="{'has-icon': $slots.icon}"
|
||||
ref="input"
|
||||
:value="modelValue"
|
||||
v-bind="$attrs"
|
||||
@@ -41,6 +43,9 @@ defineExpose({
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.input-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
.input {
|
||||
justify-content: flex-start;
|
||||
border: 1px solid transparent;
|
||||
@@ -49,6 +54,7 @@ defineExpose({
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.input:hover, .input:focus {
|
||||
@@ -56,4 +62,20 @@ defineExpose({
|
||||
background: var(--vp-c-bg-alt);
|
||||
}
|
||||
|
||||
.input.has-icon {
|
||||
padding-left: 52px;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.input-wrapper svg {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 12px;
|
||||
z-index: 1;
|
||||
color: var(--vp-c-text-2);
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
<script lang="ts">
|
||||
export default {
|
||||
inheritAttrs: false,
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import Input from './Input.vue'
|
||||
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon'
|
||||
import { search } from '../../iconNodes'
|
||||
|
||||
const SearchIcon = createLucideIcon('search', search)
|
||||
|
||||
interface Props {
|
||||
modelValue: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const input = ref()
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
defineExpose({
|
||||
focus: () => {
|
||||
input.value.focus()
|
||||
}
|
||||
})
|
||||
|
||||
const value = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Input
|
||||
type="search"
|
||||
v-bind="$attrs"
|
||||
v-model="value"
|
||||
class="input-wrapper" ref="searchInput"
|
||||
>
|
||||
<template #icon>
|
||||
<component :is="SearchIcon" class="search-icon" />
|
||||
</template>
|
||||
</Input>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.input {
|
||||
justify-content: flex-start;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
padding: 0 10px 0 12px;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
}
|
||||
|
||||
.input:hover, .input:focus {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-bg-alt);
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,40 @@
|
||||
import { refThrottled } from '@vueuse/core';
|
||||
import { nextTick, onMounted, ref, watch } from 'vue';
|
||||
|
||||
const useSearchInput = () => {
|
||||
const searchInput = ref()
|
||||
const searchQuery = ref(
|
||||
typeof window === 'undefined'
|
||||
? ''
|
||||
: (
|
||||
new URLSearchParams(window.location.search).get('search')
|
||||
|| ''
|
||||
)
|
||||
)
|
||||
const searchQueryThrottled = refThrottled(searchQuery, 200)
|
||||
|
||||
watch(searchQueryThrottled, (searchString) => {
|
||||
const newUrl = new URL(window.location.href);
|
||||
|
||||
newUrl.searchParams.set('search', searchString);
|
||||
|
||||
nextTick(() => {
|
||||
window.history.replaceState({}, '', newUrl)
|
||||
})
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
if(searchParams.has('focus')) {
|
||||
searchInput.value.focus()
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
searchInput,
|
||||
searchQuery,
|
||||
searchQueryThrottled
|
||||
};
|
||||
};
|
||||
|
||||
export default useSearchInput;
|
||||
@@ -36,13 +36,13 @@
|
||||
flex-direction: column-reverse;
|
||||
} */
|
||||
.VPHomeHero .container .main {
|
||||
flex:1;
|
||||
/* flex:1; */
|
||||
flex-shirk: 0;
|
||||
}
|
||||
.VPHomeHero .container .image {
|
||||
margin: 0;
|
||||
order: 2;
|
||||
flex: 1;
|
||||
/* flex: 1; */
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
@@ -65,14 +65,6 @@
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
@media (min-width: 720px) {
|
||||
.vp-doc[class^="_icons_"], .vp-doc[class*=" _icons_"] {
|
||||
display: grid;
|
||||
grid-template-columns: 240px minmax(0, 1fr);
|
||||
gap: 24px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
|
||||
@@ -82,6 +74,20 @@
|
||||
margin-top: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.VPHomeHero .container .main {
|
||||
/* flex: 1; */
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.VPHomeHero .container .image {
|
||||
/* flex: 1; */
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.VPHomeHero .container .image-container {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
/*
|
||||
html:has(* .outline-link:target) {
|
||||
|
||||
+28
-9
@@ -26,15 +26,20 @@ const codeExample = computed(() => params.value.codeExamples?.map(
|
||||
)
|
||||
</script>
|
||||
|
||||
<IconPreview
|
||||
:name="$params.name"
|
||||
:iconNode="$params.iconNode"
|
||||
:class="$style.preview"
|
||||
/>
|
||||
</div><div>
|
||||
<IconDetailName>
|
||||
{{$params.name}}
|
||||
</IconDetailName>
|
||||
<div :class="$style.layout">
|
||||
<div>
|
||||
<IconPreview
|
||||
:name="$params.name"
|
||||
:iconNode="$params.iconNode"
|
||||
:class="$style.preview"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<IconDetailName>
|
||||
{{$params.name}}
|
||||
</IconDetailName>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CodeGroup :groups="tabs" groupName="icon-code-example">
|
||||
<div
|
||||
@@ -46,5 +51,19 @@ const codeExample = computed(() => params.value.codeExamples?.map(
|
||||
<style module>
|
||||
.preview {
|
||||
margin-bottom: 24px;
|
||||
max-width: 240px;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.layout {
|
||||
align-items: flex-start;
|
||||
display: grid;
|
||||
grid-template-columns: 240px minmax(0, 1fr);
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.preview {
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user