Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 26 additions & 24 deletions apps/www/src/components/docs/search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -36,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<string, typeof MagnifyingGlassIcon> = {
docs: ReaderIcon,
'getting-started': RocketIcon,
Expand All @@ -59,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<Record<string, boolean>>(
{}
);
Expand All @@ -86,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<Record<string, Item[]>>((acc, item) => {
const folder = getFolderFromUrl(item.url);
if (folder === 'components') return acc;
Expand All @@ -106,6 +107,7 @@ export default function DocsSearch({ pageTree }: { pageTree: Root }) {

const trimmedQuery = search.trim();
const isSearching = trimmedQuery.length > 0;
const isLoading = isSearching && query.isLoading;
const results =
isSearching && query.data && query.data !== 'empty' ? query.data : [];

Expand Down Expand Up @@ -152,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 =>
Expand All @@ -167,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' &&
Expand Down Expand Up @@ -229,12 +227,16 @@ export default function DocsSearch({ pageTree }: { pageTree: Root }) {
</div>
<Command.Content className={styles.searchList}>
<Command.Empty className={styles.searchEmpty}>
<EmptyState
variant='empty1'
heading='No result found'
subHeading='The keyword you’re searching for isn’t in the document—try using a different term.'
icon={<ExclamationTriangleIcon />}
/>
{isLoading ? (
<Spinner size={5} aria-label='Searching docs' />
) : (
<EmptyState
variant='empty1'
heading='No result found'
subHeading='The keyword you’re searching for isn’t in the document—try using a different term.'
icon={<ExclamationTriangleIcon />}
/>
)}
</Command.Empty>
{items.map((section, index) => (
<Fragment key={section.heading}>
Expand Down
Loading