mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
Core & UI: Improve navigation
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-core': patch
|
||||
---
|
||||
|
||||
Fix `findNeighbour()` doesn't exclude other nodes of another root
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-core': minor
|
||||
---
|
||||
|
||||
Lazy build page tree by default
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-core': minor
|
||||
---
|
||||
|
||||
Support `getPageByHref()` on loader API
|
||||
@@ -29,7 +29,13 @@ import { Mermaid } from '@theguild/remark-mermaid/mermaid';
|
||||
import { Rate } from '@/components/rate';
|
||||
import { repo, owner, onRateAction } from '@/lib/github';
|
||||
import type { MDXComponents } from 'mdx/types';
|
||||
import defaultMdxComponents, { createRelativeLink } from 'fumadocs-ui/mdx';
|
||||
import defaultMdxComponents from 'fumadocs-ui/mdx';
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from '@/components/ui/hover-card';
|
||||
import Link from 'fumadocs-core/link';
|
||||
|
||||
function PreviewRenderer({ preview }: { preview: string }): ReactNode {
|
||||
if (preview && preview in Preview) {
|
||||
@@ -81,8 +87,34 @@ export default async function Page(props: {
|
||||
components={{
|
||||
...defaultMdxComponents,
|
||||
...((await import('lucide-react')) as unknown as MDXComponents),
|
||||
a: createRelativeLink(source, page),
|
||||
a: ({ href, ...props }) => {
|
||||
const found = source.getPageByHref(href ?? '', {
|
||||
dir: page.file.dirname,
|
||||
});
|
||||
|
||||
if (!found) return <Link href={href} {...props} />;
|
||||
|
||||
return (
|
||||
<HoverCard openDelay={100}>
|
||||
<HoverCardTrigger asChild>
|
||||
<Link
|
||||
href={
|
||||
found.hash
|
||||
? `${found.page.url}#${found.hash}`
|
||||
: found.page.url
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent className="text-sm">
|
||||
<p className="font-medium">{found.page.data.title}</p>
|
||||
<p className="text-fd-muted-foreground">
|
||||
{found.page.data.description}
|
||||
</p>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
);
|
||||
},
|
||||
Popup,
|
||||
PopupContent,
|
||||
PopupTrigger,
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import * as HoverCardPrimitive from '@radix-ui/react-hover-card';
|
||||
|
||||
import { cn } from '@/lib/cn';
|
||||
|
||||
const HoverCard = HoverCardPrimitive.Root;
|
||||
|
||||
const HoverCardTrigger = HoverCardPrimitive.Trigger;
|
||||
|
||||
const HoverCardContent = React.forwardRef<
|
||||
React.ComponentRef<typeof HoverCardPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
|
||||
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
|
||||
<HoverCardPrimitive.HoverCardPortal>
|
||||
<HoverCardPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'z-50 w-72 rounded-lg border bg-fd-popover p-4 text-popover-fd-foreground shadow-md outline-none data-[state=open]:animate-fd-popover-in data-[state=closed]:animate-fd-popover-out origin-[--radix-hover-card-content-transform-origin]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</HoverCardPrimitive.HoverCardPortal>
|
||||
));
|
||||
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
|
||||
|
||||
export { HoverCard, HoverCardTrigger, HoverCardContent };
|
||||
@@ -16,12 +16,20 @@ import defaultMdxComponents from 'fumadocs-ui/mdx';
|
||||
|
||||
To support links with relative file path in `href`, override the default `a` component with:
|
||||
|
||||
```ts
|
||||
```tsx
|
||||
import { createRelativeLink } from 'fumadocs-ui/mdx';
|
||||
import { source } from '@/lib/source';
|
||||
|
||||
const page = source.getPage(['...']);
|
||||
const a = createRelativeLink(source, page);
|
||||
|
||||
return (
|
||||
<MdxContent
|
||||
components={{
|
||||
// override the `a` tag
|
||||
a: createRelativeLink(source, page),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
```
|
||||
|
||||
```mdx
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"@inkeep/ai-api": "^0.8.0",
|
||||
"@oramacloud/client": "^2.1.4",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-hover-card": "^1.1.6",
|
||||
"@radix-ui/react-popover": "^1.1.6",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
|
||||
@@ -24,24 +24,46 @@ export function flattenTree(tree: PageTree.Node[]): PageTree.Item[] {
|
||||
export function findNeighbour(
|
||||
tree: PageTree.Root,
|
||||
url: string,
|
||||
options?: {
|
||||
separateRoot?: boolean;
|
||||
},
|
||||
): {
|
||||
previous?: PageTree.Item;
|
||||
next?: PageTree.Item;
|
||||
} {
|
||||
const list = flattenTree(tree.children);
|
||||
const { separateRoot = true } = options ?? {};
|
||||
const roots = separateRoot ? getPageTreeRoots(tree) : [tree];
|
||||
const lists = roots.map((node) => flattenTree(node.children));
|
||||
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
if (list[i].url === url) {
|
||||
return {
|
||||
next: list[i + 1],
|
||||
previous: list[i - 1],
|
||||
};
|
||||
for (const list of lists) {
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
if (list[i].url === url) {
|
||||
return {
|
||||
next: list[i + 1],
|
||||
previous: list[i - 1],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
export function getPageTreeRoots(
|
||||
pageTree: PageTree.Root | PageTree.Folder,
|
||||
): (PageTree.Root | PageTree.Folder)[] {
|
||||
return pageTree.children.flatMap((child) => {
|
||||
if (child.type !== 'folder') return [];
|
||||
const roots = getPageTreeRoots(child);
|
||||
|
||||
if (child.root) {
|
||||
return [child, ...roots];
|
||||
}
|
||||
|
||||
return roots;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Separate the folder nodes of a root into multiple roots
|
||||
*/
|
||||
|
||||
@@ -19,6 +19,8 @@ export interface Item {
|
||||
name: ReactNode;
|
||||
url: string;
|
||||
external?: boolean;
|
||||
|
||||
description?: ReactNode;
|
||||
icon?: ReactElement;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import type { BuildPageTreeOptions } from './page-tree-builder';
|
||||
import { createPageTreeBuilder } from './page-tree-builder';
|
||||
import { type FileInfo } from './path';
|
||||
import type { MetaFile, PageFile, Storage } from './file-system';
|
||||
import * as path from 'node:path';
|
||||
|
||||
export interface LoaderConfig {
|
||||
source: SourceConfig;
|
||||
@@ -82,7 +83,21 @@ export interface LoaderOutput<Config extends LoaderConfig> {
|
||||
? Record<string, PageTree.Root>
|
||||
: PageTree.Root;
|
||||
|
||||
getPageTree(locale?: string): PageTree.Root;
|
||||
getPageTree: (locale?: string) => PageTree.Root;
|
||||
getPageByHref: (
|
||||
href: string,
|
||||
options?: {
|
||||
/**
|
||||
* resolve relative file paths in `href` from specified directory
|
||||
*/
|
||||
dir?: string;
|
||||
},
|
||||
) =>
|
||||
| {
|
||||
page: Page<Config['source']['pageData']>;
|
||||
hash?: string;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
_i18n?: I18nConfig;
|
||||
|
||||
@@ -128,7 +143,7 @@ function indexPages(
|
||||
getUrl: UrlFn,
|
||||
i18n?: I18nConfig,
|
||||
): {
|
||||
// (locale.slugs -> page[])
|
||||
// (locale.slugs -> page)
|
||||
pages: Map<string, Page>;
|
||||
|
||||
pathToFile: Map<string, Page | Meta>;
|
||||
@@ -168,6 +183,8 @@ function indexPages(
|
||||
}
|
||||
|
||||
export function createGetUrl(baseUrl: string, i18n?: I18nConfig): UrlFn {
|
||||
const baseSlugs = baseUrl.split('/');
|
||||
|
||||
return (slugs, locale) => {
|
||||
const hideLocale = i18n?.hideLocale ?? 'never';
|
||||
let urlLocale: string | undefined;
|
||||
@@ -181,9 +198,8 @@ export function createGetUrl(baseUrl: string, i18n?: I18nConfig): UrlFn {
|
||||
urlLocale = locale;
|
||||
}
|
||||
|
||||
const paths = urlLocale
|
||||
? [urlLocale, ...baseUrl.split('/'), ...slugs]
|
||||
: [...baseUrl.split('/'), ...slugs];
|
||||
const paths = [...baseSlugs, ...slugs];
|
||||
if (urlLocale) paths.unshift(urlLocale);
|
||||
|
||||
return `/${paths.filter((v) => v.length > 0).join('/')}`;
|
||||
};
|
||||
@@ -230,25 +246,58 @@ function createOutput(options: LoaderOptions): LoaderOutput<LoaderConfig> {
|
||||
);
|
||||
const walker = indexPages(storage, getUrl, options.i18n);
|
||||
const builder = createPageTreeBuilder();
|
||||
const pageTree =
|
||||
options.i18n === undefined
|
||||
? builder.build({
|
||||
storage,
|
||||
resolveIcon: options.icon,
|
||||
getUrl,
|
||||
...options.pageTree,
|
||||
})
|
||||
: builder.buildI18n({
|
||||
let pageTree: LoaderOutput<LoaderConfig>['pageTree'] | undefined;
|
||||
|
||||
return {
|
||||
_i18n: options.i18n,
|
||||
get pageTree() {
|
||||
if (options.i18n) {
|
||||
pageTree ??= builder.buildI18n({
|
||||
storage,
|
||||
resolveIcon: options.icon,
|
||||
getUrl,
|
||||
i18n: options.i18n,
|
||||
...options.pageTree,
|
||||
}) as unknown as LoaderOutput<LoaderConfig>['pageTree'];
|
||||
} else {
|
||||
pageTree ??= builder.build({
|
||||
storage,
|
||||
resolveIcon: options.icon,
|
||||
getUrl,
|
||||
...options.pageTree,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
_i18n: options.i18n,
|
||||
pageTree: pageTree as LoaderOutput<LoaderConfig>['pageTree'],
|
||||
return pageTree;
|
||||
},
|
||||
set pageTree(v) {
|
||||
pageTree = v;
|
||||
},
|
||||
getPageByHref(href, { dir = '' } = {}) {
|
||||
const pages = Array.from(walker.pages.values());
|
||||
const [value, hash] = href.split('#', 2);
|
||||
|
||||
if (
|
||||
value.startsWith('.') &&
|
||||
(value.endsWith('.md') || value.endsWith('.mdx'))
|
||||
) {
|
||||
const hrefPath = path.join(dir, value);
|
||||
const target = pages.find((item) => item.file.path === hrefPath);
|
||||
|
||||
if (target)
|
||||
return {
|
||||
page: target,
|
||||
hash,
|
||||
};
|
||||
}
|
||||
|
||||
const target = pages.find((item) => item.url === value);
|
||||
if (target)
|
||||
return {
|
||||
page: target,
|
||||
hash,
|
||||
};
|
||||
},
|
||||
getPages(language = options.i18n?.defaultLanguage ?? '') {
|
||||
const pages: Page[] = [];
|
||||
|
||||
@@ -281,12 +330,12 @@ function createOutput(options: LoaderOptions): LoaderOutput<LoaderConfig> {
|
||||
},
|
||||
getPageTree(locale) {
|
||||
if (options.i18n) {
|
||||
return pageTree[
|
||||
return this.pageTree[
|
||||
(locale ?? options.i18n.defaultLanguage) as keyof typeof pageTree
|
||||
] as PageTree.Root;
|
||||
];
|
||||
}
|
||||
|
||||
return pageTree as PageTree.Root;
|
||||
return this.pageTree;
|
||||
},
|
||||
getNodePage(node) {
|
||||
const ref = node.$ref?.file ?? node.$id;
|
||||
|
||||
@@ -258,6 +258,7 @@ function buildFileNode(
|
||||
$id: localized.file.path,
|
||||
type: 'page',
|
||||
name: localized.data.data.title ?? pathToName(localized.file.name),
|
||||
description: localized.data.data.description,
|
||||
icon: ctx.options.resolveIcon?.(localized.data.data.icon),
|
||||
url: ctx.options.getUrl(localized.data.slugs, ctx.lang),
|
||||
$ref: !ctx.options.noRef
|
||||
|
||||
@@ -13,6 +13,7 @@ export interface MetaData {
|
||||
export interface PageData {
|
||||
icon?: string | undefined;
|
||||
title?: string;
|
||||
description?: string | undefined;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- infer types
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { LoaderConfig, LoaderOutput, Page } from 'fumadocs-core/source';
|
||||
import type { ComponentProps, FC } from 'react';
|
||||
import defaultMdxComponents from '@/mdx';
|
||||
import * as path from 'node:path';
|
||||
|
||||
/**
|
||||
* Extend the default Link component to resolve relative file paths in `href`.
|
||||
@@ -16,16 +15,10 @@ export function createRelativeLink(
|
||||
OverrideLink: FC<ComponentProps<'a'>> = defaultMdxComponents.a,
|
||||
): FC<ComponentProps<'a'>> {
|
||||
return async function RelativeLink({ href, ...props }) {
|
||||
if (
|
||||
href &&
|
||||
href.startsWith('.') &&
|
||||
(href.endsWith('.md') || href.endsWith('.mdx'))
|
||||
) {
|
||||
const pages = source.getPages();
|
||||
const hrefPath = path.join(page.file.dirname, href);
|
||||
const targetPage = pages.find((item) => item.file.path === hrefPath);
|
||||
|
||||
if (targetPage) href = targetPage.url;
|
||||
// resolve relative href
|
||||
if (href && href.startsWith('.')) {
|
||||
const target = source.getPageByHref(href, { dir: page.file.dirname });
|
||||
if (target) href = target.page.url;
|
||||
}
|
||||
|
||||
return <OverrideLink href={href} {...props} />;
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
} from 'react';
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { cva } from 'class-variance-authority';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { useI18n } from './contexts/i18n';
|
||||
import { useTreeContext, useTreePath } from './contexts/tree';
|
||||
@@ -122,24 +121,17 @@ export function LastUpdate(props: { date: Date }) {
|
||||
);
|
||||
}
|
||||
|
||||
type Item = Pick<PageTree.Item, 'name' | 'description' | 'url'>;
|
||||
export interface FooterProps {
|
||||
/**
|
||||
* Items including information for the next and previous page
|
||||
*/
|
||||
items?: {
|
||||
previous?: { name: string; url: string };
|
||||
next?: { name: string; url: string };
|
||||
previous?: Item;
|
||||
next?: Item;
|
||||
};
|
||||
}
|
||||
|
||||
const itemVariants = cva(
|
||||
'flex w-full flex-col gap-2 rounded-lg border p-4 text-sm transition-colors hover:bg-fd-accent/80 hover:text-fd-accent-foreground',
|
||||
);
|
||||
|
||||
const itemLabel = cva(
|
||||
'inline-flex items-center gap-0.5 text-fd-muted-foreground',
|
||||
);
|
||||
|
||||
function scanNavigationList(tree: PageTree.Node[]) {
|
||||
const list: PageTree.Item[] = [];
|
||||
|
||||
@@ -165,7 +157,6 @@ const listCache = new WeakMap<PageTree.Root, PageTree.Item[]>();
|
||||
|
||||
export function Footer({ items }: FooterProps) {
|
||||
const { root } = useTreeContext();
|
||||
const { text } = useI18n();
|
||||
const pathname = usePathname();
|
||||
|
||||
const { previous, next } = useMemo(() => {
|
||||
@@ -185,32 +176,55 @@ export function Footer({ items }: FooterProps) {
|
||||
}, [items, pathname, root]);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-4 pb-6">
|
||||
{previous ? (
|
||||
<Link href={previous.url} className={cn(itemVariants())}>
|
||||
<div className={cn(itemLabel())}>
|
||||
<ChevronLeft className="-ms-1 size-4 shrink-0 rtl:rotate-180" />
|
||||
<p>{text.previousPage}</p>
|
||||
</div>
|
||||
<p className="font-medium md:text-[15px]">{previous.name}</p>
|
||||
</Link>
|
||||
) : null}
|
||||
{next ? (
|
||||
<Link
|
||||
href={next.url}
|
||||
className={cn(itemVariants({ className: 'col-start-2 text-end' }))}
|
||||
>
|
||||
<div className={cn(itemLabel({ className: 'flex-row-reverse' }))}>
|
||||
<ChevronRight className="-me-1 size-4 shrink-0 rtl:rotate-180" />
|
||||
<p>{text.nextPage}</p>
|
||||
</div>
|
||||
<p className="font-medium md:text-[15px]">{next.name}</p>
|
||||
</Link>
|
||||
) : null}
|
||||
<div
|
||||
className={cn(
|
||||
'@container grid gap-4 pb-6',
|
||||
previous && next ? 'grid-cols-2' : 'grid-cols-1',
|
||||
)}
|
||||
>
|
||||
{previous ? <FooterItem item={previous} index={0} /> : null}
|
||||
{next ? <FooterItem item={next} index={1} /> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FooterItem({ item, index }: { item: Item; index: 0 | 1 }) {
|
||||
const { text } = useI18n();
|
||||
const Icon = index === 0 ? ChevronLeft : ChevronRight;
|
||||
const title = item.description ? item.name : null;
|
||||
const description = item.description ?? item.name;
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={item.url}
|
||||
className={cn(
|
||||
'flex flex-col gap-2 rounded-lg border p-4 text-sm transition-colors hover:bg-fd-accent/80 hover:text-fd-accent-foreground @max-lg:col-span-full',
|
||||
index === 1 && 'text-end',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'inline-flex items-center gap-1.5',
|
||||
index === 1 && 'flex-row-reverse',
|
||||
title ? 'font-medium' : 'text-fd-muted-foreground',
|
||||
)}
|
||||
>
|
||||
<Icon className="-mx-1 size-4 shrink-0 rtl:rotate-180" />
|
||||
<p>{title ?? text.nextPage}</p>
|
||||
</div>
|
||||
<p
|
||||
className={cn(
|
||||
title
|
||||
? 'text-fd-muted-foreground truncate'
|
||||
: 'font-medium md:text-[15px]',
|
||||
)}
|
||||
>
|
||||
{description}
|
||||
</p>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export type BreadcrumbProps = BreadcrumbOptions;
|
||||
|
||||
export function Breadcrumb(options: BreadcrumbProps) {
|
||||
|
||||
Generated
+33
@@ -68,6 +68,9 @@ importers:
|
||||
'@radix-ui/react-dialog':
|
||||
specifier: ^1.1.6
|
||||
version: 1.1.6(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
|
||||
'@radix-ui/react-hover-card':
|
||||
specifier: ^1.1.6
|
||||
version: 1.1.6(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
|
||||
'@radix-ui/react-popover':
|
||||
specifier: ^1.1.6
|
||||
version: 1.1.6(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
|
||||
@@ -2410,6 +2413,19 @@ packages:
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/[email protected]':
|
||||
resolution: {integrity: sha512-E4ozl35jq0VRlrdc4dhHrNSV0JqBb4Jy73WAhBEK7JoYnQ83ED5r0Rb/XdVKw89ReAJN38N492BAPBZQ57VmqQ==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/[email protected]':
|
||||
resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==}
|
||||
peerDependencies:
|
||||
@@ -8158,6 +8174,23 @@ snapshots:
|
||||
'@types/react': 19.0.10
|
||||
'@types/react-dom': 19.0.4(@types/[email protected])
|
||||
|
||||
'@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.1
|
||||
'@radix-ui/react-compose-refs': 1.1.1(@types/[email protected])([email protected])
|
||||
'@radix-ui/react-context': 1.1.1(@types/[email protected])([email protected])
|
||||
'@radix-ui/react-dismissable-layer': 1.1.5(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
|
||||
'@radix-ui/react-popper': 1.2.2(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
|
||||
'@radix-ui/react-portal': 1.1.4(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
|
||||
'@radix-ui/react-presence': 1.1.2(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
|
||||
'@radix-ui/react-primitive': 2.0.2(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
|
||||
'@radix-ui/react-use-controllable-state': 1.1.0(@types/[email protected])([email protected])
|
||||
react: 19.0.0
|
||||
react-dom: 19.0.0([email protected])
|
||||
optionalDependencies:
|
||||
'@types/react': 19.0.10
|
||||
'@types/react-dom': 19.0.4(@types/[email protected])
|
||||
|
||||
'@radix-ui/[email protected](@types/[email protected])([email protected])':
|
||||
dependencies:
|
||||
'@radix-ui/react-use-layout-effect': 1.1.0(@types/[email protected])([email protected])
|
||||
|
||||
Reference in New Issue
Block a user