mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
Core & UI: Improve inline elements in search
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-core': patch
|
||||
---
|
||||
|
||||
[Remark Feedback] skip MDX elements by default to avoid interfering with component logic
|
||||
@@ -7,7 +7,7 @@ description: Extract information from your documents, useful for implementing do
|
||||
|
||||
Add it as a remark plugin.
|
||||
|
||||
```ts title="MDX Compiler"
|
||||
```ts tab="MDX Compiler"
|
||||
import { compile } from '@mdx-js/mdx';
|
||||
import { remarkStructure } from 'fumadocs-core/mdx-plugins';
|
||||
|
||||
@@ -16,7 +16,17 @@ const vfile = await compile('...', {
|
||||
});
|
||||
```
|
||||
|
||||
> This plugin is included by default on Fumadocs MDX.
|
||||
```ts tab="Fumadocs MDX" title="source.config.ts"
|
||||
import { defineConfig } from 'fumadocs-mdx/config';
|
||||
|
||||
export default defineConfig({
|
||||
mdxOptions: {
|
||||
// This plugin is enabled by default on Fumadocs MDX
|
||||
// options:
|
||||
remarkStructureOptions: {},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Extracted information could be found in `vfile.data.structuredData`, you may
|
||||
write your own plugin to convert it into a MDX export.
|
||||
@@ -40,14 +50,16 @@ array, each item contains a `heading` prop indicating the heading of paragraph.
|
||||
| Prop | |
|
||||
| --------- | ------------------------------------ |
|
||||
| `id` | unique identifier or slug of heading |
|
||||
| `content` | Text content |
|
||||
| `content` | Text content (Markdown) |
|
||||
|
||||
#### Content
|
||||
|
||||
| Prop | |
|
||||
| --------- | ------------------------------- |
|
||||
| `heading` | Heading of paragraph (nullable) |
|
||||
| `content` | Text content |
|
||||
| `content` | Text content (Markdown) |
|
||||
|
||||
The content is stringified as Markdown text, you can customise it using the [`stringify`](#options) option.
|
||||
|
||||
## As a Function
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import type { ElementContent } from 'hast';
|
||||
import jsonSchema from 'fumadocs-mdx/plugins/json-schema';
|
||||
import lastModified from 'fumadocs-mdx/plugins/last-modified';
|
||||
import type { ShikiTransformer } from 'shiki';
|
||||
import type { RemarkFeedbackBlockOptions } from 'fumadocs-core/mdx-plugins';
|
||||
import type { RemarkAutoTypeTableOptions } from 'fumadocs-typescript';
|
||||
import { shikiConfig } from './lib/shiki';
|
||||
import { metaSchema, pageSchema } from 'fumadocs-core/source/schema';
|
||||
@@ -43,13 +42,6 @@ export const docs = defineDocs({
|
||||
const { remarkAutoTypeTable, createGenerator, createFileSystemGeneratorCache } =
|
||||
await import('fumadocs-typescript');
|
||||
|
||||
const feedbackOptions: RemarkFeedbackBlockOptions = {
|
||||
resolve(node) {
|
||||
// defensive approach
|
||||
if (node.type === 'mdxJsxFlowElement') return 'skip';
|
||||
return node.type === 'paragraph' || node.type === 'image' || node.type === 'list';
|
||||
},
|
||||
};
|
||||
const typeTableOptions: RemarkAutoTypeTableOptions = {
|
||||
generator: createGenerator({
|
||||
cache: createFileSystemGeneratorCache('.next/fumadocs-typescript'),
|
||||
@@ -77,6 +69,27 @@ export const docs = defineDocs({
|
||||
remarkCodeTabOptions: {
|
||||
parseMdx: true,
|
||||
},
|
||||
remarkStructureOptions: {
|
||||
stringify: {
|
||||
filterElement(node) {
|
||||
switch (node.type) {
|
||||
case 'mdxJsxFlowElement':
|
||||
case 'mdxJsxTextElement':
|
||||
switch (node.name) {
|
||||
case 'File':
|
||||
case 'TypeTable':
|
||||
case 'Callout':
|
||||
case 'Card':
|
||||
case 'Custom':
|
||||
return true;
|
||||
}
|
||||
return 'children-only';
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
},
|
||||
remarkNpmOptions: {
|
||||
persist: {
|
||||
id: 'package-manager',
|
||||
@@ -87,7 +100,7 @@ export const docs = defineDocs({
|
||||
: [
|
||||
remarkSteps,
|
||||
remarkMath,
|
||||
[remarkFeedbackBlock, feedbackOptions],
|
||||
remarkFeedbackBlock,
|
||||
[remarkAutoTypeTable, typeTableOptions],
|
||||
remarkTypeScriptToJavaScript,
|
||||
],
|
||||
|
||||
@@ -82,60 +82,56 @@ const mdRenderer = createMarkdownRenderer({
|
||||
rehypePlugins: [rehypeRaw, rehypeCustomElements],
|
||||
});
|
||||
|
||||
const badgeVariant = cva('rounded-md border px-0.5 bg-fd-muted text-fd-muted-foreground', {
|
||||
variants: {
|
||||
variant: {
|
||||
primary: 'bg-fd-primary text-fd-primary-foreground',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const mdComponents = {
|
||||
mark(props: ComponentProps<'mark'>) {
|
||||
return <span {...props} className={cn('text-fd-primary underline', props.className)} />;
|
||||
},
|
||||
a({ children }: ComponentProps<'a'>) {
|
||||
return <span>{children}</span>;
|
||||
},
|
||||
strong(props: ComponentProps<'strong'>) {
|
||||
return <strong {...props} className="font-medium" />;
|
||||
return <span {...props} className="text-fd-primary underline" />;
|
||||
},
|
||||
a: 'span',
|
||||
p(props: ComponentProps<'p'>) {
|
||||
return <p {...props} className="min-w-0" />;
|
||||
},
|
||||
strong(props: ComponentProps<'strong'>) {
|
||||
return <strong {...props} className="text-fd-accent-foreground font-medium" />;
|
||||
},
|
||||
code(props: ComponentProps<'pre'>) {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks -- this is a component
|
||||
const inPre = use(PreContext);
|
||||
if (inPre) return <code {...props} />;
|
||||
if (inPre)
|
||||
return (
|
||||
<code
|
||||
{...props}
|
||||
className="mask-[linear-gradient(to_bottom,white,white_30px,transparent_80px)]"
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<code
|
||||
{...props}
|
||||
className="border rounded-md -mx-0.5 px-0.5 bg-fd-secondary text-fd-secondary-foreground"
|
||||
className="border rounded-md px-px bg-fd-secondary text-fd-secondary-foreground"
|
||||
/>
|
||||
);
|
||||
},
|
||||
custom({
|
||||
_tagName,
|
||||
_tagName = 'fragment',
|
||||
children,
|
||||
...rest
|
||||
}: Record<string, unknown> & { _tagName: string; children: ReactNode }) {
|
||||
const propNodes = Object.entries(rest).map(([k, v]) => {
|
||||
if (typeof v !== 'string') return;
|
||||
|
||||
return (
|
||||
<code key={k} className={cn(badgeVariant(), 'truncate')}>
|
||||
{k}: {v}
|
||||
</code>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<span {...rest}>
|
||||
<span className="flex min-w-0 text-xs items-center gap-2">
|
||||
<code className={cn(badgeVariant({ variant: 'primary' }))}>{_tagName}</code>
|
||||
{propNodes}
|
||||
</span>
|
||||
{children}
|
||||
<span className="inline-flex max-w-full items-center border p-0.5 rounded-md bg-fd-card text-fd-card-foreground divide-x divide-fd-border">
|
||||
<code className="rounded-sm px-0.5 me-1 bg-fd-primary font-medium text-xs text-fd-primary-foreground border-none">
|
||||
{_tagName}
|
||||
</code>
|
||||
{Object.entries(rest).map(([k, v]) => {
|
||||
if (typeof v !== 'string') return;
|
||||
|
||||
return (
|
||||
<code key={k} className="truncate text-xs text-fd-muted-foreground px-1">
|
||||
<span className="text-fd-card-foreground">{k}: </span>
|
||||
{v}
|
||||
</code>
|
||||
);
|
||||
})}
|
||||
{children && <span className="ps-1">{children}</span>}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
@@ -144,7 +140,7 @@ const mdComponents = {
|
||||
<pre
|
||||
{...props}
|
||||
className={cn(
|
||||
'flex flex-col border rounded-md my-0.5 p-2 bg-fd-secondary text-fd-secondary-foreground max-h-20 overflow-hidden *:mask-[linear-gradient(to_bottom,white,white_30px,transparent_80px)]',
|
||||
'flex flex-col border rounded-md my-0.5 p-2 bg-fd-secondary text-fd-secondary-foreground max-h-20 overflow-hidden',
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
@@ -178,7 +174,11 @@ export function SearchDialog({
|
||||
children,
|
||||
}: SearchDialogProps) {
|
||||
const router = useRouter();
|
||||
const onSelect = useEffectEvent((item: SearchItemType) => {
|
||||
const onOpenChangeCallback = useRef(onOpenChange);
|
||||
onOpenChangeCallback.current = onOpenChange;
|
||||
const onSearchChangeCallback = useRef(onSearchChange);
|
||||
onSearchChangeCallback.current = onSearchChange;
|
||||
const onSelect = (item: SearchItemType) => {
|
||||
if (item.type === 'action') {
|
||||
item.onSelect();
|
||||
} else if (item.external) {
|
||||
@@ -189,7 +189,9 @@ export function SearchDialog({
|
||||
|
||||
onOpenChange(false);
|
||||
onSelectProp?.(item);
|
||||
});
|
||||
};
|
||||
const onSelectCallback = useRef(onSelect);
|
||||
onSelectCallback.current = onSelect;
|
||||
|
||||
return (
|
||||
<Dialog.Root open={open} onOpenChange={onOpenChange}>
|
||||
@@ -197,14 +199,13 @@ export function SearchDialog({
|
||||
value={useMemo(
|
||||
() => ({
|
||||
open,
|
||||
onOpenChange,
|
||||
search,
|
||||
onSearchChange,
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks -- used in child components
|
||||
onSelect,
|
||||
isLoading,
|
||||
onOpenChange: (v) => onOpenChangeCallback.current(v),
|
||||
onSearchChange: (v) => onSearchChangeCallback.current(v),
|
||||
onSelect: (v) => onSelectCallback.current(v),
|
||||
}),
|
||||
[isLoading, onOpenChange, onSearchChange, open, search],
|
||||
[isLoading, open, search],
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
@@ -492,7 +493,6 @@ export function SearchDialogListItem({
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function SearchDialogIcon(props: ComponentProps<'svg'>) {
|
||||
const { isLoading } = useSearch();
|
||||
|
||||
@@ -524,18 +524,19 @@ const itemVariants = cva(
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export function TagsList({ tag, onTagChange, allowClear = false, ...props }: TagsListProps) {
|
||||
const onTagChangeCallback = useRef(onTagChange);
|
||||
onTagChangeCallback.current = onTagChange;
|
||||
return (
|
||||
<div {...props} className={cn('flex items-center gap-1 flex-wrap', props.className)}>
|
||||
<TagsListContext
|
||||
value={useMemo(
|
||||
() => ({
|
||||
value: tag,
|
||||
onValueChange: onTagChange,
|
||||
onValueChange: (v) => onTagChangeCallback.current(v),
|
||||
allowClear,
|
||||
}),
|
||||
[allowClear, onTagChange, tag],
|
||||
[allowClear, tag],
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
@@ -559,9 +560,7 @@ export function TagsListItem({
|
||||
type="button"
|
||||
data-active={selected}
|
||||
className={cn(itemVariants({ active: selected, className }))}
|
||||
onClick={() => {
|
||||
onValueChange(selected && allowClear ? undefined : value);
|
||||
}}
|
||||
onClick={() => onValueChange(selected && allowClear ? undefined : value)}
|
||||
tabIndex={-1}
|
||||
{...props}
|
||||
>
|
||||
|
||||
@@ -8,7 +8,6 @@ export default [
|
||||
{
|
||||
rules: {
|
||||
'no-console': 'off',
|
||||
'no-fallthrough': 'off',
|
||||
// handled by bundler
|
||||
'import/no-cycle': 'off',
|
||||
},
|
||||
|
||||
@@ -19,7 +19,7 @@ export interface RemarkFeedbackBlockOptions {
|
||||
/**
|
||||
* determine how the node should be resolved into a feedback block.
|
||||
*
|
||||
* scan paragraph, list, and image nodes by default.
|
||||
* default: skip MDX elements, convert paragraph, list item, and image nodes.
|
||||
*
|
||||
* @returns
|
||||
* - `true`: convert the node into a feedback block.
|
||||
@@ -50,20 +50,31 @@ export interface FeedbackBlockProps {
|
||||
export function remarkFeedbackBlock({
|
||||
generateHash = ({ body }) => createHash('md5').update(body).digest('hex').substring(0, 16),
|
||||
tagName = 'FeedbackBlock',
|
||||
resolve = (node) => node.type === 'paragraph' || node.type === 'image' || node.type === 'list',
|
||||
resolve = (node) => {
|
||||
switch (node.type) {
|
||||
case 'mdxJsxFlowElement':
|
||||
return 'skip';
|
||||
case 'paragraph':
|
||||
case 'image':
|
||||
case 'listItem':
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
},
|
||||
generateBody = true,
|
||||
}: RemarkFeedbackBlockOptions = {}): Transformer<Root, Root> {
|
||||
return (tree) => {
|
||||
const counts = new Map<string, number>();
|
||||
|
||||
visit(tree, (node, index, parent) => {
|
||||
if (node.type === 'root') return;
|
||||
if (node.type === 'root' || !parent || typeof index !== 'number') return;
|
||||
const resolved = resolve(node);
|
||||
if (resolved === false) return;
|
||||
if (resolved === 'skip') return 'skip';
|
||||
|
||||
const text = flattenNode(node).trim();
|
||||
if (text.length === 0 || !parent || typeof index !== 'number') return;
|
||||
if (text.length === 0) return;
|
||||
let id = generateHash({ body: text });
|
||||
const count = counts.get(id) ?? 0;
|
||||
if (count > 0) id = `${id}-${count}`;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { Link, Nodes, Root } from 'mdast';
|
||||
import type { Heading, Link, Nodes, Root } from 'mdast';
|
||||
import { remark } from 'remark';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import type { PluggableList, Processor, Transformer } from 'unified';
|
||||
import { visit } from 'unist-util-visit';
|
||||
import { flattenNode, toMdxExport } from './mdast-utils';
|
||||
import { toMdxExport } from './mdast-utils';
|
||||
import type {
|
||||
MdxJsxAttribute,
|
||||
MdxJsxExpressionAttribute,
|
||||
@@ -37,13 +37,34 @@ interface StringifierContext {
|
||||
addContent: (...content: StructuredDataContent[]) => void;
|
||||
}
|
||||
|
||||
interface StringifyOptions {
|
||||
export interface StringifyOptions {
|
||||
/**
|
||||
* Filter the elements to be included in the output:
|
||||
*
|
||||
* - `true`: include element & its children.
|
||||
* - `children-only`: exclude element but keep its children.
|
||||
* - `false`: exclude element & its children.
|
||||
*
|
||||
* Default:
|
||||
*
|
||||
* ```ts
|
||||
* filterElement = (node) => {
|
||||
* switch (node.type) {
|
||||
* case 'mdxJsxFlowElement':
|
||||
* case 'mdxJsxTextElement':
|
||||
* switch (node.name) {
|
||||
* case 'File':
|
||||
* case 'TypeTable':
|
||||
* case 'Callout':
|
||||
* case 'Card':
|
||||
* return true;
|
||||
* }
|
||||
* return 'children-only';
|
||||
* }
|
||||
*
|
||||
* return true;
|
||||
* },
|
||||
* ```
|
||||
*/
|
||||
filterElement?: (node: Nodes) => boolean | 'children-only';
|
||||
|
||||
@@ -60,14 +81,14 @@ export interface StructureOptions {
|
||||
/**
|
||||
* MDAST node types to be scanned as a content block.
|
||||
*
|
||||
* If a node's type represents in this array, it will be converted into a single content block.
|
||||
* If a node's type is listed in this array, it will be converted into a single content block.
|
||||
*
|
||||
* @defaultValue ['heading', 'paragraph', 'blockquote', 'tableCell', 'mdxJsxFlowElement']
|
||||
*/
|
||||
types?: string[] | ((node: Nodes) => boolean);
|
||||
|
||||
/**
|
||||
* stringify a given node & its children, you can use something like `mdast-util-to-markdown`.
|
||||
* stringify text content from a MDAST node.
|
||||
*/
|
||||
stringify?: Stringifier | StringifyOptions;
|
||||
|
||||
@@ -148,7 +169,7 @@ export function remarkStructure(
|
||||
const stringify =
|
||||
typeof stringifyOptions === 'function'
|
||||
? stringifyOptions
|
||||
: defaultStringify({
|
||||
: defaultStringifier({
|
||||
filterMdxAttributes: Array.isArray(allowedMdxAttributes)
|
||||
? (_node, attribute) =>
|
||||
attribute.type === 'mdxJsxAttribute' &&
|
||||
@@ -191,26 +212,28 @@ export function remarkStructure(
|
||||
case 'mdxJsxTextElement':
|
||||
if (!mdxTypes(element)) return;
|
||||
break;
|
||||
}
|
||||
case 'heading': {
|
||||
element.data ||= {};
|
||||
element.data.hProperties ||= {};
|
||||
const id = element.data.hProperties.id;
|
||||
if (typeof id !== 'string') {
|
||||
console.warn(
|
||||
'[remark-structure] hProperties.id is missing in heading node, it is required to generate heading data. You can add remark-heading prior to remark-structure to generate heading IDs.',
|
||||
);
|
||||
return 'skip';
|
||||
}
|
||||
|
||||
if (element.type === 'heading') {
|
||||
element.data ||= {};
|
||||
element.data.hProperties ||= {};
|
||||
const id = element.data.hProperties.id;
|
||||
if (typeof id !== 'string') {
|
||||
console.warn(
|
||||
'[remark-structure] hProperties.id is missing in heading node, it is required to generate heading data. You can add remark-heading prior to remark-structure to generate heading IDs.',
|
||||
);
|
||||
const content = stringify.call(this, element, stringifierCtx).trim();
|
||||
if (content.length > 0) {
|
||||
data.headings.push({
|
||||
id,
|
||||
content,
|
||||
});
|
||||
}
|
||||
|
||||
lastHeading = id;
|
||||
return 'skip';
|
||||
}
|
||||
|
||||
data.headings.push({
|
||||
id,
|
||||
content: flattenNode(element).trim(),
|
||||
});
|
||||
|
||||
lastHeading = id;
|
||||
return 'skip';
|
||||
}
|
||||
|
||||
const content = stringify.call(this, element, stringifierCtx).trim();
|
||||
@@ -257,7 +280,7 @@ interface CustomRootNode {
|
||||
ctx: StringifierContext;
|
||||
}
|
||||
|
||||
export function defaultStringify(config: Options & StringifyOptions = {}): Stringifier {
|
||||
export function defaultStringifier(config: Options & StringifyOptions = {}): Stringifier {
|
||||
const {
|
||||
filterMdxAttributes,
|
||||
filterElement = (node) => {
|
||||
@@ -283,7 +306,7 @@ export function defaultStringify(config: Options & StringifyOptions = {}): Strin
|
||||
const { structuredData, _string } = node.data ?? {};
|
||||
if (structuredData) ctx.addContent(...structuredData.contents);
|
||||
if (_string) return typeof _string === 'function' ? _string() : _string;
|
||||
const visibility = filterElement ? filterElement(node) : true;
|
||||
const visibility = filterElement(node);
|
||||
|
||||
if (visibility === false) return '';
|
||||
|
||||
@@ -330,6 +353,9 @@ export function defaultStringify(config: Options & StringifyOptions = {}): Strin
|
||||
link(node: Link, _, state, info) {
|
||||
return state.containerPhrasing(node, info);
|
||||
},
|
||||
heading(node: Heading, _, state, info) {
|
||||
return state.containerPhrasing(node, info);
|
||||
},
|
||||
image() {
|
||||
return '';
|
||||
},
|
||||
@@ -348,7 +374,7 @@ export function defaultStringify(config: Options & StringifyOptions = {}): Strin
|
||||
// from https://github.com/remarkjs/remark/blob/main/packages/remark-stringify/lib/index.js
|
||||
const defaultExtensions = this.data('toMarkdownExtensions') ?? [];
|
||||
|
||||
return toMarkdown({ type: '_custom', root, ctx } as never, {
|
||||
return toMarkdown({ type: '_custom', root, ctx } satisfies CustomRootNode as never, {
|
||||
...this.data('settings'),
|
||||
...config,
|
||||
extensions: config.extensions
|
||||
|
||||
@@ -82,21 +82,11 @@ const mdRenderer = createMarkdownRenderer({
|
||||
rehypePlugins: [rehypeRaw, rehypeCustomElements],
|
||||
});
|
||||
|
||||
const badgeVariant = cva('rounded-md border px-0.5 bg-fd-muted text-fd-muted-foreground', {
|
||||
variants: {
|
||||
variant: {
|
||||
primary: 'bg-fd-primary text-fd-primary-foreground',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const mdComponents = {
|
||||
mark(props: ComponentProps<'mark'>) {
|
||||
return <span {...props} className={cn('text-fd-primary underline', props.className)} />;
|
||||
},
|
||||
a({ children }: ComponentProps<'a'>) {
|
||||
return <span>{children}</span>;
|
||||
return <span {...props} className="text-fd-primary underline" />;
|
||||
},
|
||||
a: 'span',
|
||||
p(props: ComponentProps<'p'>) {
|
||||
return <p {...props} className="min-w-0" />;
|
||||
},
|
||||
@@ -106,36 +96,42 @@ const mdComponents = {
|
||||
code(props: ComponentProps<'pre'>) {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks -- this is a component
|
||||
const inPre = use(PreContext);
|
||||
if (inPre) return <code {...props} />;
|
||||
if (inPre)
|
||||
return (
|
||||
<code
|
||||
{...props}
|
||||
className="mask-[linear-gradient(to_bottom,white,white_30px,transparent_80px)]"
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<code
|
||||
{...props}
|
||||
className="border rounded-md -mx-0.5 px-0.5 bg-fd-secondary text-fd-secondary-foreground"
|
||||
className="border rounded-md px-px bg-fd-secondary text-fd-secondary-foreground"
|
||||
/>
|
||||
);
|
||||
},
|
||||
custom({
|
||||
_tagName,
|
||||
_tagName = 'fragment',
|
||||
children,
|
||||
...rest
|
||||
}: Record<string, unknown> & { _tagName: string; children: ReactNode }) {
|
||||
const propNodes = Object.entries(rest).map(([k, v]) => {
|
||||
if (typeof v !== 'string') return;
|
||||
|
||||
return (
|
||||
<code key={k} className={cn(badgeVariant(), 'truncate')}>
|
||||
{k}: {v}
|
||||
</code>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<span {...rest}>
|
||||
<span className="flex min-w-0 text-xs items-center gap-2">
|
||||
<code className={cn(badgeVariant({ variant: 'primary' }))}>{_tagName}</code>
|
||||
{propNodes}
|
||||
</span>
|
||||
{children}
|
||||
<span className="inline-flex max-w-full items-center border p-0.5 rounded-md bg-fd-card text-fd-card-foreground divide-x divide-fd-border">
|
||||
<code className="rounded-sm px-0.5 me-1 bg-fd-primary font-medium text-xs text-fd-primary-foreground border-none">
|
||||
{_tagName}
|
||||
</code>
|
||||
{Object.entries(rest).map(([k, v]) => {
|
||||
if (typeof v !== 'string') return;
|
||||
|
||||
return (
|
||||
<code key={k} className="truncate text-xs text-fd-muted-foreground px-1">
|
||||
<span className="text-fd-card-foreground">{k}: </span>
|
||||
{v}
|
||||
</code>
|
||||
);
|
||||
})}
|
||||
{children && <span className="ps-1">{children}</span>}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
@@ -144,7 +140,7 @@ const mdComponents = {
|
||||
<pre
|
||||
{...props}
|
||||
className={cn(
|
||||
'flex flex-col border rounded-md my-0.5 p-2 bg-fd-secondary text-fd-secondary-foreground max-h-20 overflow-hidden *:mask-[linear-gradient(to_bottom,white,white_30px,transparent_80px)]',
|
||||
'flex flex-col border rounded-md my-0.5 p-2 bg-fd-secondary text-fd-secondary-foreground max-h-20 overflow-hidden',
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
@@ -178,7 +174,11 @@ export function SearchDialog({
|
||||
children,
|
||||
}: SearchDialogProps) {
|
||||
const router = useRouter();
|
||||
const onSelect = useEffectEvent((item: SearchItemType) => {
|
||||
const onOpenChangeCallback = useRef(onOpenChange);
|
||||
onOpenChangeCallback.current = onOpenChange;
|
||||
const onSearchChangeCallback = useRef(onSearchChange);
|
||||
onSearchChangeCallback.current = onSearchChange;
|
||||
const onSelect = (item: SearchItemType) => {
|
||||
if (item.type === 'action') {
|
||||
item.onSelect();
|
||||
} else if (item.external) {
|
||||
@@ -189,7 +189,9 @@ export function SearchDialog({
|
||||
|
||||
onOpenChange(false);
|
||||
onSelectProp?.(item);
|
||||
});
|
||||
};
|
||||
const onSelectCallback = useRef(onSelect);
|
||||
onSelectCallback.current = onSelect;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
@@ -197,14 +199,13 @@ export function SearchDialog({
|
||||
value={useMemo(
|
||||
() => ({
|
||||
open,
|
||||
onOpenChange,
|
||||
search,
|
||||
onSearchChange,
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks -- used in child components
|
||||
onSelect,
|
||||
isLoading,
|
||||
onOpenChange: (v) => onOpenChangeCallback.current(v),
|
||||
onSearchChange: (v) => onSearchChangeCallback.current(v),
|
||||
onSelect: (v) => onSelectCallback.current(v),
|
||||
}),
|
||||
[isLoading, onOpenChange, onSearchChange, open, search],
|
||||
[isLoading, open, search],
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
@@ -513,16 +514,18 @@ const itemVariants = cva(
|
||||
);
|
||||
|
||||
export function TagsList({ tag, onTagChange, allowClear = false, ...props }: TagsListProps) {
|
||||
const onTagChangeCallback = useRef(onTagChange);
|
||||
onTagChangeCallback.current = onTagChange;
|
||||
return (
|
||||
<div {...props} className={cn('flex items-center gap-1 flex-wrap', props.className)}>
|
||||
<TagsListContext
|
||||
value={useMemo(
|
||||
() => ({
|
||||
value: tag,
|
||||
onValueChange: onTagChange,
|
||||
onValueChange: (v) => onTagChangeCallback.current(v),
|
||||
allowClear,
|
||||
}),
|
||||
[allowClear, onTagChange, tag],
|
||||
[allowClear, tag],
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
@@ -546,9 +549,7 @@ export function TagsListItem({
|
||||
type="button"
|
||||
data-active={selected}
|
||||
className={cn(itemVariants({ active: selected, className }))}
|
||||
onClick={() => {
|
||||
onValueChange(selected && allowClear ? undefined : value);
|
||||
}}
|
||||
onClick={() => onValueChange(selected && allowClear ? undefined : value)}
|
||||
tabIndex={-1}
|
||||
{...props}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user