From c98eceeb9c88e4c95a9ecef4a26671558665fc18 Mon Sep 17 00:00:00 2001 From: Shreyag02 Date: Tue, 9 Jun 2026 12:56:51 +0530 Subject: [PATCH 1/2] feat(spotlight-search): added spinner for loading state during loading state in spotlight search --- .../www/src/components/docs/search.module.css | 3 +- apps/www/src/components/docs/search.tsx | 34 ++++++++++++++----- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/apps/www/src/components/docs/search.module.css b/apps/www/src/components/docs/search.module.css index 4b44bd1e0..e3ae7ec8b 100644 --- a/apps/www/src/components/docs/search.module.css +++ b/apps/www/src/components/docs/search.module.css @@ -19,7 +19,8 @@ max-height: 440px; } -.searchEmpty { +.searchEmpty, +.searchLoading { display: flex; align-items: center; justify-content: center; diff --git a/apps/www/src/components/docs/search.tsx b/apps/www/src/components/docs/search.tsx index 31755b7ab..5f7a32c85 100644 --- a/apps/www/src/components/docs/search.tsx +++ b/apps/www/src/components/docs/search.tsx @@ -15,7 +15,13 @@ import { SpaceBetweenVerticallyIcon, TextIcon } from '@radix-ui/react-icons'; -import { Command, Dialog, EmptyState, IconButton } from '@raystack/apsara'; +import { + Command, + Dialog, + EmptyState, + IconButton, + Spinner +} from '@raystack/apsara'; import { flattenTree, type Item as PageItem, @@ -106,6 +112,10 @@ export default function DocsSearch({ pageTree }: { pageTree: Root }) { const trimmedQuery = search.trim(); const isSearching = trimmedQuery.length > 0; + // While fumadocs debounces + fetches, `query.data` is undefined. Without + // this flag the empty state would flash "No result found" for the duration + // of the request; instead we show a spinner until the fetch settles. + const isLoading = isSearching && query.isLoading; const results = isSearching && query.data && query.data !== 'empty' ? query.data : []; @@ -228,14 +238,20 @@ export default function DocsSearch({ pageTree }: { pageTree: Root }) { )} - - } - /> - + {isLoading ? ( +
+ +
+ ) : ( + + } + /> + + )} {items.map((section, index) => ( From 9a7b31ffc2e0a52242d74cc3f96dba0e3f6b2c0f Mon Sep 17 00:00:00 2001 From: Shreyag02 Date: Tue, 9 Jun 2026 13:13:56 +0530 Subject: [PATCH 2/2] feat(spotlight-search): fixed comments --- .../www/src/components/docs/search.module.css | 3 +- apps/www/src/components/docs/search.tsx | 40 ++++++------------- 2 files changed, 14 insertions(+), 29 deletions(-) diff --git a/apps/www/src/components/docs/search.module.css b/apps/www/src/components/docs/search.module.css index e3ae7ec8b..4b44bd1e0 100644 --- a/apps/www/src/components/docs/search.module.css +++ b/apps/www/src/components/docs/search.module.css @@ -19,8 +19,7 @@ max-height: 440px; } -.searchEmpty, -.searchLoading { +.searchEmpty { display: flex; align-items: center; justify-content: center; diff --git a/apps/www/src/components/docs/search.tsx b/apps/www/src/components/docs/search.tsx index 5f7a32c85..5e77f0cea 100644 --- a/apps/www/src/components/docs/search.tsx +++ b/apps/www/src/components/docs/search.tsx @@ -42,10 +42,7 @@ type SearchItems = { items: Item[]; }; -// Docs pages carry no icon in their data, so map known page slugs to icons -// (overview + foundations pages get meaningful ones). Everything else — -// components and any future pages — falls back to a generic icon so every -// top-level row stays icon-aligned. +/* Map known page slugs to icons; everything else falls back to a generic one. */ const PAGE_ICONS: Record = { docs: ReaderIcon, 'getting-started': RocketIcon, @@ -65,8 +62,7 @@ const getPageIcon = (url: string): typeof MagnifyingGlassIcon => export default function DocsSearch({ pageTree }: { pageTree: Root }) { const router = useRouter(); const [open, setOpen] = useState(false); - // Per-result manual expand/collapse overrides; reset whenever the query - // changes so the auto-open rule re-applies for the fresh result set. + /* Manual expand/collapse overrides; reset on query change. */ const [openOverrides, setOpenOverrides] = useState>( {} ); @@ -92,9 +88,8 @@ export default function DocsSearch({ pageTree }: { pageTree: Root }) { const flattened = flattenTree(pageTree.children); if (!flattened.length) return []; - // Default view shows the overview + foundations (theme) pages grouped by - // folder. Components are intentionally excluded — there are dozens of them, - // so they only surface once the user actually searches. + /* Default view: overview + foundations grouped by folder; components are + excluded until the user searches. */ const items = flattened.reduce>((acc, item) => { const folder = getFolderFromUrl(item.url); if (folder === 'components') return acc; @@ -112,9 +107,6 @@ export default function DocsSearch({ pageTree }: { pageTree: Root }) { const trimmedQuery = search.trim(); const isSearching = trimmedQuery.length > 0; - // While fumadocs debounces + fetches, `query.data` is undefined. Without - // this flag the empty state would flash "No result found" for the duration - // of the request; instead we show a spinner until the fetch settles. const isLoading = isSearching && query.isLoading; const results = isSearching && query.data && query.data !== 'empty' ? query.data : []; @@ -162,10 +154,8 @@ export default function DocsSearch({ pageTree }: { pageTree: Root }) { const items = !isSearching ? defaultItems : searchResults; - // The `items` prop opts the Command out of its built-in per-item filtering - // and group-unwrapping: results are already filtered by fumadocs, and the - // grouped layout must stay intact while searching. An empty array is still - // truthy, so `hasItems` stays true even when there are no results. + /* The `items` prop opts Command out of built-in filtering/unwrapping — + results are pre-filtered by fumadocs and the grouped layout stays intact. */ const itemValues = useMemo( () => items.flatMap(section => @@ -177,10 +167,8 @@ export default function DocsSearch({ pageTree }: { pageTree: Root }) { [items] ); - // A result's sub-details collapse behaves like an accordion: open it only - // when the match came from a sub-detail rather than the page title (e.g. - // "Toolbar" surfacing for "button" via its Button section). When the title - // itself matches, the page is the hit, so keep its sub-details collapsed. + /* Auto-open sub-details only when the match came from a sub-detail, not the + page title; a title match keeps its sub-details collapsed. */ const queryLower = trimmedQuery.toLowerCase(); const titleMatches = (item: Item) => typeof item.name === 'string' && @@ -238,20 +226,18 @@ export default function DocsSearch({ pageTree }: { pageTree: Root }) { )} - {isLoading ? ( -
+ + {isLoading ? ( -
- ) : ( - + ) : ( } /> - - )} + )} + {items.map((section, index) => (