mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
feat(ui): type-safe provider props
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'fumadocs-ui': patch
|
||||
'@fumadocs/base-ui': patch
|
||||
---
|
||||
|
||||
type-safe provider props
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useDocsSearch } from 'fumadocs-core/search/client';
|
||||
import { SearchClient, useDocsSearch } from 'fumadocs-core/search/client';
|
||||
import { fetchClient } from 'fumadocs-core/search/client/fetch';
|
||||
import { type ReactNode, useMemo, useState } from 'react';
|
||||
import { type ReactNode, use, useMemo, useState } from 'react';
|
||||
import { useOnChange } from 'fumadocs-core/utils/use-on-change';
|
||||
import { useI18n } from '@/contexts/i18n';
|
||||
import {
|
||||
@@ -23,6 +23,9 @@ import type { SortedResult } from 'fumadocs-core/search';
|
||||
import type { SearchLink, TagItem } from '@/contexts/search';
|
||||
|
||||
export interface DefaultSearchDialogProps extends SharedProps {
|
||||
/** @deprecated re-create the dialog instead for other clients, see https://fumadocs.dev/docs/search/orama */
|
||||
type?: 'static';
|
||||
|
||||
links?: SearchLink[];
|
||||
|
||||
defaultTag?: string;
|
||||
@@ -48,7 +51,10 @@ export interface DefaultSearchDialogProps extends SharedProps {
|
||||
allowClear?: boolean;
|
||||
}
|
||||
|
||||
let STATIC: Promise<typeof import('fumadocs-core/search/client/orama-static')> | undefined;
|
||||
|
||||
export default function DefaultSearchDialog({
|
||||
type,
|
||||
defaultTag,
|
||||
tags = [],
|
||||
api,
|
||||
@@ -60,14 +66,27 @@ export default function DefaultSearchDialog({
|
||||
}: DefaultSearchDialogProps) {
|
||||
const { locale } = useI18n();
|
||||
const [tag, setTag] = useState(defaultTag);
|
||||
const { search, setSearch, query } = useDocsSearch({
|
||||
client: fetchClient({
|
||||
let client: SearchClient;
|
||||
|
||||
if (type === 'static') {
|
||||
// TODO: must remove it on next major, currently, this will bundle the Orama client unnecessarily
|
||||
|
||||
client = use((STATIC ??= import('fumadocs-core/search/client/orama-static'))).oramaStaticClient(
|
||||
{
|
||||
from: api,
|
||||
locale,
|
||||
tag,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
client = fetchClient({
|
||||
api,
|
||||
locale,
|
||||
tag,
|
||||
}),
|
||||
delayMs,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const { search, setSearch, query } = useDocsSearch({ client, delayMs });
|
||||
const defaultItems = useMemo<SortedResult[] | null>(() => {
|
||||
if (links.length === 0) return null;
|
||||
return links.map(([name, link]) => ({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
'use client';
|
||||
import type { DefaultSearchDialogProps } from '@/components/dialog/search-default';
|
||||
import {
|
||||
type ComponentType,
|
||||
createContext,
|
||||
@@ -33,7 +34,7 @@ export interface TagItem {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface SearchProviderProps {
|
||||
export interface SearchProviderProps<DialogProps extends SharedProps = DefaultSearchDialogProps> {
|
||||
/**
|
||||
* Preload search dialog before opening it
|
||||
*
|
||||
@@ -58,12 +59,12 @@ export interface SearchProviderProps {
|
||||
*
|
||||
* It receives the `open` and `onOpenChange` prop, can be lazy loaded with `React.lazy()`
|
||||
*/
|
||||
SearchDialog?: ComponentType<SharedProps>;
|
||||
SearchDialog?: ComponentType<DialogProps>;
|
||||
|
||||
/**
|
||||
* Additional props to the dialog
|
||||
*/
|
||||
options?: Partial<SharedProps & Record<string, unknown>>;
|
||||
options?: Partial<DialogProps>;
|
||||
|
||||
children?: ReactNode;
|
||||
}
|
||||
@@ -109,14 +110,14 @@ const DEFAULT_HOT_KEYS: HotKey[] = [
|
||||
|
||||
const DefaultSearchDialog = lazy(() => import('@/components/dialog/search-default'));
|
||||
|
||||
export function SearchProvider({
|
||||
export function SearchProvider<DialogProps extends SharedProps = DefaultSearchDialogProps>({
|
||||
SearchDialog = DefaultSearchDialog,
|
||||
children,
|
||||
preload = true,
|
||||
options,
|
||||
hotKey = DEFAULT_HOT_KEYS,
|
||||
links,
|
||||
}: SearchProviderProps) {
|
||||
}: SearchProviderProps<DialogProps>) {
|
||||
const [isOpen, setIsOpen] = useState(preload ? false : undefined);
|
||||
const onKeyDown = useEffectEvent((e: KeyboardEvent) => {
|
||||
if (hotKey.every((v) => (typeof v.key === 'string' ? e.key === v.key : v.key(e)))) {
|
||||
@@ -146,13 +147,8 @@ export function SearchProvider({
|
||||
>
|
||||
<Suspense fallback={null}>
|
||||
{isOpen !== undefined && (
|
||||
<SearchDialog
|
||||
open={isOpen}
|
||||
onOpenChange={setIsOpen}
|
||||
// @ts-expect-error -- insert prop for official UIs
|
||||
links={links}
|
||||
{...options}
|
||||
/>
|
||||
// @ts-expect-error -- assume all required props are filled
|
||||
<SearchDialog open={isOpen} onOpenChange={setIsOpen} links={links} {...options} />
|
||||
)}
|
||||
</Suspense>
|
||||
|
||||
|
||||
@@ -2,14 +2,11 @@
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
import { DirectionProvider } from '@base-ui/react/direction-provider';
|
||||
import type { DefaultSearchDialogProps } from '@/components/dialog/search-default';
|
||||
import { ThemeProvider, type ThemeProviderProps } from 'next-themes';
|
||||
import { I18nProvider, type I18nProviderProps } from '@/contexts/i18n';
|
||||
import { SearchProvider, type SearchProviderProps } from '@/contexts/search';
|
||||
|
||||
interface SearchOptions extends Omit<SearchProviderProps, 'children'> {
|
||||
options?: Partial<DefaultSearchDialogProps>;
|
||||
|
||||
/**
|
||||
* Enable search functionality
|
||||
*
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useDocsSearch } from 'fumadocs-core/search/client';
|
||||
import { SearchClient, useDocsSearch } from 'fumadocs-core/search/client';
|
||||
import { fetchClient } from 'fumadocs-core/search/client/fetch';
|
||||
import { type ReactNode, useMemo, useState } from 'react';
|
||||
import { type ReactNode, use, useMemo, useState } from 'react';
|
||||
import { useOnChange } from 'fumadocs-core/utils/use-on-change';
|
||||
import { useI18n } from '@/contexts/i18n';
|
||||
import {
|
||||
@@ -23,6 +23,9 @@ import type { SortedResult } from 'fumadocs-core/search';
|
||||
import type { SearchLink, TagItem } from '@/contexts/search';
|
||||
|
||||
export interface DefaultSearchDialogProps extends SharedProps {
|
||||
/** @deprecated re-create the dialog instead for other clients, see https://fumadocs.dev/docs/search/orama */
|
||||
type?: 'static';
|
||||
|
||||
links?: SearchLink[];
|
||||
defaultTag?: string;
|
||||
tags?: TagItem[];
|
||||
@@ -47,7 +50,10 @@ export interface DefaultSearchDialogProps extends SharedProps {
|
||||
allowClear?: boolean;
|
||||
}
|
||||
|
||||
let STATIC: Promise<typeof import('fumadocs-core/search/client/orama-static')> | undefined;
|
||||
|
||||
export default function DefaultSearchDialog({
|
||||
type,
|
||||
defaultTag,
|
||||
tags = [],
|
||||
api,
|
||||
@@ -59,14 +65,27 @@ export default function DefaultSearchDialog({
|
||||
}: DefaultSearchDialogProps) {
|
||||
const { locale } = useI18n();
|
||||
const [tag, setTag] = useState(defaultTag);
|
||||
const { search, setSearch, query } = useDocsSearch({
|
||||
client: fetchClient({
|
||||
let client: SearchClient;
|
||||
|
||||
if (type === 'static') {
|
||||
// TODO: must remove it on next major, currently, this will bundle the Orama client unnecessarily
|
||||
|
||||
client = use((STATIC ??= import('fumadocs-core/search/client/orama-static'))).oramaStaticClient(
|
||||
{
|
||||
from: api,
|
||||
locale,
|
||||
tag,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
client = fetchClient({
|
||||
api,
|
||||
locale,
|
||||
tag,
|
||||
}),
|
||||
delayMs,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const { search, setSearch, query } = useDocsSearch({ client, delayMs });
|
||||
const defaultItems = useMemo<SortedResult[] | null>(() => {
|
||||
if (links.length === 0) return null;
|
||||
return links.map(([name, link]) => ({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
'use client';
|
||||
import type { DefaultSearchDialogProps } from '@/components/dialog/search-default';
|
||||
import {
|
||||
type ComponentType,
|
||||
createContext,
|
||||
@@ -33,7 +34,7 @@ export interface TagItem {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface SearchProviderProps {
|
||||
export interface SearchProviderProps<DialogProps extends SharedProps = DefaultSearchDialogProps> {
|
||||
/**
|
||||
* Preload search dialog before opening it
|
||||
*
|
||||
@@ -58,12 +59,12 @@ export interface SearchProviderProps {
|
||||
*
|
||||
* It receives the `open` and `onOpenChange` prop, can be lazy loaded with `React.lazy()`
|
||||
*/
|
||||
SearchDialog?: ComponentType<SharedProps>;
|
||||
SearchDialog?: ComponentType<DialogProps>;
|
||||
|
||||
/**
|
||||
* Additional props to the dialog
|
||||
*/
|
||||
options?: Partial<SharedProps & Record<string, unknown>>;
|
||||
options?: Partial<DialogProps>;
|
||||
|
||||
children?: ReactNode;
|
||||
}
|
||||
@@ -109,14 +110,14 @@ const DEFAULT_HOT_KEYS: HotKey[] = [
|
||||
|
||||
const DefaultSearchDialog = lazy(() => import('@/components/dialog/search-default'));
|
||||
|
||||
export function SearchProvider({
|
||||
export function SearchProvider<DialogProps extends SharedProps = DefaultSearchDialogProps>({
|
||||
SearchDialog = DefaultSearchDialog,
|
||||
children,
|
||||
preload = true,
|
||||
options,
|
||||
hotKey = DEFAULT_HOT_KEYS,
|
||||
links,
|
||||
}: SearchProviderProps) {
|
||||
}: SearchProviderProps<DialogProps>) {
|
||||
const [isOpen, setIsOpen] = useState(preload ? false : undefined);
|
||||
const onKeyDown = useEffectEvent((e: KeyboardEvent) => {
|
||||
if (hotKey.every((v) => (typeof v.key === 'string' ? e.key === v.key : v.key(e)))) {
|
||||
@@ -146,13 +147,8 @@ export function SearchProvider({
|
||||
>
|
||||
<Suspense fallback={null}>
|
||||
{isOpen !== undefined && (
|
||||
<SearchDialog
|
||||
open={isOpen}
|
||||
onOpenChange={setIsOpen}
|
||||
// @ts-expect-error -- insert prop for official UIs
|
||||
links={links}
|
||||
{...options}
|
||||
/>
|
||||
// @ts-expect-error -- assume all required props are filled
|
||||
<SearchDialog open={isOpen} onOpenChange={setIsOpen} links={links} {...options} />
|
||||
)}
|
||||
</Suspense>
|
||||
|
||||
|
||||
@@ -2,14 +2,11 @@
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
import { DirectionProvider } from '@radix-ui/react-direction';
|
||||
import type { DefaultSearchDialogProps } from '@/components/dialog/search-default';
|
||||
import { ThemeProvider, type ThemeProviderProps } from 'next-themes';
|
||||
import { I18nProvider, type I18nProviderProps } from '@/contexts/i18n';
|
||||
import { SearchProvider, type SearchProviderProps } from '@/contexts/search';
|
||||
|
||||
interface SearchOptions extends Omit<SearchProviderProps, 'options' | 'children'> {
|
||||
options?: Partial<DefaultSearchDialogProps>;
|
||||
|
||||
interface SearchOptions extends Omit<SearchProviderProps, 'children'> {
|
||||
/**
|
||||
* Enable search functionality
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user