diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bea3cc92b0..c4f699ea69b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ ## vNEXT (not yet published) +### `@liveblocks/react-ui` + +- Improve Markdown lists in `AiChat`: better spacing and support for arbitrary + starting numbers in ordered lists. (e.g. `3.` instead of `1.`) + ## v3.2.0 ### `@liveblocks/react-ui` diff --git a/docs/pages/api-reference/liveblocks-react-ui.mdx b/docs/pages/api-reference/liveblocks-react-ui.mdx index 65bb9dde8ae..a40a6267c6f 100644 --- a/docs/pages/api-reference/liveblocks-react-ui.mdx +++ b/docs/pages/api-reference/liveblocks-react-ui.mdx @@ -522,7 +522,7 @@ Override specific parts of `AiChat` with custom components. ReactNode`} + detailedType={`({ type: "ordered" | "unordered", items: { checked?: boolean, children: ReactNode }[], start?: number }) => ReactNode`} > The component used to render lists. diff --git a/e2e/next-ai-kitchen-sink/app/styles/page.tsx b/e2e/next-ai-kitchen-sink/app/styles/page.tsx index 27f1c9470fb..c88b27cf8c2 100644 --- a/e2e/next-ai-kitchen-sink/app/styles/page.tsx +++ b/e2e/next-ai-kitchen-sink/app/styles/page.tsx @@ -344,6 +344,42 @@ Just some plain text --- +1. A numbered list item +- A "nested" list item +- Another "nested" list item + +2. Another numbered list item +- A "nested" list item +- Another "nested" list item + +3. Yet another numbered list item +- A "nested" list item +- Another "nested" list item + +--- + +1. A numbered list item + +\`\`\` +const a = 2; +\`\`\` + +2. Another numbered list item + +> A quote. + +3. Yet another numbered list item + +A paragraph. + +--- + +1. A numbered list item +1. Another numbered list item +1. Yet another numbered list item + +--- + The abbreviation for HyperText Markup Language is HTML. Press Ctrl + C to copy. diff --git a/packages/liveblocks-react-ui/src/primitives/Markdown.tsx b/packages/liveblocks-react-ui/src/primitives/Markdown.tsx index fbf9ea73223..ec64ddaacc2 100644 --- a/packages/liveblocks-react-ui/src/primitives/Markdown.tsx +++ b/packages/liveblocks-react-ui/src/primitives/Markdown.tsx @@ -1,4 +1,4 @@ -import { assertNever, sanitizeUrl } from "@liveblocks/core"; +import { assertNever, type Relax, sanitizeUrl } from "@liveblocks/core"; import { Slot } from "@radix-ui/react-slot"; import { Lexer, type MarkedToken, type Token, type Tokens } from "marked"; import { @@ -159,10 +159,10 @@ export type MarkdownComponents = { * ```tsx * { + * List: ({ type, items, start }) => { * const List = type === "ordered" ? "ol" : "ul"; * return ( - * + * * {items.map((item, index) => ( *
  • * {item.checked !== undefined && ( @@ -257,8 +257,18 @@ interface MarkdownComponentsListItem { children: ReactNode; } -export interface MarkdownComponentsListProps { - type: "ordered" | "unordered"; +export type MarkdownComponentsListProps = Relax< + MarkdownComponentsOrderedListProps | MarkdownComponentsUnorderedListProps +>; + +interface MarkdownComponentsOrderedListProps { + type: "ordered"; + items: MarkdownComponentsListItem[]; + start: number; +} + +interface MarkdownComponentsUnorderedListProps { + type: "unordered"; items: MarkdownComponentsListItem[]; } @@ -368,11 +378,11 @@ const defaultComponents: MarkdownComponents = { ); }, - List: ({ type, items }) => { + List: ({ type, items, start }) => { const List = type === "ordered" ? "ol" : "ul"; return ( - + {items.map((item, index) => (
  • {item.checked !== undefined && ( @@ -586,9 +596,11 @@ export function MarkdownToken({ }; }); - return ( - - ); + const props: MarkdownComponentsListProps = token.ordered + ? { type: "ordered", items, start: token.start || 1 } + : { type: "unordered", items }; + + return ; } case "table": { diff --git a/packages/liveblocks-react-ui/src/primitives/__tests__/Markdown.test.tsx b/packages/liveblocks-react-ui/src/primitives/__tests__/Markdown.test.tsx index 3998c1687e5..64cc6c8ef8e 100644 --- a/packages/liveblocks-react-ui/src/primitives/__tests__/Markdown.test.tsx +++ b/packages/liveblocks-react-ui/src/primitives/__tests__/Markdown.test.tsx @@ -319,6 +319,95 @@ describe("Markdown", () => { ); }, }, + { + description: "numbered lists with arbitrary start indices", + content: dedent` + 1. A numbered list item + - A "nested" list item + - Another "nested" list item + + 2. Another numbered list item + - A "nested" list item + - Another "nested" list item + + 3. Yet another numbered list item + - A "nested" list item + - Another "nested" list item + + --- + + 1. A numbered list item + + \`\`\` + const a = 2; + \`\`\` + + 2. Another numbered list item + + > A quote. + + 3. Yet another numbered list item + + A paragraph. + + --- + + 1. A numbered list item + 1. Another numbered list item + 1. Yet another numbered list item + `, + assertions: (element) => { + const listItems = element.querySelectorAll("li"); + expect(listItems).toHaveLength(15); + + // 1. A numbered list item + // - A "nested" list item + // - Another "nested" list item + // + // 2. Another numbered list item + // - A "nested" list item + // - Another "nested" list item + // + // 3. Yet another numbered list item + // - A "nested" list item + // - Another "nested" list item + const firstListFirstItem = listItems[0]?.parentElement; + expect(firstListFirstItem).not.toHaveAttribute("start"); + const firstListSecondItem = listItems[3]?.parentElement; + expect(firstListSecondItem).toHaveAttribute("start", "2"); + const firstListThirdItem = listItems[6]?.parentElement; + expect(firstListThirdItem).toHaveAttribute("start", "3"); + + // 1. A numbered list item + // + // \`\`\` + // const a = 2; + // \`\`\` + // + // 2. Another numbered list item + // + // > A quote. + // + // 3. Yet another numbered list item + // + // A paragraph. + const secondListFirstItem = listItems[9]?.parentElement; + expect(secondListFirstItem).not.toHaveAttribute("start"); + const secondListSecondItem = listItems[10]?.parentElement; + expect(secondListSecondItem).toHaveAttribute("start", "2"); + const secondListThirdItem = listItems[11]?.parentElement; + expect(secondListThirdItem).toHaveAttribute("start", "3"); + + // 1. A numbered list item + // 1. Another numbered list item + // 1. Yet another numbered list item + const thirdList = document.querySelector("ol:last-of-type"); + expect(listItems[12]?.parentElement).toBe(thirdList); + expect(listItems[13]?.parentElement).toBe(thirdList); + expect(listItems[14]?.parentElement).toBe(thirdList); + expect(thirdList).not.toHaveAttribute("start"); + }, + }, { description: "blockquotes", content: dedent` @@ -664,11 +753,11 @@ describe("Markdown", () => { + [x] Yet another list item `, components: { - List: ({ items, type }) => { + List: ({ items, type, start }) => { const List = type === "ordered" ? "ol" : "ul"; return ( - + {items.map((item, index) => (
  • {item.checked !== undefined && ( diff --git a/packages/liveblocks-react-ui/src/styles/index.css b/packages/liveblocks-react-ui/src/styles/index.css index d3033b4afc0..35f1bde236e 100644 --- a/packages/liveblocks-react-ui/src/styles/index.css +++ b/packages/liveblocks-react-ui/src/styles/index.css @@ -604,7 +604,7 @@ flex-direction: column; gap: 0.25em; margin-block: 0.75em; - padding-inline-start: 1.5em; + padding-inline-start: 1.125em; list-style-position: outside; :where(ol, ul) {