Add search icon

This commit is contained in:
Eric Fennis
2023-04-30 22:31:18 +02:00
parent 3ea6fb4706
commit 2d29235168
9 changed files with 218 additions and 87 deletions
+8
View File
@@ -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)
)
+12 -29
View File
@@ -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('')"/>
+29 -7
View File
@@ -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;
+16 -10
View File
@@ -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
View File
@@ -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>