@@ -36,7 +35,7 @@ export const Navbar = async () => {
-
+
diff --git a/website/src/content/pages/components/date-input.mdx b/website/src/content/pages/components/date-input.mdx
index 988bae1c89..82ff65ffc3 100644
--- a/website/src/content/pages/components/date-input.mdx
+++ b/website/src/content/pages/components/date-input.mdx
@@ -2,7 +2,6 @@
id: date-input
title: Date Input
description: A segment-based date input that allows users to enter dates by navigating individual date parts.
-status: Preview
---
diff --git a/website/src/content/pages/components/tree-view.mdx b/website/src/content/pages/components/tree-view.mdx
index bacde260cf..139794acc5 100644
--- a/website/src/content/pages/components/tree-view.mdx
+++ b/website/src/content/pages/components/tree-view.mdx
@@ -1,7 +1,6 @@
---
id: tree-view
title: Tree View
-status: preview
description: A component that is used to show a tree hierarchy.
---
diff --git a/website/src/lib/docs.ts b/website/src/lib/docs.ts
new file mode 100644
index 0000000000..7b527c04a5
--- /dev/null
+++ b/website/src/lib/docs.ts
@@ -0,0 +1,73 @@
+import { matchSorter } from 'match-sorter'
+import type { Pages } from '.velite'
+import { getSidebarGroupsWithPages } from './sidebar'
+
+export interface DocsEntry {
+ slug: string
+ title: string
+ description: string
+ category: string
+ url: string
+}
+
+export interface DocsPage extends DocsEntry {
+ content: string
+}
+
+interface DocsPageSource {
+ page: Pages
+ category: string
+}
+
+const getDocsSources = (): DocsPageSource[] =>
+ getSidebarGroupsWithPages().flatMap((group) =>
+ group.items
+ .filter((page) => !!page.llm)
+ .map((page) => ({
+ page,
+ category: group.title,
+ })),
+ )
+
+const toEntry = ({ page, category }: DocsPageSource): DocsEntry => ({
+ slug: page.slug,
+ title: page.title,
+ description: page.description,
+ category,
+ url: `https://ark-ui.com/docs/${page.slug}`,
+})
+
+export const formatDocContent = (page: Pages) => `# ${page.title}
+
+URL: https://ark-ui.com/docs/${page.slug}
+LLM: https://ark-ui.com/llms.txt/${page.slug}
+
+${page.description || ''}
+
+---
+
+${page.llm}`
+
+export const listDocs = (): DocsEntry[] => getDocsSources().map(toEntry)
+
+export const getDoc = (slug: string): DocsPage | null => {
+ const normalized = slug.replace(/\.mdx$/, '')
+ const source = getDocsSources().find(({ page }) => page.slug === normalized)
+ if (!source) return null
+
+ return {
+ ...toEntry(source),
+ content: formatDocContent(source.page),
+ }
+}
+
+export const searchDocs = (query: string, limit = 10): DocsEntry[] => {
+ const value = query.trim()
+ const docs = listDocs()
+
+ if (!value) return docs.slice(0, limit)
+
+ return matchSorter(docs, value, {
+ keys: ['title', 'description', 'slug', 'category'],
+ }).slice(0, limit)
+}
diff --git a/website/src/lib/search-query.ts b/website/src/lib/search-query.ts
new file mode 100644
index 0000000000..82802aaf63
--- /dev/null
+++ b/website/src/lib/search-query.ts
@@ -0,0 +1,91 @@
+import { matchSorter } from 'match-sorter'
+
+export interface SearchItem {
+ label: string
+ value: string
+ group: string
+ description?: string
+ parents?: string[]
+}
+
+export interface SearchData {
+ defaults: Record
+ items: SearchItem[]
+}
+
+const GROUP_ORDER = ['Overview', 'Guides', 'Components', 'Sections', 'Examples', 'Utilities', 'Collections', 'AI']
+
+export const SEARCH_MAX_PER_GROUP = 5
+
+export const groupSearchResults = (
+ items: SearchItem[],
+ maxPerGroup = Number.POSITIVE_INFINITY,
+): Record => {
+ const grouped: Record = {}
+ for (const item of items) {
+ grouped[item.group] ??= []
+ if (grouped[item.group].length >= maxPerGroup) continue
+ grouped[item.group].push(item)
+ }
+
+ return Object.fromEntries(
+ GROUP_ORDER.filter((group) => grouped[group]?.length).map((group) => [group, grouped[group]]),
+ )
+}
+
+export const querySearch = (data: SearchData, query: string): Record => {
+ const value = query.trim()
+ if (!value) return data.defaults
+
+ const results = matchSorter(data.items, value, {
+ keys: ['label', 'description', (item) => item.parents?.join(' ') ?? ''],
+ })
+
+ return groupSearchResults(results, SEARCH_MAX_PER_GROUP)
+}
+
+const fetchSearchData = async (): Promise => {
+ const response = await fetch('/api/search')
+ if (!response.ok) throw new Error('Failed to load search data')
+ return response.json() as Promise
+}
+
+let searchDataPromise: Promise | null = null
+
+export const loadSearchData = () => {
+ searchDataPromise ??= retry(fetchSearchData).catch((error) => {
+ searchDataPromise = null
+ throw error
+ })
+
+ return searchDataPromise
+}
+
+export const preloadSearchData = () => {
+ void loadSearchData().catch(() => {})
+}
+
+const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
+
+interface RetryOptions {
+ attempts?: number
+ delayMs?: number
+}
+
+const retry = async (fn: () => Promise, opts: RetryOptions = {}): Promise => {
+ const { attempts = 3, delayMs = 300 } = opts
+
+ let lastError: unknown
+
+ for (let attempt = 0; attempt < attempts; attempt++) {
+ try {
+ return await fn()
+ } catch (error) {
+ lastError = error
+ if (attempt === attempts - 1) break
+ await wait(delayMs * 2 ** attempt)
+ }
+ }
+
+ throw lastError
+}
diff --git a/website/src/lib/search.ts b/website/src/lib/search.ts
index 651352374d..1c87365626 100644
--- a/website/src/lib/search.ts
+++ b/website/src/lib/search.ts
@@ -1,24 +1,91 @@
import { fetchExamplesGroupedByCategory } from './examples'
+import type { SearchItem } from './search-query'
import { pages } from '.velite'
-const components = pages
- .filter((page) => page.category === 'components')
+export type { SearchData, SearchItem } from './search-query'
+
+type TocEntry = {
+ title: string
+ url: string
+ items: TocEntry[]
+}
+
+const categoryToGroup: Record = {
+ overview: 'Overview',
+ guides: 'Guides',
+ components: 'Components',
+ utilities: 'Utilities',
+ collections: 'Collections',
+ ai: 'AI',
+}
+
+const uniquePages = [...new Map(pages.map((page) => [page.slug, page])).values()].filter(
+ (page) => page.category !== 'license.mdx',
+)
+
+const flattenToc = (entries: TocEntry[], pageTitle: string, pageSlug: string, parents: string[] = []): SearchItem[] =>
+ entries.flatMap((entry) => {
+ const item: SearchItem = {
+ label: entry.title,
+ value: `/docs/${pageSlug}${entry.url}`,
+ group: 'Sections',
+ parents: [pageTitle, ...parents],
+ description: pageTitle,
+ }
+ const nested = entry.items?.length ? flattenToc(entry.items, pageTitle, pageSlug, [...parents, entry.title]) : []
+ return [item, ...nested]
+ })
+
+const docsPages: SearchItem[] = uniquePages
+ .filter((page) => categoryToGroup[page.category])
.map((page) => ({
label: page.title,
value: `/docs/${page.slug}`,
+ group: categoryToGroup[page.category],
description: page.description,
- category: 'Component',
}))
-const examples = await fetchExamplesGroupedByCategory().then((groups) =>
+const sections: SearchItem[] = uniquePages
+ .filter((page) => categoryToGroup[page.category] && page.toc?.length)
+ .flatMap((page) => flattenToc(page.toc as TocEntry[], page.title, page.slug))
+
+const examples: SearchItem[] = await fetchExamplesGroupedByCategory().then((groups) =>
groups.flatMap((group) =>
group.items.map((item) => ({
label: item.title,
value: `/examples/${item.id}`,
+ group: 'Examples',
description: item.description,
- category: 'Example',
})),
),
)
-export const data = { components, examples }
+export const searchItems: SearchItem[] = [...docsPages, ...sections, ...examples]
+
+const popularComponentSlugs = [
+ 'components/accordion',
+ 'components/dialog',
+ 'components/combobox',
+ 'components/select',
+ 'components/menu',
+ 'components/date-picker',
+ 'components/toast',
+ 'components/tabs',
+]
+
+const bySlug = (slug: string) => docsPages.find((item) => item.value === `/docs/${slug}`)
+
+export const defaultGroups: Record = {
+ Overview: ['overview/getting-started', 'overview/about', 'overview/changelog']
+ .map(bySlug)
+ .filter((item): item is SearchItem => !!item),
+ Guides: ['guides/styling', 'guides/composition', 'guides/animation', 'guides/forms']
+ .map(bySlug)
+ .filter((item): item is SearchItem => !!item),
+ Components: popularComponentSlugs.map(bySlug).filter((item): item is SearchItem => !!item),
+}
+
+export const data = {
+ defaults: defaultGroups,
+ items: searchItems,
+}
diff --git a/website/src/theme/recipes/combobox.ts b/website/src/theme/recipes/combobox.ts
index ee00bb5580..84cb760598 100644
--- a/website/src/theme/recipes/combobox.ts
+++ b/website/src/theme/recipes/combobox.ts
@@ -53,9 +53,6 @@ export const combobox = defineSlotRecipe({
cursor: 'pointer',
display: 'flex',
justifyContent: 'space-between',
- transitionDuration: 'fast',
- transitionProperty: 'background, color',
- transitionTimingFunction: 'default',
_hover: {
background: 'bg.muted',
},