diff --git a/docs/.vitepress/theme/components/base/FakeInput.vue b/docs/.vitepress/theme/components/base/FakeInput.vue index 613a48aca..26a9380b1 100644 --- a/docs/.vitepress/theme/components/base/FakeInput.vue +++ b/docs/.vitepress/theme/components/base/FakeInput.vue @@ -3,12 +3,20 @@ import createLucideIcon from 'lucide-vue-next/src/createLucideIcon' import { search } from '../../../data/iconNodes' const SearchIcon = createLucideIcon('search', search) + +defineProps({ + shortcut: { + type: String, + required: false + } +}) @@ -33,4 +41,23 @@ const SearchIcon = createLucideIcon('search', search) border-color: var(--vp-c-brand); background: var(--vp-c-bg-alt); } + +.shortcut { + margin-left: auto; + padding: 2px 6px; + font-size: 12px; + font-family: inherit; + font-weight: 500; + line-height: 1.5; + color: var(--vp-c-text-3); + background: var(--vp-c-default-soft); + border: 1px solid var(--vp-c-divider); + border-radius: 4px; +} + +@media (hover: none) { + .shortcut { + display: none; + } +} diff --git a/docs/.vitepress/theme/components/base/Input.vue b/docs/.vitepress/theme/components/base/Input.vue index d624b265a..59b4fbfa3 100644 --- a/docs/.vitepress/theme/components/base/Input.vue +++ b/docs/.vitepress/theme/components/base/Input.vue @@ -6,20 +6,35 @@ export default { export interface InputProps { type: string modelValue: string + shortcut?: string } @@ -57,6 +73,10 @@ defineExpose({ font-size: 14px; } +.input.has-shortcut { + padding-right: calc(var(--shortcut-width, 40px) + 22px); +} + .input:hover, .input:focus { border-color: var(--vp-c-brand); background: var(--vp-c-bg-alt); @@ -66,7 +86,28 @@ defineExpose({ padding-left: 52px; } +.shortcut { + position: absolute; + right: 12px; + top: 50%; + transform: translateY(-50%); + padding: 2px 6px; + font-size: 12px; + font-family: inherit; + font-weight: 500; + line-height: 1.5; + color: var(--vp-c-text-3); + background: var(--vp-c-default-soft); + border: 1px solid var(--vp-c-divider); + border-radius: 4px; + pointer-events: none; +} +@media (hover: none) { + .shortcut { + display: none; + } +} diff --git a/docs/.vitepress/theme/components/home/HomeHeroIconsCard.vue b/docs/.vitepress/theme/components/home/HomeHeroIconsCard.vue index 9471e815f..b3acb5f03 100644 --- a/docs/.vitepress/theme/components/home/HomeHeroIconsCard.vue +++ b/docs/.vitepress/theme/components/home/HomeHeroIconsCard.vue @@ -5,10 +5,15 @@ import LucideIcon from '../base/LucideIcon.vue' import { useRouter } from 'vitepress'; import { random } from 'lodash-es' import FakeInput from '../base/FakeInput.vue' +import useSearchShortcut from '../../utils/useSearchShortcut' const { go } = useRouter() const intervalTime = shallowRef() +const { shortcutText: kbdSearchShortcut } = useSearchShortcut(() => { + go('/icons/?focus') +}) + const getInitialItems = () => data.icons.slice(0, 48) const items = ref(getInitialItems()) let id = items.value.length + 1 @@ -64,7 +69,11 @@ onBeforeUnmount(() => { - + Search {{ data.iconsCount }} icons... diff --git a/docs/.vitepress/theme/components/icons/IconsCategoryOverview.vue b/docs/.vitepress/theme/components/icons/IconsCategoryOverview.vue index 4da3ab34a..355bc2685 100644 --- a/docs/.vitepress/theme/components/icons/IconsCategoryOverview.vue +++ b/docs/.vitepress/theme/components/icons/IconsCategoryOverview.vue @@ -4,6 +4,7 @@ import type { IconEntity, Category } from '../../types'; import useSearch from '../../composables/useSearch'; import InputSearch from '../base/InputSearch.vue'; import useSearchInput from '../../composables/useSearchInput'; +import useSearchShortcut from '../../utils/useSearchShortcut'; import StickyBar from './StickyBar.vue'; import IconsCategory from './IconsCategory.vue'; import useFetchTags from '../../composables/useFetchTags'; @@ -27,6 +28,10 @@ const activeIconName = ref(null); const { searchInput, searchQuery, searchQueryDebounced } = useSearchInput(); const isSearching = computed(() => !!searchQuery.value); +const { shortcutText: kbdSearchShortcut } = useSearchShortcut(() => { + searchInput.value?.focus(); +}); + function setActiveIconName(name: string) { activeIconName.value = name; } @@ -154,6 +159,7 @@ watchEffect(() => { -import { ref, computed, defineAsyncComponent, onMounted, watch } from 'vue'; +import { ref, computed, defineAsyncComponent, onMounted, onBeforeUnmount, 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 InputSearch from '../base/InputSearch.vue'; import useSearch from '../../composables/useSearch'; import useSearchInput from '../../composables/useSearchInput'; +import useSearchShortcut from '../../utils/useSearchShortcut'; import StickyBar from './StickyBar.vue'; import useFetchTags from '../../composables/useFetchTags'; import useFetchCategories from '../../composables/useFetchCategories'; @@ -58,6 +60,11 @@ const mappedIcons = computed(() => { }); const { searchInput, searchQuery, searchQueryDebounced } = useSearchInput(); + +const { shortcutText: kbdSearchShortcut } = useSearchShortcut(() => { + searchInput.value?.focus(); +}); + const searchResults = useSearch(searchQueryDebounced, mappedIcons, [ { name: 'name', weight: 3 }, { name: 'aliases', weight: 3 }, @@ -80,8 +87,13 @@ const { list, containerProps, wrapperProps, scrollTo } = useVirtualList( 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')) { + searchInput.value?.focus() + } +}) function setActiveIconName(name: string) { activeIconName.value = name; @@ -118,6 +130,7 @@ function handleCloseDrawer() { :placeholder="`Search ${icons.length} icons ...`" v-model="searchQuery" ref="searchInput" + :shortcut="kbdSearchShortcut" class="input-wrapper" @focus="onFocusSearchInput" /> diff --git a/docs/.vitepress/theme/utils/useSearchShortcut.ts b/docs/.vitepress/theme/utils/useSearchShortcut.ts new file mode 100644 index 000000000..192ce782f --- /dev/null +++ b/docs/.vitepress/theme/utils/useSearchShortcut.ts @@ -0,0 +1,35 @@ +import { ref, onMounted, onBeforeUnmount } from 'vue'; + +/** + * Composable for handling search keyboard shortcuts. + * Listens for Cmd/Ctrl+K and "/" keys to trigger a search action. + * + * @param callback - Function to execute when shortcut is triggered + * @returns Object containing the platform-specific shortcut display text + */ +export default function useSearchShortcut(callback: () => void) { + const shortcutText = ref(''); + + function handleKeydown(event: KeyboardEvent) { + // Check for Cmd+K (Mac), Ctrl+K (Windows/Linux), or forward slash + if (((event.metaKey || event.ctrlKey) && event.key === 'k') || event.key === '/') { + event.preventDefault(); + callback(); + } + } + + onMounted(() => { + // Detect platform and set appropriate keyboard shortcut for search focus + const isMac = /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform); + shortcutText.value = isMac ? '⌘K' : 'Ctrl+K'; + + // Add keyboard shortcut listener + window.addEventListener('keydown', handleKeydown); + }); + + onBeforeUnmount(() => { + window.removeEventListener('keydown', handleKeydown); + }); + + return { shortcutText }; +}