From e63c0a8ab8832bed99fc355dba91e0337f83fea1 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Fri, 27 Jun 2025 14:33:53 +0200 Subject: [PATCH 01/18] upd --- .../client/components/tabs/index.client.tsx | 104 +++++++++++------- .../src/client/components/tabs/index.tsx | 32 +++--- .../server/remark-plugins/remark-headings.ts | 55 --------- packages/nextra/src/server/tsdoc/tsdoc.tsx | 6 +- 4 files changed, 84 insertions(+), 113 deletions(-) diff --git a/packages/nextra/src/client/components/tabs/index.client.tsx b/packages/nextra/src/client/components/tabs/index.client.tsx index 96de16ff9b..bffcd25ba3 100644 --- a/packages/nextra/src/client/components/tabs/index.client.tsx +++ b/packages/nextra/src/client/components/tabs/index.client.tsx @@ -14,11 +14,11 @@ import type { TabPanelProps } from '@headlessui/react' import cn from 'clsx' -import type { FC, ReactElement, ReactNode } from 'react' -import { Fragment, useEffect, useRef, useState } from 'react' +import type { FC, ReactNode } from 'react' +import { Children, Fragment, useEffect, useRef, useState } from 'react' import { useHash } from '../../hooks/use-hash.js' -type TabItem = string | ReactElement +type TabItem = string type TabObjectItem = { label: TabItem @@ -31,7 +31,10 @@ function isTabObjectItem(item: unknown): item is TabObjectItem { export const Tabs: FC< { - items: (TabItem | TabObjectItem)[] + /** + * @deprecated Use `Tabs.Tab#label` and `Tabs.Tab#disabled` props instead. + */ + items?: (TabItem | TabObjectItem)[] children: ReactNode /** LocalStorage key for persisting the selected tab. */ storageKey?: string @@ -41,14 +44,14 @@ export const Tabs: FC< tabClassName?: HeadlessTabProps['className'] } & Pick > = ({ - items, children, storageKey, defaultIndex = 0, selectedIndex: _selectedIndex, onChange, className, - tabClassName + tabClassName, + ...props }) => { const [selectedIndex, setSelectedIndex] = useState(defaultIndex) const hash = useHash() @@ -116,6 +119,20 @@ export const Tabs: FC< setSelectedIndex(index) onChange?.(index) } + const hasLabelPropInTab = Children.toArray(children).some( + child => (child as any).props.label + ) + const items: TabObjectItem[] = hasLabelPropInTab + ? (children as any).map((child: any) => child.props as TabObjectItem) + : props.items!.map(item => { + if (!isTabObjectItem(item)) { + return { id: item } + } + return { + id: item.label, + disabled: item.disabled + } + }) return ( - {items.map((item, index) => ( - { - const { selected, disabled, hover, focus } = args - return cn( - focus && 'x:nextra-focus x:ring-inset', - 'x:whitespace-nowrap x:cursor-pointer', - 'x:rounded-t x:p-2 x:font-medium x:leading-5 x:transition-colors', - 'x:-mb-0.5 x:select-none x:border-b-2', - selected - ? 'x:border-current x:outline-none' - : hover - ? 'x:border-gray-200 x:dark:border-neutral-800' - : 'x:border-transparent', - selected - ? 'x:text-primary-600' - : disabled - ? 'x:text-gray-400 x:dark:text-neutral-600 x:pointer-events-none' + {items.map((item, index) => { + return ( + { + const { selected, disabled, hover, focus } = args + return cn( + focus && 'x:nextra-focus x:ring-inset', + 'x:whitespace-nowrap x:cursor-pointer', + 'x:rounded-t x:p-2 x:font-medium x:leading-5 x:transition-colors', + 'x:-mb-0.5 x:select-none x:border-b-2', + selected + ? 'x:border-current x:outline-none' : hover - ? 'x:text-black x:dark:text-white' - : 'x:text-gray-600 x:dark:text-gray-200', - typeof tabClassName === 'function' - ? tabClassName(args) - : tabClassName - ) - }} - > - {isTabObjectItem(item) ? item.label : item} - - ))} + ? 'x:border-gray-200 x:dark:border-neutral-800' + : 'x:border-transparent', + selected + ? 'x:text-primary-600' + : disabled + ? 'x:text-gray-400 x:dark:text-neutral-600 x:pointer-events-none' + : hover + ? 'x:text-black x:dark:text-white' + : 'x:text-gray-600 x:dark:text-gray-200', + typeof tabClassName === 'function' + ? tabClassName(args) + : tabClassName + ) + }} + > +

+ {item.label} +

+ {item.label} +
+ ) + })} {children}
) } -export const Tab: FC = ({ +export const Tab: FC = ({ children, // For SEO display all the Panel in the DOM and set `display: none;` for those that are not selected unmount = false, diff --git a/packages/nextra/src/client/components/tabs/index.tsx b/packages/nextra/src/client/components/tabs/index.tsx index eae8f5a8f9..5cd9f77957 100644 --- a/packages/nextra/src/client/components/tabs/index.tsx +++ b/packages/nextra/src/client/components/tabs/index.tsx @@ -3,28 +3,25 @@ import type { ComponentProps } from 'react' import { Tabs as _Tabs, Tab } from './index.client.js' -// Workaround to fix -// Error: Cannot access Tab.propTypes on the server. You cannot dot into a client module from a -// server component. You can only pass the imported name through. /** * A built-in component for creating tabbed content, helping organize related information in a * compact, interactive layout. * * @example - * - * **pnpm**: Fast, disk space efficient package manager. - * **npm** is a package manager for the JavaScript programming language. - * **Yarn** is a software packaging system. + * + * **pnpm**: Fast, disk space efficient package manager. + * **npm** is a package manager for the JavaScript programming language. + * **Yarn** is a software packaging system. * * * @usage * ```mdx * import { Tabs } from 'nextra/components' * - * - * **pnpm**: Fast, disk space efficient package manager. - * **npm** is a package manager for the JavaScript programming language. - * **Yarn** is a software packaging system. + * + * **pnpm**: Fast, disk space efficient package manager. + * **npm** is a package manager for the JavaScript programming language. + * **Yarn** is a software packaging system. * * ``` * @@ -35,20 +32,23 @@ import { Tabs as _Tabs, Tab } from './index.client.js' * ```mdx /defaultIndex="1"/ * import { Tabs } from 'nextra/components' * - * + * * ... * * ``` * * And you will have `npm` as the default tab: * - * - * **pnpm**: Fast, disk space efficient package manager. - * **npm** is a package manager for the JavaScript programming language. - * **Yarn** is a software packaging system. + * + * **pnpm**: Fast, disk space efficient package manager. + * **npm** is a package manager for the JavaScript programming language. + * **Yarn** is a software packaging system. * */ export const Tabs = Object.assign( + // Workaround to fix + // Error: Cannot access Tab.propTypes on the server. You cannot dot into a client module from a + // server component. You can only pass the imported name through. (props: ComponentProps) => <_Tabs {...props} />, { Tab } ) diff --git a/packages/nextra/src/server/remark-plugins/remark-headings.ts b/packages/nextra/src/server/remark-plugins/remark-headings.ts index 5486ff4839..95d48ddbee 100644 --- a/packages/nextra/src/server/remark-plugins/remark-headings.ts +++ b/packages/nextra/src/server/remark-plugins/remark-headings.ts @@ -60,61 +60,6 @@ export const remarkHeadings: Plugin< return } - const isTab = - node.type === 'mdxJsxFlowElement' && node.name === 'Tabs.Tab' - if (isTab) { - const itemsAttr: any = - parent && - parent.type === 'mdxJsxFlowElement' && - parent.name === 'Tabs' && - parent.attributes.find( - ( - attr: MdxJsxExpressionAttribute | MdxJsxAttribute - ): attr is MdxJsxAttribute => - attr.type === 'mdxJsxAttribute' && attr.name === 'items' - ) - if (!itemsAttr) return - const tabName = - itemsAttr.value.data.estree.body[0].expression.elements.map( - (el: Literal) => el.value - )[index!] - const id = slugger.slug(tabName) - node.children.unshift({ - type: 'mdxJsxFlowElement', - name: 'h3', - data: { _mdxExplicitJsx: true }, - children: [{ type: 'text', value: tabName }], - attributes: [ - { type: 'mdxJsxAttribute', name: 'id', value: id }, - { - type: 'mdxJsxAttribute', - name: 'style', - value: { - type: 'mdxJsxAttributeValueExpression', - value: '', - data: { - estree: { - type: 'Program', - sourceType: 'module', - comments: [], - body: [ - { - type: 'ExpressionStatement', - expression: createAstObject({ - visibility: 'hidden', - width: 0, - height: 0 - }) - } - ] - } - } - } - } - ] satisfies MdxJsxAttribute[] - } as any) - } - const isDetails = node.type === 'mdxJsxFlowElement' && node.name === 'details' if (isDetails) { diff --git a/packages/nextra/src/server/tsdoc/tsdoc.tsx b/packages/nextra/src/server/tsdoc/tsdoc.tsx index e8f2ecd86c..94a09260a6 100644 --- a/packages/nextra/src/server/tsdoc/tsdoc.tsx +++ b/packages/nextra/src/server/tsdoc/tsdoc.tsx @@ -160,11 +160,9 @@ export const TSDoc: FC = ({ } return ( - `Function Signature ${index + 1}`)} - > + {signatures.map((signature, index) => ( - + ))} From 5d470188e68c958fab677716aa7828ed8b8a9e57 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Fri, 27 Jun 2025 14:49:31 +0200 Subject: [PATCH 02/18] show deprecated --- packages/nextra/src/server/tsdoc/tsdoc.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/nextra/src/server/tsdoc/tsdoc.tsx b/packages/nextra/src/server/tsdoc/tsdoc.tsx index 94a09260a6..16c5cca715 100644 --- a/packages/nextra/src/server/tsdoc/tsdoc.tsx +++ b/packages/nextra/src/server/tsdoc/tsdoc.tsx @@ -333,10 +333,14 @@ const FieldsTable: FC< const id = slugger.slug(field.name) const tags = field.tags ?? {} const defaultValue = tags.default || tags.defaultValue - const description = - // - await renderMarkdown(field.description || tags.description) - + const description = await renderMarkdown( + [ + field.description || tags.description, + tags.deprecated && `**Deprecated**: ${tags.deprecated}` + ] + .filter(Boolean) + .join('\n') + ) return ( From fcf24efa6d902967e35056686663fbcf56a3b1ee Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Fri, 27 Jun 2025 15:45:43 +0200 Subject: [PATCH 03/18] upd --- .../client/components/tabs/index.client.tsx | 65 +++++++++---------- 1 file changed, 31 insertions(+), 34 deletions(-) diff --git a/packages/nextra/src/client/components/tabs/index.client.tsx b/packages/nextra/src/client/components/tabs/index.client.tsx index bffcd25ba3..c49d7e41eb 100644 --- a/packages/nextra/src/client/components/tabs/index.client.tsx +++ b/packages/nextra/src/client/components/tabs/index.client.tsx @@ -63,29 +63,38 @@ export const Tabs: FC< } }, [_selectedIndex]) + const hasLabelPropInTab = Children.toArray(children).some( + child => (child as any).props.label + ) + const items: TabObjectItem[] = hasLabelPropInTab + ? (children as any).map((child: any) => child.props as TabObjectItem) + : props.items!.map(item => { + if (!isTabObjectItem(item)) { + return { id: item } + } + return { + id: item.label, + disabled: item.disabled + } + }) + useEffect(() => { if (!hash) return - const tabPanel = tabPanelsRef.current.querySelector( - `[role=tabpanel]:has([id="${hash}"])` - ) - if (!tabPanel) return - - for (const [index, el] of Object.entries(tabPanelsRef.current.children)) { - if (el === tabPanel) { - setSelectedIndex(Number(index)) - // Clear hash first, otherwise page isn't scrolled - location.hash = '' - // Execute on next tick after `selectedIndex` update - requestAnimationFrame(() => { - location.hash = `#${hash}` - }) - } - } + const index = items.findIndex(item => item.label === hash) + if (index === -1) return + setSelectedIndex(index) + + // Clear hash first, otherwise the page isn't scrolled + location.hash = '' + // Execute on next tick after `selectedIndex` update + requestAnimationFrame(() => { + location.hash = `#${hash}` + }) }, [hash]) useEffect(() => { if (!storageKey) { - // Do not listen storage events if there is no storage key + // Do not listen to storage events if there is no storage key return } @@ -119,20 +128,6 @@ export const Tabs: FC< setSelectedIndex(index) onChange?.(index) } - const hasLabelPropInTab = Children.toArray(children).some( - child => (child as any).props.label - ) - const items: TabObjectItem[] = hasLabelPropInTab - ? (children as any).map((child: any) => child.props as TabObjectItem) - : props.items!.map(item => { - if (!isTabObjectItem(item)) { - return { id: item } - } - return { - id: item.label, - disabled: item.disabled - } - }) return ( { return ( { + history.replaceState(null, '', `#${item.label}`) + }} key={index} disabled={item.disabled} className={args => { @@ -195,7 +191,7 @@ export const Tabs: FC< ) })} - {children} + {children} ) } @@ -205,6 +201,7 @@ export const Tab: FC = ({ // For SEO display all the Panel in the DOM and set `display: none;` for those that are not selected unmount = false, className, + label: _label, ...props }) => { return ( From f1dd76b20bb5dc48a4391d5b88b3df58ea6d05b9 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Fri, 27 Jun 2025 15:45:59 +0200 Subject: [PATCH 04/18] upd --- packages/nextra/src/client/components/tabs/index.client.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/nextra/src/client/components/tabs/index.client.tsx b/packages/nextra/src/client/components/tabs/index.client.tsx index c49d7e41eb..4b6732cd74 100644 --- a/packages/nextra/src/client/components/tabs/index.client.tsx +++ b/packages/nextra/src/client/components/tabs/index.client.tsx @@ -15,7 +15,7 @@ import type { } from '@headlessui/react' import cn from 'clsx' import type { FC, ReactNode } from 'react' -import { Children, Fragment, useEffect, useRef, useState } from 'react' +import { Children, Fragment, useEffect, useState } from 'react' import { useHash } from '../../hooks/use-hash.js' type TabItem = string @@ -55,7 +55,6 @@ export const Tabs: FC< }) => { const [selectedIndex, setSelectedIndex] = useState(defaultIndex) const hash = useHash() - const tabPanelsRef = useRef(null!) useEffect(() => { if (_selectedIndex !== undefined) { From 0b2f529b8e478f0e7d0c46ebadaee2e9cc8e97f6 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Fri, 27 Jun 2025 16:32:53 +0200 Subject: [PATCH 05/18] upd --- .../src/client/mdx-components/details.tsx | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/packages/nextra/src/client/mdx-components/details.tsx b/packages/nextra/src/client/mdx-components/details.tsx index 4b95883e69..103355aa20 100644 --- a/packages/nextra/src/client/mdx-components/details.tsx +++ b/packages/nextra/src/client/mdx-components/details.tsx @@ -13,6 +13,57 @@ import { Children, cloneElement, useEffect, useRef, useState } from 'react' import { Collapse } from '../components/collapse.js' import { useHash } from '../hooks/index.js' +/** + * A vertically stacked, interactive heading component that reveals or hides content when toggled. + * + * > [!NOTE] + * > + * > This Accordion uses native HTML `
` and `` elements, which are + * > collapsible by default and fully compatible with platforms like GitHub. + * + * @usage + * ```mdx filename="page.mdx" + *
+ * Section 1 + * Content for section 1. + *
+ * Section 2 + * Content for section 2. + *
+ *
+ * ``` + * + * ```jsx filename="page.jsx" + * import { + * Details as Accordion, + * Summary as AccordionTrigger + * } from 'nextra/components' + * + * export function Demo() { + * return ( + * + * Section 1 + * Content for section 1. + * + * + * Section 2 + * Content for section 2. + * + * + * ) + * } + * ``` + * + * @example + *
+ * Summary + * Details + *
+ * Summary 2 + * Details 2 + *
+ *
+ */ export const Details: FC> = ({ children, open, From 9109ec42dcec79dd84b1c69de220f18523436ec5 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Fri, 27 Jun 2025 16:51:00 +0200 Subject: [PATCH 06/18] upd --- docs/app/docs/built-ins/[name]/page.tsx | 5 +++++ packages/nextra/src/client/mdx-components/details.tsx | 9 +++------ packages/nextra/src/client/mdx-components/index.ts | 4 ++-- packages/nextra/src/client/mdx-components/summary.tsx | 2 +- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/docs/app/docs/built-ins/[name]/page.tsx b/docs/app/docs/built-ins/[name]/page.tsx index ca2b2001ed..b5516aa5e9 100644 --- a/docs/app/docs/built-ins/[name]/page.tsx +++ b/docs/app/docs/built-ins/[name]/page.tsx @@ -31,6 +31,11 @@ const API_REFERENCE: ( "Omit" }, { type: 'separator', title: 'Content Components', name: '_2' }, + { + name: 'Accordion', + packageName: 'nextra/components', + groupKeys: 'ComponentProps<"details">' + }, { name: 'Bleed', packageName: 'nextra/components', diff --git a/packages/nextra/src/client/mdx-components/details.tsx b/packages/nextra/src/client/mdx-components/details.tsx index 103355aa20..34576fd428 100644 --- a/packages/nextra/src/client/mdx-components/details.tsx +++ b/packages/nextra/src/client/mdx-components/details.tsx @@ -34,10 +34,7 @@ import { useHash } from '../hooks/index.js' * ``` * * ```jsx filename="page.jsx" - * import { - * Details as Accordion, - * Summary as AccordionTrigger - * } from 'nextra/components' + * import { Accordion, AccordionTrigger } from 'nextra/components' * * export function Demo() { * return ( @@ -64,7 +61,7 @@ import { useHash } from '../hooks/index.js' *
* */ -export const Details: FC> = ({ +export const Accordion: FC> = ({ children, open, className, @@ -160,7 +157,7 @@ function findSummary( return } // @ts-expect-error -- fixme - if (child.type !== Details && child.props.children) { + if (child.type !== Accordion && child.props.children) { // @ts-expect-error -- fixme ;[summary, child] = findSummary(child.props.children, setIsOpen) } diff --git a/packages/nextra/src/client/mdx-components/index.ts b/packages/nextra/src/client/mdx-components/index.ts index 33432c5962..0b1d814bb7 100644 --- a/packages/nextra/src/client/mdx-components/index.ts +++ b/packages/nextra/src/client/mdx-components/index.ts @@ -1,7 +1,7 @@ export { Pre } from './pre/index.js' export { Anchor } from './anchor.js' export { Code } from './code.js' -export { Details } from './details.js' +export { Accordion, Accordion as Details } from './details.js' export { Image } from './image.js' -export { Summary } from './summary.js' +export { AccordionTrigger, AccordionTrigger as Summary } from './summary.js' export { Table } from './table.js' diff --git a/packages/nextra/src/client/mdx-components/summary.tsx b/packages/nextra/src/client/mdx-components/summary.tsx index 3c120f2548..53576feecd 100644 --- a/packages/nextra/src/client/mdx-components/summary.tsx +++ b/packages/nextra/src/client/mdx-components/summary.tsx @@ -2,7 +2,7 @@ import cn from 'clsx' import type { FC, HTMLAttributes } from 'react' import { ArrowRightIcon, LinkIcon } from '../icons/index.js' -export const Summary: FC> = ({ +export const AccordionTrigger: FC> = ({ children, className, id, From 11534dca3a45af99e990f8166d5254008f3c6c23 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Fri, 27 Jun 2025 17:45:04 +0200 Subject: [PATCH 07/18] upd --- docs/app/docs/built-ins/[name]/page.tsx | 3 ++- packages/nextra/src/client/mdx-components/details.tsx | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/app/docs/built-ins/[name]/page.tsx b/docs/app/docs/built-ins/[name]/page.tsx index b5516aa5e9..1ca391639a 100644 --- a/docs/app/docs/built-ins/[name]/page.tsx +++ b/docs/app/docs/built-ins/[name]/page.tsx @@ -34,7 +34,8 @@ const API_REFERENCE: ( { name: 'Accordion', packageName: 'nextra/components', - groupKeys: 'ComponentProps<"details">' + groupKeys: 'Omit, "open" | "className">', + isFlattened: false, }, { name: 'Bleed', diff --git a/packages/nextra/src/client/mdx-components/details.tsx b/packages/nextra/src/client/mdx-components/details.tsx index 34576fd428..1ac575eddc 100644 --- a/packages/nextra/src/client/mdx-components/details.tsx +++ b/packages/nextra/src/client/mdx-components/details.tsx @@ -63,7 +63,9 @@ import { useHash } from '../hooks/index.js' */ export const Accordion: FC> = ({ children, + /** Default open state. */ open, + /** CSS class name. */ className, ...props }) => { From 2c8923d36f51c29eb08485e12171577d7408020c Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Fri, 27 Jun 2025 17:46:08 +0200 Subject: [PATCH 08/18] upd --- docs/app/docs/built-ins/[name]/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/app/docs/built-ins/[name]/page.tsx b/docs/app/docs/built-ins/[name]/page.tsx index 1ca391639a..8d71994348 100644 --- a/docs/app/docs/built-ins/[name]/page.tsx +++ b/docs/app/docs/built-ins/[name]/page.tsx @@ -35,7 +35,7 @@ const API_REFERENCE: ( name: 'Accordion', packageName: 'nextra/components', groupKeys: 'Omit, "open" | "className">', - isFlattened: false, + isFlattened: false }, { name: 'Bleed', From a2f9178d911fa66da3041f3c6200c2527b890947 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 29 Jun 2025 14:55:11 +0200 Subject: [PATCH 09/18] fix lint --- .../nextra/src/client/components/tabs/index.client.tsx | 6 ++++-- .../nextra/src/server/remark-plugins/remark-headings.ts | 8 +------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/nextra/src/client/components/tabs/index.client.tsx b/packages/nextra/src/client/components/tabs/index.client.tsx index 4b6732cd74..1ec57c7bf7 100644 --- a/packages/nextra/src/client/components/tabs/index.client.tsx +++ b/packages/nextra/src/client/components/tabs/index.client.tsx @@ -18,9 +18,10 @@ import type { FC, ReactNode } from 'react' import { Children, Fragment, useEffect, useState } from 'react' import { useHash } from '../../hooks/use-hash.js' +// eslint-disable-next-line sonarjs/redundant-type-aliases type TabItem = string -type TabObjectItem = { +interface TabObjectItem { label: TabItem disabled: boolean } @@ -67,7 +68,8 @@ export const Tabs: FC< ) const items: TabObjectItem[] = hasLabelPropInTab ? (children as any).map((child: any) => child.props as TabObjectItem) - : props.items!.map(item => { + : // eslint-disable-next-line @typescript-eslint/no-deprecated + props.items!.map(item => { if (!isTabObjectItem(item)) { return { id: item } } diff --git a/packages/nextra/src/server/remark-plugins/remark-headings.ts b/packages/nextra/src/server/remark-plugins/remark-headings.ts index 95d48ddbee..728e9a8886 100644 --- a/packages/nextra/src/server/remark-plugins/remark-headings.ts +++ b/packages/nextra/src/server/remark-plugins/remark-headings.ts @@ -1,16 +1,10 @@ import Slugger from 'github-slugger' -import type { Literal } from 'hast' import type { Parent, Root } from 'mdast' -import type { - MdxJsxAttribute, - MdxJsxExpressionAttribute -} from 'mdast-util-mdx-jsx' import type { Plugin } from 'unified' import { visit } from 'unist-util-visit' import { visitChildren } from 'unist-util-visit-children' import type { Heading } from '../../types.js' import { MARKDOWN_EXTENSION_RE } from '../constants.js' -import { createAstObject } from '../utils.js' import type { HProperties } from './remark-custom-heading-id.js' export const getFlattenedValue = (node: Parent): string => @@ -44,7 +38,7 @@ export const remarkHeadings: Plugin< // verify .md/.mdx exports and attach named `toc` export 'mdxjsEsm' ], - (node, index, parent) => { + node => { if (node.type === 'heading') { if (node.depth === 1) { return From e02f2201bf9812fb9ce80a6a9f98c00d34ab7e13 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 29 Jun 2025 14:56:17 +0200 Subject: [PATCH 10/18] fix lint --- packages/nextra/src/client/components/tabs/index.client.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nextra/src/client/components/tabs/index.client.tsx b/packages/nextra/src/client/components/tabs/index.client.tsx index 1ec57c7bf7..9b2f4cc76f 100644 --- a/packages/nextra/src/client/components/tabs/index.client.tsx +++ b/packages/nextra/src/client/components/tabs/index.client.tsx @@ -91,7 +91,7 @@ export const Tabs: FC< requestAnimationFrame(() => { location.hash = `#${hash}` }) - }, [hash]) + }, [hash]) // eslint-disable-line react-hooks/exhaustive-deps -- check only hash useEffect(() => { if (!storageKey) { From f62e3b89eeaee03fb66d1daa26799ab0faffe48c Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 29 Jun 2025 14:57:44 +0200 Subject: [PATCH 11/18] upd snapshots --- packages/tsdoc/src/__tests__/base.test.ts | 28 +++++++++++------------ 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/tsdoc/src/__tests__/base.test.ts b/packages/tsdoc/src/__tests__/base.test.ts index bab29a4d7a..d63722e098 100644 --- a/packages/tsdoc/src/__tests__/base.test.ts +++ b/packages/tsdoc/src/__tests__/base.test.ts @@ -90,7 +90,7 @@ export default MyType` "params": [ { "name": "props", - "type": "{ items: (TabItem | TabObjectItem)[]; children: ReactNode; storageKey?: string; className?: string | ((bag: ListRenderPropArg) => string) | undefined; tabClassName?: string | ... 1 more ... | undefined; } & Pick<...>", + "type": "{ items?: (string | TabObjectItem)[]; children: ReactNode; storageKey?: string; className?: string | ((bag: ListRenderPropArg) => string) | undefined; tabClassName?: string | ... 1 more ... | undefined; } & Pick<...>", }, ], "returns": { @@ -99,18 +99,18 @@ export default MyType` }, ], "tags": { - "example": " - **pnpm**: Fast, disk space efficient package manager. - **npm** is a package manager for the JavaScript programming language. - **Yarn** is a software packaging system. + "example": " + **pnpm**: Fast, disk space efficient package manager. + **npm** is a package manager for the JavaScript programming language. + **Yarn** is a software packaging system. ", "usage": "\`\`\`mdx import { Tabs } from 'nextra/components' - - **pnpm**: Fast, disk space efficient package manager. - **npm** is a package manager for the JavaScript programming language. - **Yarn** is a software packaging system. + + **pnpm**: Fast, disk space efficient package manager. + **npm** is a package manager for the JavaScript programming language. + **Yarn** is a software packaging system. \`\`\` @@ -121,17 +121,17 @@ export default MyType` \`\`\`mdx /defaultIndex="1"/ import { Tabs } from 'nextra/components' - + ... \`\`\` And you will have \`npm\` as the default tab: - - **pnpm**: Fast, disk space efficient package manager. - **npm** is a package manager for the JavaScript programming language. - **Yarn** is a software packaging system. + + **pnpm**: Fast, disk space efficient package manager. + **npm** is a package manager for the JavaScript programming language. + **Yarn** is a software packaging system. ", }, } From 526167b306a0fb004a6922b9a3e5d486177cb8d3 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 29 Jun 2025 15:06:04 +0200 Subject: [PATCH 12/18] polish --- .../nextra/src/client/mdx-components/details.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/nextra/src/client/mdx-components/details.tsx b/packages/nextra/src/client/mdx-components/details.tsx index 1ac575eddc..4378b2f73d 100644 --- a/packages/nextra/src/client/mdx-components/details.tsx +++ b/packages/nextra/src/client/mdx-components/details.tsx @@ -13,6 +13,16 @@ import { Children, cloneElement, useEffect, useRef, useState } from 'react' import { Collapse } from '../components/collapse.js' import { useHash } from '../hooks/index.js' +type DetailsProps = ComponentProps<'details'> + +interface AccordionProps extends DetailsProps { + /** Default open state. */ + open?: DetailsProps['open'] + + /** CSS class name. */ + className?: DetailsProps['className'] +} + /** * A vertically stacked, interactive heading component that reveals or hides content when toggled. * @@ -61,16 +71,14 @@ import { useHash } from '../hooks/index.js' * * */ -export const Accordion: FC> = ({ +export const Accordion: FC = ({ children, - /** Default open state. */ open, - /** CSS class name. */ className, ...props }) => { const [isOpen, setIsOpen] = useState(!!open) - // To animate the close animation we have to delay the DOM node state here. + // To animate the close animation, we have to delay the DOM node state here. const [delayedOpenState, setDelayedOpenState] = useState(isOpen) const animationRef = useRef(0) From 9270a44f1cfe78c0a1b3b2e7dc51ae11480a9a8b Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 29 Jun 2025 15:11:59 +0200 Subject: [PATCH 13/18] upd --- packages/nextra/src/client/mdx-components/details.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nextra/src/client/mdx-components/details.tsx b/packages/nextra/src/client/mdx-components/details.tsx index 4378b2f73d..7564b794dc 100644 --- a/packages/nextra/src/client/mdx-components/details.tsx +++ b/packages/nextra/src/client/mdx-components/details.tsx @@ -15,7 +15,7 @@ import { useHash } from '../hooks/index.js' type DetailsProps = ComponentProps<'details'> -interface AccordionProps extends DetailsProps { +export interface AccordionProps extends DetailsProps { /** Default open state. */ open?: DetailsProps['open'] From 644c86f8601ee6f0b2ecb9d302cc57f707ccc0eb Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 29 Jun 2025 15:29:46 +0200 Subject: [PATCH 14/18] upd snapshots --- .../src/server/__tests__/compile.test.ts | 60 ------------------- 1 file changed, 60 deletions(-) diff --git a/packages/nextra/src/server/__tests__/compile.test.ts b/packages/nextra/src/server/__tests__/compile.test.ts index 142c41e662..ebed741e1c 100644 --- a/packages/nextra/src/server/__tests__/compile.test.ts +++ b/packages/nextra/src/server/__tests__/compile.test.ts @@ -636,44 +636,14 @@ describe('Code block', () => { <> -

- {'pnpm'} -

<_components.strong>{'pnpm'} {': Fast, disk space efficient package manager.'}
-

- {'npm'} -

<_components.strong>{'npm'} {' is a package manager for the JavaScript programming language.'}
-

- {'yarn'} -

<_components.strong>{'Yarn'} {' is a software packaging system.'}
@@ -681,44 +651,14 @@ describe('Code block', () => { {'\\n'} -

- {'pnpm'} -

<_components.strong>{'pnpm'} {': Fast, disk space efficient package manager.'}
-

- {'npm'} -

<_components.strong>{'npm'} {' is a package manager for the JavaScript programming language.'}
-

- {'yarn'} -

<_components.strong>{'Yarn'} {' is a software packaging system.'}
From bbe4169edaaeef8c020675b6132f45a3c358f050 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 29 Jun 2025 18:08:59 +0200 Subject: [PATCH 15/18] feat(tsdoc): add support for `@inline` tag on function parameters --- packages/tsdoc/src/__tests__/base.test.ts | 25 ++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/tsdoc/src/__tests__/base.test.ts b/packages/tsdoc/src/__tests__/base.test.ts index d63722e098..d91d9081dd 100644 --- a/packages/tsdoc/src/__tests__/base.test.ts +++ b/packages/tsdoc/src/__tests__/base.test.ts @@ -960,6 +960,18 @@ type InlineOnType = () => Promise; */ type InlineAndRemarksOnType = () => Promise; +/** + * @inline + */ +type InlineParam = { + baz: string +} + +/** + * @inline + */ +type InlineFunctionParamOnType = (options?: InlineParam) => Promise; + type $ = { /** * @inline @@ -971,6 +983,7 @@ type $ = { foo: Foo bar: InlineOnType quz: InlineAndRemarksOnType + zz: InlineFunctionParamOnType } export default $` @@ -983,7 +996,11 @@ export default $` "tags": { "inline": "", }, - "type": "(viewport: Viewport, options?: ViewportHelperFunctionOptions) => Promise", + "type": "(viewport: Viewport, options?: { + duration?: number; + ease?: (t: number) => number; + interpolate?: 'smooth' | 'linear'; + }) => Promise", }, { "name": "foo", @@ -1002,6 +1019,12 @@ export default $` "name": "quz", "type": "1337", }, + { + "name": "zz", + "type": "(options?: { + baz: string + }) => Promise", + }, ], "name": "$", } From 6439f9e8c87ed8a88f0c960b2fafd914e11502f2 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 29 Jun 2025 18:10:05 +0200 Subject: [PATCH 16/18] feat(tsdoc): add support for `@inline` tag on function parameters --- packages/nextra/src/server/tsdoc/base.ts | 41 ++++++++++++++++++++---- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/packages/nextra/src/server/tsdoc/base.ts b/packages/nextra/src/server/tsdoc/base.ts index b31a67a3bb..6bb26b64f5 100644 --- a/packages/nextra/src/server/tsdoc/base.ts +++ b/packages/nextra/src/server/tsdoc/base.ts @@ -252,14 +252,15 @@ function getDocEntry({ valueDeclaration.isOptional() : symbol.isOptional() + const typeName = getTypeName({ + tags, + symbol, + subType, + valueDeclaration + }) return { name: prexify(prefix, name), - type: getTypeName({ - tags, - symbol, - subType, - valueDeclaration - }), + type: typeName, ...(typeDescription && { description: typeDescription }), ...(Object.keys(tags).length && { tags }), ...(isOptional && { optional: isOptional }) @@ -315,7 +316,33 @@ function getTypeName({ const [signature] = subType.getCallSignatures() const isFunction = !!signature if (isFunction) { - return signature.getDeclaration().getText() + const params = signature.getParameters().map(param => { + const paramDecl = param.getDeclarations()[0]! + const paramType = project + .getTypeChecker() + .getTypeOfSymbolAtLocation(param, paramDecl) + const inlineParamAlias = paramType + .getNonNullableType() + .getAliasSymbolOrThrow() + const paramTags = inlineParamAlias && getTags(inlineParamAlias) + + const shouldInlineParam = paramTags && 'inline' in paramTags + const paramTypeStr = shouldInlineParam + ? inlineParamAlias! + .getDeclarations()[0]! + .asKindOrThrow(SyntaxKind.TypeAliasDeclaration) + .getTypeNodeOrThrow() + .getText() + : getFormattedText(paramType) + + const optional = paramDecl + .asKindOrThrow(SyntaxKind.Parameter) + .isOptional() + + return `${param.getName()}${optional ? '?' : ''}: ${paramTypeStr}` + }) + + return `(${params.join(', ')}) => ${getFormattedText(signature.getReturnType())}` } const [aliasDecl] = aliasSymbol!.getDeclarations() if (!aliasDecl) { From 13f30911fda7d89224a913f31000498e8b296a82 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 29 Jun 2025 18:11:28 +0200 Subject: [PATCH 17/18] feat(tsdoc): add support for `@inline` tag on function parameters --- .changeset/dark-hotels-drop.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/dark-hotels-drop.md diff --git a/.changeset/dark-hotels-drop.md b/.changeset/dark-hotels-drop.md new file mode 100644 index 0000000000..4849c1e8d5 --- /dev/null +++ b/.changeset/dark-hotels-drop.md @@ -0,0 +1,5 @@ +--- +'nextra': minor +--- + +feat(tsdoc): add support for `@inline` tag on function parameters From fb119d3cd13e8438758ec5365727241419d198db Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 29 Jun 2025 18:21:27 +0200 Subject: [PATCH 18/18] fix lint --- packages/nextra/src/server/tsdoc/base.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/nextra/src/server/tsdoc/base.ts b/packages/nextra/src/server/tsdoc/base.ts index 6bb26b64f5..de75de32d6 100644 --- a/packages/nextra/src/server/tsdoc/base.ts +++ b/packages/nextra/src/server/tsdoc/base.ts @@ -324,16 +324,16 @@ function getTypeName({ const inlineParamAlias = paramType .getNonNullableType() .getAliasSymbolOrThrow() - const paramTags = inlineParamAlias && getTags(inlineParamAlias) + const paramTags = getTags(inlineParamAlias) - const shouldInlineParam = paramTags && 'inline' in paramTags - const paramTypeStr = shouldInlineParam - ? inlineParamAlias! - .getDeclarations()[0]! - .asKindOrThrow(SyntaxKind.TypeAliasDeclaration) - .getTypeNodeOrThrow() - .getText() - : getFormattedText(paramType) + const paramTypeStr = + 'inline' in paramTags + ? inlineParamAlias + .getDeclarations()[0]! + .asKindOrThrow(SyntaxKind.TypeAliasDeclaration) + .getTypeNodeOrThrow() + .getText() + : getFormattedText(paramType) const optional = paramDecl .asKindOrThrow(SyntaxKind.Parameter)