feat: integration search bar
This commit is contained in:
1 parent
a8fea8d900
commit
25d9c30b8a
22 files changed
+2558
-41
No files matched your search
@@ -0,0 +1,55 @@
|
||||
<script setup lang="ts">
|
||||
import { t } from '@/i18n'
|
||||
|
||||
import { useMediaQuery } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
import ApiSearchPanel from '@/components/ApiSearchPanel.vue'
|
||||
import type { SearchEntry, SearchIndex } from '@/lib/apiSearch'
|
||||
|
||||
defineProps<{
|
||||
index: SearchIndex
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{ select: [entry: SearchEntry] }>()
|
||||
|
||||
const DESKTOP_QUERY = '(min-width: 64rem)'
|
||||
|
||||
const open = defineModel<boolean>('open', { required: true })
|
||||
const isDesktop = useMediaQuery(DESKTOP_QUERY)
|
||||
|
||||
const contentClass = computed(() =>
|
||||
isDesktop.value
|
||||
? 'divide-y-0 rounded-none border border-default shadow-(--shadow-modal) ring-0 sm:top-24 sm:max-w-180 sm:translate-y-0'
|
||||
: 'divide-y-0 rounded-none',
|
||||
)
|
||||
|
||||
function keepFocusWithCaller(event: Event) {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
function onSelect(entry: SearchEntry) {
|
||||
open.value = false
|
||||
emit('select', entry)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UModal
|
||||
v-model:open="open"
|
||||
:title="t('Search the API reference')"
|
||||
:description="t('Search the API reference…')"
|
||||
:fullscreen="!isDesktop"
|
||||
:close="false"
|
||||
:content="{ onCloseAutoFocus: keepFocusWithCaller }"
|
||||
:ui="{ content: contentClass }"
|
||||
>
|
||||
<template #content>
|
||||
<ApiSearchPanel
|
||||
:index="index"
|
||||
:mobile="!isDesktop"
|
||||
@select="onSelect"
|
||||
@close="open = false"
|
||||
/>
|
||||
</template>
|
||||
</UModal>
|
||||
</template>
|
||||
@@ -0,0 +1,250 @@
|
||||
<script setup lang="ts">
|
||||
import { t } from '@/i18n'
|
||||
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||
import ApiSearchRow from '@/components/ApiSearchRow.vue'
|
||||
import {
|
||||
ENDPOINT_CAP,
|
||||
queryTerms,
|
||||
searchIndex,
|
||||
suggestedHits,
|
||||
type SearchEntry,
|
||||
type SearchHit,
|
||||
type SearchIndex,
|
||||
} from '@/lib/apiSearch'
|
||||
|
||||
const props = defineProps<{
|
||||
index: SearchIndex
|
||||
mobile: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{ select: [entry: SearchEntry]; close: [] }>()
|
||||
|
||||
interface PanelRow {
|
||||
id: string
|
||||
hit: SearchHit
|
||||
}
|
||||
|
||||
interface PanelGroup {
|
||||
id: string
|
||||
title: string
|
||||
rows: PanelRow[]
|
||||
}
|
||||
|
||||
const LIST_ID = 'rc-sr-list'
|
||||
|
||||
const root = ref<HTMLElement | null>(null)
|
||||
const query = ref('')
|
||||
const position = ref(0)
|
||||
|
||||
const terms = computed(() => queryTerms(query.value))
|
||||
const suggesting = computed(() => terms.value.length === 0)
|
||||
const result = computed(() => searchIndex(props.index, query.value))
|
||||
const shownEndpoints = computed(() => result.value.endpoints.slice(0, ENDPOINT_CAP))
|
||||
const hiddenEndpoints = computed(() => result.value.endpoints.length - shownEndpoints.value.length)
|
||||
|
||||
const hitGroups = computed(() => {
|
||||
if (suggesting.value) {
|
||||
return [{ title: t('Suggested'), hits: suggestedHits(props.index) }]
|
||||
}
|
||||
return [
|
||||
{ title: t('Endpoints'), hits: shownEndpoints.value },
|
||||
{ title: t('Sections'), hits: result.value.sections },
|
||||
].filter((group) => group.hits.length > 0)
|
||||
})
|
||||
|
||||
const groups = computed<PanelGroup[]>(() => {
|
||||
let next = 0
|
||||
return hitGroups.value.map((group, groupIndex) => ({
|
||||
id: `rc-sr-g${groupIndex}`,
|
||||
title: group.title,
|
||||
rows: group.hits.map((hit) => ({ id: `rc-sr-${next++}`, hit })),
|
||||
}))
|
||||
})
|
||||
|
||||
const rows = computed(() => groups.value.flatMap((group) => group.rows))
|
||||
const selected = computed(() => Math.min(position.value, Math.max(rows.value.length - 1, 0)))
|
||||
const activeId = computed(() => rows.value[selected.value]?.id)
|
||||
const empty = computed(() => !suggesting.value && rows.value.length === 0)
|
||||
|
||||
const total = computed(() =>
|
||||
suggesting.value
|
||||
? rows.value.length
|
||||
: result.value.endpoints.length + result.value.sections.length,
|
||||
)
|
||||
|
||||
const countText = computed(() => {
|
||||
if (suggesting.value) {
|
||||
return t('{0} suggestions', [total.value])
|
||||
}
|
||||
return total.value === 1 ? t('{0} result', [1]) : t('{0} results', [total.value])
|
||||
})
|
||||
|
||||
const liveText = computed(() => {
|
||||
if (empty.value) {
|
||||
return t('No results')
|
||||
}
|
||||
return hiddenEndpoints.value > 0
|
||||
? t('{0} results, showing {1}', [total.value, rows.value.length])
|
||||
: countText.value
|
||||
})
|
||||
|
||||
const moreText = computed(() =>
|
||||
hiddenEndpoints.value === 1
|
||||
? t('{0} more endpoint. Keep typing to narrow the list.', [1])
|
||||
: t('{0} more endpoints. Keep typing to narrow the list.', [hiddenEndpoints.value]),
|
||||
)
|
||||
|
||||
const emptyText = computed(() =>
|
||||
t('No match for "{0}". Try a path, a method or a status code like 404.', [query.value.trim()]),
|
||||
)
|
||||
|
||||
watch(query, () => {
|
||||
position.value = 0
|
||||
})
|
||||
|
||||
watch(selected, async () => {
|
||||
await nextTick()
|
||||
document.getElementById(activeId.value ?? '')?.scrollIntoView?.({ block: 'nearest' })
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick()
|
||||
root.value?.querySelector('input')?.focus()
|
||||
})
|
||||
|
||||
function choose(row: PanelRow) {
|
||||
emit('select', row.hit.entry)
|
||||
}
|
||||
|
||||
function move(target: number) {
|
||||
const count = rows.value.length
|
||||
position.value = count === 0 ? 0 : (target + count) % count
|
||||
}
|
||||
|
||||
function onKeydown(event: KeyboardEvent) {
|
||||
if (event.isComposing) {
|
||||
return
|
||||
}
|
||||
const handlers: Record<string, () => void> = {
|
||||
ArrowDown: () => move(selected.value + 1),
|
||||
ArrowUp: () => move(selected.value - 1),
|
||||
Home: () => move(0),
|
||||
End: () => move(rows.value.length - 1),
|
||||
Enter: () => {
|
||||
const row = rows.value[selected.value]
|
||||
if (row) {
|
||||
choose(row)
|
||||
}
|
||||
},
|
||||
}
|
||||
const handler = handlers[event.key]
|
||||
if (handler) {
|
||||
event.preventDefault()
|
||||
handler()
|
||||
return
|
||||
}
|
||||
if (event.key === 'Escape') {
|
||||
emit('close')
|
||||
}
|
||||
}
|
||||
|
||||
const KBD_CLASS =
|
||||
'h-5 min-w-5 justify-center rounded-none border border-accented bg-default px-1 font-mono normal-case text-xs font-medium text-toned ring-0'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="root" class="flex h-full min-h-0 flex-col text-sm text-default">
|
||||
<div
|
||||
class="flex shrink-0 items-center gap-2.5 border-b border-default px-4 ring-2 ring-primary ring-inset"
|
||||
:class="mobile ? 'h-14' : 'h-13'"
|
||||
>
|
||||
<UInput
|
||||
v-model="query"
|
||||
variant="none"
|
||||
icon="i-lucide-search"
|
||||
:placeholder="t('Search the API reference…')"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
role="combobox"
|
||||
aria-expanded="true"
|
||||
:aria-controls="LIST_ID"
|
||||
aria-autocomplete="list"
|
||||
:aria-activedescendant="activeId"
|
||||
:aria-label="t('Search the API reference')"
|
||||
:ui="{
|
||||
root: 'min-w-0 flex-1',
|
||||
base: 'h-full px-0 ps-7 text-lg text-highlighted lg:text-base',
|
||||
leading: 'ps-0',
|
||||
leadingIcon: 'size-4.5 text-muted',
|
||||
}"
|
||||
class="h-full"
|
||||
@keydown="onKeydown"
|
||||
/>
|
||||
<UKbd v-if="!mobile" value="Esc" :class="KBD_CLASS" />
|
||||
<UButton
|
||||
v-else
|
||||
:label="t('Cancel')"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
class="-mr-2 h-11 rounded-none px-2 text-base font-medium text-highlighted"
|
||||
@click="emit('close')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<span role="status" aria-live="polite" class="sr-only">{{ liveText }}</span>
|
||||
|
||||
<div
|
||||
:id="LIST_ID"
|
||||
role="listbox"
|
||||
:aria-label="t('Search results')"
|
||||
class="min-h-0 flex-1 overflow-y-auto py-1 lg:max-h-110"
|
||||
>
|
||||
<div v-if="empty" class="flex flex-col items-center gap-2.5 px-6 py-10 text-center">
|
||||
<UIcon name="i-lucide-search-x" class="size-5 text-muted" />
|
||||
<span class="max-w-90 text-base/5.5 text-pretty text-toned">{{ emptyText }}</span>
|
||||
</div>
|
||||
<div v-for="group in groups" :key="group.id" role="group" :aria-labelledby="group.id">
|
||||
<div
|
||||
:id="group.id"
|
||||
class="px-4 pt-2 pb-1 text-xs/5 font-medium tracking-wide text-muted uppercase"
|
||||
>
|
||||
{{ group.title }}
|
||||
</div>
|
||||
<ApiSearchRow
|
||||
v-for="row in group.rows"
|
||||
:id="row.id"
|
||||
:key="row.id"
|
||||
:hit="row.hit"
|
||||
:terms="terms"
|
||||
:selected="row.id === activeId"
|
||||
:mobile="mobile"
|
||||
@choose="choose(row)"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="hiddenEndpoints > 0" class="px-4 py-2 text-muted">{{ moreText }}</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex h-10 shrink-0 items-center gap-4 border-t border-default bg-muted px-4 text-muted"
|
||||
>
|
||||
<template v-if="!mobile">
|
||||
<span aria-hidden="true" class="inline-flex items-center gap-1.5">
|
||||
<UKbd value="↑" :class="KBD_CLASS" />
|
||||
<UKbd value="↓" :class="KBD_CLASS" />
|
||||
{{ t('Move') }}
|
||||
</span>
|
||||
<span aria-hidden="true" class="inline-flex items-center gap-1.5">
|
||||
<UKbd value="↵" :class="KBD_CLASS" />
|
||||
{{ t('Open') }}
|
||||
</span>
|
||||
<span aria-hidden="true" class="inline-flex items-center gap-1.5">
|
||||
<UKbd value="Esc" :class="KBD_CLASS" />
|
||||
{{ t('Close') }}
|
||||
</span>
|
||||
</template>
|
||||
<span class="flex-1" />
|
||||
<span aria-hidden="true">{{ countText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,132 @@
|
||||
<script setup lang="ts">
|
||||
import { t } from '@/i18n'
|
||||
|
||||
import { computed } from 'vue'
|
||||
import HighlightedText from '@/components/HighlightedText.vue'
|
||||
import type { SearchHit } from '@/lib/apiSearch'
|
||||
import { CHIP_CLASS, METHOD_CLASS, SESSION_METHOD_CLASS } from '@/lib/apiReference'
|
||||
|
||||
const props = defineProps<{
|
||||
id: string
|
||||
hit: SearchHit
|
||||
terms: string[]
|
||||
selected: boolean
|
||||
mobile: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{ choose: [] }>()
|
||||
|
||||
const endpoint = computed(() => (props.hit.entry.kind === 'endpoint' ? props.hit.entry : null))
|
||||
const section = computed(() => (props.hit.entry.kind === 'section' ? props.hit.entry : null))
|
||||
|
||||
const optionLabel = computed(() =>
|
||||
endpoint.value
|
||||
? `${endpoint.value.method} ${endpoint.value.path}. ${endpoint.value.description}. ${endpoint.value.access}`
|
||||
: t('Section {0}. {1}', [section.value!.title, section.value!.description]),
|
||||
)
|
||||
|
||||
const viaLabel = computed(() => {
|
||||
const kind = props.hit.via?.kind
|
||||
if (kind === 'query') {
|
||||
return t('Query parameter')
|
||||
}
|
||||
return kind === 'body' ? t('Body field') : t('Error')
|
||||
})
|
||||
|
||||
const descriptionClass = computed(() => (endpoint.value?.session ? 'text-muted' : 'text-toned'))
|
||||
const pathClass = computed(() =>
|
||||
endpoint.value?.session ? 'font-normal text-toned' : 'font-medium text-highlighted',
|
||||
)
|
||||
const chipClass = computed(() => (endpoint.value?.session ? CHIP_CLASS.dashed : CHIP_CLASS.neutral))
|
||||
const methodClass = computed(() =>
|
||||
endpoint.value?.session ? SESSION_METHOD_CLASS : METHOD_CLASS[endpoint.value!.method],
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:id="id"
|
||||
role="option"
|
||||
:aria-selected="selected"
|
||||
:aria-label="optionLabel"
|
||||
class="grid cursor-pointer items-start gap-3 px-4 hover:bg-elevated"
|
||||
:class="[
|
||||
mobile
|
||||
? 'min-h-14 grid-cols-[3.5rem_minmax(0,1fr)] py-2.5'
|
||||
: 'min-h-10 grid-cols-[3.5rem_minmax(0,1fr)_auto] py-2',
|
||||
selected ? 'search-row-selected bg-elevated' : '',
|
||||
]"
|
||||
@click="emit('choose')"
|
||||
>
|
||||
<UBadge
|
||||
v-if="endpoint"
|
||||
:label="endpoint.method"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
:class="methodClass"
|
||||
/>
|
||||
<span v-else class="flex h-5.5 w-14 items-center justify-center text-muted">
|
||||
<UIcon :name="section!.locked ? 'i-lucide-lock' : 'i-lucide-hash'" class="size-4" />
|
||||
</span>
|
||||
|
||||
<div class="flex min-w-0 flex-col gap-0.5">
|
||||
<div v-if="endpoint" class="flex min-h-5.5 min-w-0 items-center gap-3">
|
||||
<span
|
||||
class="min-w-0 truncate font-mono text-sm"
|
||||
:class="[pathClass, mobile ? '' : 'shrink-0']"
|
||||
><HighlightedText :text="endpoint.path" :terms="terms"
|
||||
/></span>
|
||||
<span v-if="!mobile" class="min-w-0 flex-1 truncate" :class="descriptionClass"
|
||||
><HighlightedText :text="endpoint.description" :terms="terms"
|
||||
/></span>
|
||||
</div>
|
||||
<div v-else class="flex min-h-5.5 min-w-0 items-center gap-3">
|
||||
<span class="shrink-0 font-medium text-highlighted"
|
||||
><HighlightedText :text="section!.title" :terms="terms"
|
||||
/></span>
|
||||
<span v-if="!mobile" class="min-w-0 flex-1 truncate text-muted"
|
||||
><HighlightedText :text="section!.description" :terms="terms"
|
||||
/></span>
|
||||
</div>
|
||||
<div v-if="mobile" class="flex min-w-0 items-center gap-2">
|
||||
<UBadge
|
||||
v-if="endpoint"
|
||||
:label="endpoint.access"
|
||||
:icon="endpoint.session ? 'i-lucide-lock' : undefined"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
:class="chipClass"
|
||||
:ui="{ leadingIcon: 'size-3' }"
|
||||
/>
|
||||
<span class="min-w-0 flex-1 truncate" :class="descriptionClass"
|
||||
><HighlightedText
|
||||
:text="endpoint ? endpoint.description : section!.description"
|
||||
:terms="terms"
|
||||
/></span>
|
||||
</div>
|
||||
<div v-if="hit.via" class="flex min-w-0 items-baseline gap-2 text-xs/4.5 text-muted">
|
||||
<span class="shrink-0">{{ viaLabel }}</span>
|
||||
<span class="min-w-0 truncate font-mono text-toned"
|
||||
><HighlightedText :text="hit.via.text" :terms="terms"
|
||||
/></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UBadge
|
||||
v-if="endpoint && !mobile"
|
||||
:label="endpoint.access"
|
||||
:icon="endpoint.session ? 'i-lucide-lock' : undefined"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
class="mt-px"
|
||||
:class="chipClass"
|
||||
:ui="{ leadingIcon: 'size-3' }"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.search-row-selected {
|
||||
box-shadow: inset 2px 0 0 var(--ui-primary);
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { t, translate } from '@/i18n'
|
||||
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import ApiFieldTable from '@/components/ApiFieldTable.vue'
|
||||
import CodeSnippet from '@/components/CodeSnippet.vue'
|
||||
import { RATE_LIMIT_SENTENCE, endpointExamples, type ApiEndpoint } from '@/data/apiCatalogue'
|
||||
@@ -10,9 +10,19 @@ import { CHIP_CLASS, METHOD_CLASS, endpointAnchor } from '@/lib/apiReference'
|
||||
const props = defineProps<{
|
||||
endpoint: ApiEndpoint
|
||||
origin: string
|
||||
flash?: 'animated' | 'static' | null
|
||||
}>()
|
||||
|
||||
const open = ref(false)
|
||||
const rowButton = ref<HTMLButtonElement | null>(null)
|
||||
|
||||
async function reveal() {
|
||||
open.value = true
|
||||
await nextTick()
|
||||
rowButton.value?.focus({ preventScroll: true })
|
||||
}
|
||||
|
||||
defineExpose({ reveal })
|
||||
const activeTab = ref('fetch')
|
||||
|
||||
const examples = computed(() => endpointExamples(props.endpoint, props.origin))
|
||||
@@ -42,10 +52,15 @@ const snippetTabs = computed(() => [
|
||||
:ui="{ content: 'border-t border-muted' }"
|
||||
>
|
||||
<button
|
||||
ref="rowButton"
|
||||
type="button"
|
||||
:aria-label="rowLabel"
|
||||
class="flex min-h-11 w-full cursor-pointer flex-col items-stretch gap-1.5 px-3 py-3 text-left focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary lg:grid lg:grid-cols-[300px_minmax(0,1fr)_auto_16px] lg:items-center lg:gap-4 lg:px-4 lg:py-0"
|
||||
:class="open ? 'bg-muted' : ''"
|
||||
:class="[
|
||||
open ? 'bg-muted' : '',
|
||||
flash === 'animated' ? 'endpoint-flash' : '',
|
||||
flash === 'static' ? 'endpoint-flash-static' : '',
|
||||
]"
|
||||
>
|
||||
<span class="flex min-w-0 items-center gap-2.5">
|
||||
<UBadge
|
||||
@@ -134,3 +149,27 @@ const snippetTabs = computed(() => [
|
||||
</template>
|
||||
</UCollapsible>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.endpoint-flash,
|
||||
.endpoint-flash-static {
|
||||
background: var(--rc-sand);
|
||||
box-shadow: inset 0 0 0 2px var(--ui-primary);
|
||||
}
|
||||
|
||||
.endpoint-flash {
|
||||
animation: endpoint-flash-fade 1.5s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes endpoint-flash-fade {
|
||||
0%,
|
||||
60% {
|
||||
background: var(--rc-sand);
|
||||
box-shadow: inset 0 0 0 2px var(--ui-primary);
|
||||
}
|
||||
100% {
|
||||
background: var(--ui-bg-muted);
|
||||
box-shadow: inset 0 0 0 2px transparent;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { highlightSegments } from '@/lib/apiSearch'
|
||||
|
||||
const props = defineProps<{
|
||||
text: string
|
||||
terms: string[]
|
||||
}>()
|
||||
|
||||
const segments = computed(() => highlightSegments(props.text, props.terms))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-for="(segment, position) in segments" :key="position">
|
||||
<mark v-if="segment.marked" class="bg-(--rc-sand) text-(--rc-on-sand)">{{
|
||||
segment.text
|
||||
}}</mark>
|
||||
<template v-else>{{ segment.text }}</template>
|
||||
</template>
|
||||
</template>
|
||||
@@ -13,7 +13,7 @@ const props = defineProps<{
|
||||
active: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{ select: [id: string] }>()
|
||||
const emit = defineEmits<{ select: [id: string]; search: [trigger: HTMLElement | null] }>()
|
||||
|
||||
const activeLabel = computed(
|
||||
() => props.sections.find((section) => section.id === props.active)?.label ?? '',
|
||||
@@ -22,6 +22,10 @@ const activeLabel = computed(
|
||||
function onJump(id: string) {
|
||||
emit('select', id)
|
||||
}
|
||||
|
||||
function onSearch(event: MouseEvent) {
|
||||
emit('search', event.currentTarget as HTMLElement | null)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -51,7 +55,7 @@ function onJump(id: string) {
|
||||
</nav>
|
||||
|
||||
<div
|
||||
class="sticky -top-4 z-5 order-first -mx-4 -mt-4 mb-4 border-b border-default bg-default px-4 py-2 lg:hidden"
|
||||
class="sticky -top-4 z-5 order-first -mx-4 -mt-4 mb-4 flex items-center gap-2 border-b border-default bg-default px-4 py-2 lg:hidden"
|
||||
>
|
||||
<USelectMenu
|
||||
:model-value="active"
|
||||
@@ -63,7 +67,7 @@ function onJump(id: string) {
|
||||
:aria-label="t('Jump to section')"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
class="w-full"
|
||||
class="min-w-0 flex-1"
|
||||
:content="{ sideOffset: 4 }"
|
||||
:ui="{
|
||||
base: 'h-11 rounded-none border border-accented bg-default px-3 text-base ring-0 hover:bg-default',
|
||||
@@ -80,5 +84,15 @@ function onJump(id: string) {
|
||||
}}</span>
|
||||
</span>
|
||||
</USelectMenu>
|
||||
<UButton
|
||||
icon="i-lucide-search"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
:aria-label="t('Search the API reference')"
|
||||
aria-haspopup="dialog"
|
||||
class="size-11 shrink-0 justify-center rounded-none border border-accented bg-default text-highlighted ring-0 hover:bg-default"
|
||||
:ui="{ leadingIcon: 'size-4.5' }"
|
||||
@click="onSearch"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
Reference in new issue
Block a user