Skip to main content
Glama
SiSearch.vue8.1 kB
<template> <div :class=" clsx( 'siSearchRoot', variant === 'dropdownmenu' ? 'rounded-t-md' : borderBottom && 'dark:border-neutral-600 border-b', ) " > <label :class=" clsx( 'relative text-neutral-400 focus-within:text-neutral-600 block h-[34px]', variant === 'dropdownmenu' && 'rounded-t-md', ) " > <!-- data-1p-ignore is used to prevent 1password from targeting this field --> <input ref="searchInputRef" v-model="searchString" :placeholder="placeholder" :class=" clsx( 'w-full text-xs pl-[32px] py-2xs h-[34px] placeholder:italic', variant === 'standard' && 'outline-offset-[-1px] focus:outline focus:outline-2', { dropdownmenu: 'text-white bg-shade-100 placeholder:text-neutral-400 rounded-t-md focus:outline-none', standard: themeClasses( 'text-black bg-shade-0 placeholder:text-neutral-500 focus:bg-neutral-50 focus:outline-action-500', 'text-white bg-neutral-800 placeholder:text-neutral-400 focus:bg-shade-100 focus:outline-action-300', ), new: [ 'border outline-none focus:outline-none', themeClasses( 'text-black bg-shade-0 placeholder:text-neutral-500 border-neutral-400 focus:border-action-500', 'text-white bg-shade-100 placeholder:text-neutral-400 border-neutral-600 focus:border-action-300', ), ], }[variant], filtersEnabled ? 'pr-[58px]' : 'pr-[30px]', ) " data-lpignore="true" data-1p-ignore data-bwignore data-form-type="other" :tabindex="tabIndex" @input="onChange" @keydown="onKeyDown" @focus="onFocus" @blur="onBlur" /> <Icon name="search" :class=" clsx( 'absolute left-2xs top-[6px]', variant === 'dropdownmenu' ? 'text-neutral-600' : 'dark:text-neutral-600', ) " /> <div class="absolute right-0 top-0 h-[34px] flex flex-row items-center px-2xs" > <slot name="right" /> <Icon v-if="searchString" name="x-circle" class="text-neutral-400 dark:text-neutral-500 cursor-pointer hover:text-shade-100 hover:dark:text-shade-0" @click="clearSearch" /> <IconButton v-if="filtersEnabled && filters && filters.length > 0" icon="filter" :selected="showFilters" @click="toggleShowFilters" /> </div> </label> <Transition name="expand-height" enterActiveClass="transition-[height] overflow-hidden" leaveActiveClass="transition-[height] overflow-hidden" enterFromClass="!h-0" leaveToClass="!h-0" :onBeforeEnter="captureHeight" :onAfterEnter="clearHeight" :onBeforeLeave="captureHeight" :onAfterLeave="clearHeight" > <div v-show="showFilters" ref="transitionRef"> <div class="px-xs pt-xs pb-2xs flex flex-row flex-wrap gap-2xs select-none" > <FilterPill v-if="allFilter" :filter="allFilter" :selected="!filteringActive" @click="resetFilters" /> <FilterPill v-for="(filter, index) in filters" :key="index" :filter="filter" :selected="activeFilters[index]" @click="toggleFilter(index)" /> <div class="flex flex-row items-center text-sm text-neutral-500 dark:text-neutral-400 hover:underline cursor-pointer active:dark:text-action-300 active:text-action-500 active:font-bold group" @click="resetFilters" > <Icon name="x" size="sm" class="group-active:scale-125" /> <div>Reset</div> </div> </div> </div> </Transition> </div> </template> <script lang="ts" setup> import * as _ from "lodash-es"; import { computed, PropType, ref, watch } from "vue"; import clsx from "clsx"; import FilterPill from "./FilterPill.vue"; import Icon from "../icons/Icon.vue"; import IconButton from "./IconButton.vue"; import { Tones } from "../utils/color_utils"; import { IconNames } from "../icons/icon_set"; import { themeClasses } from "../utils/theme_tools"; const transitionRef = ref<HTMLDivElement>(); const searchInputRef = ref<HTMLInputElement>(); const captureHeight = () => { if (transitionRef.value) { if (transitionRef.value.style.display === "none") { transitionRef.value.style.removeProperty("display"); } transitionRef.value.style.height = `${transitionRef.value.clientHeight}px`; } }; const clearHeight = () => { if (transitionRef.value) { transitionRef.value.style.height = ""; } }; export type Filter = { name: string; iconColor?: string; iconTone?: Tones; iconName?: IconNames; count?: number; }; export type SearchVariant = "standard" | "dropdownmenu" | "new"; const emit = defineEmits<{ (e: "search", searchTerm: string): void; (e: "clearSearch"): void; (e: "update:modelValue", newValue: string): void; (e: "enterPressed"): void; (e: "blur", event: FocusEvent): void; (e: "focus", event: FocusEvent): void; (e: "input", event: Event): void; }>(); const props = defineProps({ placeholder: { type: String, default: "search" }, modelValue: { type: String }, filters: { type: Array<Filter> }, disableFilters: { type: Boolean }, variant: { type: String as PropType<SearchVariant>, default: "standard" }, borderBottom: { type: Boolean, default: true }, allFilter: { type: Object as PropType<Filter> }, tabIndex: { type: Number }, }); const filtersEnabled = computed(() => props.filters && !props.disableFilters); const showFilters = ref(false); const toggleShowFilters = () => { showFilters.value = !showFilters.value; if (!showFilters.value) resetFilters(); // if the filter panel is collapsed, reset all filters }; const activeFilters = ref( Array.from({ length: props.filters?.length || 0 }, () => false), ); const filteringActive = computed(() => activeFilters.value.some((filter) => filter), ); const resetFilters = () => { for (let i = 0; i < activeFilters.value.length; i++) { activeFilters.value[i] = false; } }; const toggleFilter = (index: number) => { if (index < 0 || index >= activeFilters.value.length) return; activeFilters.value[index] = !activeFilters.value[index]; if (filteringActive.value) { emit("search", searchString.value || ""); } }; const searchString = ref(props.modelValue) || ""; watch( () => props.modelValue, () => { searchString.value = props.modelValue; }, ); function triggerSearch() { emit("search", searchString.value || ""); } function clearSearch() { searchString.value = ""; emit("clearSearch"); } const debouncedAutoSearch = _.debounce(triggerAutoSearch, 50); watch(searchString, () => { emit("update:modelValue", searchString.value || ""); debouncedAutoSearch(); }); function triggerAutoSearch() { triggerSearch(); } function onKeyDown(e: KeyboardEvent) { if (e.key === "Enter") { triggerSearch(); emit("enterPressed"); } if (e.key === "Escape" && searchInputRef.value) { searchInputRef.value.blur(); } } const focusSearch = () => { if (searchInputRef.value) { searchInputRef.value.focus(); } }; const blurSearch = () => { if (searchInputRef.value) { searchInputRef.value.blur(); } }; const inputDOMEl = computed(() => searchInputRef.value); const onBlur = (e: FocusEvent) => { emit("blur", e); }; const onFocus = (e: FocusEvent) => { emit("focus", e); }; const onChange = (e: Event) => { emit("input", e); }; defineExpose({ filteringActive, activeFilters, clearSearch, focusSearch, blurSearch, inputDOMEl, }); </script>

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/systeminit/si'

If you have feedback or need assistance with the MCP directory API, please join our Discord server