diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/BotMessage.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/BotMessage.tsx index 18e381d0e..ce79badce 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/BotMessage.tsx +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/BotMessage.tsx @@ -1,4 +1,12 @@ -import { CSSProperties, useState, Fragment, FunctionComponent, MouseEvent, Ref } from 'react'; +import { + CSSProperties, + useState, + Fragment, + FunctionComponent, + MouseEvent as ReactMouseEvent, + KeyboardEvent as ReactKeyboardEvent, + Ref +} from 'react'; import Message from '@patternfly/chatbot/dist/dynamic/Message'; import patternflyAvatar from './patternfly_avatar.jpg'; import squareImg from './PF-social-color-square.svg'; @@ -44,6 +52,8 @@ export const BotMessageExample: FunctionComponent = () => { return table; case 'Image': return image; + case 'Footnote': + return footnote; default: return; } @@ -150,6 +160,18 @@ _Italic text, formatted with single underscores_ const image = `![Multi-colored wavy lines on a black background](https://cdn.dribbble.com/userupload/10651749/file/original-8a07b8e39d9e8bf002358c66fce1223e.gif)`; + const footnote = `This is some text that has a short footnote[^1] and this is text with a longer footnote.[^bignote] + + [^1]: This is a short footnote. To return the highlight to the original message, click the arrow. + + [^bignote]: This is a long footnote with multiple paragraphs and formatting. + + To break long footnotes into paragraphs, indent the text. + + Add as many paragraphs as you like. You can include *italic text*, **bold text**, and \`code\`. + + > You can even include blockquotes in footnotes!`; + const error = { title: 'Could not load chat', children: 'Wait a few minutes and check your network settings. If the issue persists: ', @@ -165,8 +187,8 @@ _Italic text, formatted with single underscores_ ) }; - const onSelect = (_event: MouseEvent | undefined, value: string | number | undefined) => { - setVariant(value); + const onSelect = (_event: ReactMouseEvent | undefined, value: string | number | undefined) => { + setVariant(value as string); setSelected(value as string); setIsOpen(false); if (value === 'Expandable code') { @@ -196,6 +218,76 @@ _Italic text, formatted with single underscores_ ); + const handleFootnoteNavigation = (event: ReactMouseEvent | ReactKeyboardEvent) => { + const target = event.target as HTMLElement; + + // Depending on whether it is a click event or keyboard event, target may be a link or something like a span + // Look for the closest anchor element (could be a parent) + const anchorElement = target.closest('a'); + const href = anchorElement?.getAttribute('href'); + + // Check if this is a footnote link - we only have internal links in this example, so this is all we need here + if (href && href.startsWith('#')) { + // Prevent default behavior to avoid page re-render on click in PatternFly docs framework + event.preventDefault(); + + let targetElement: HTMLElement | null = null; + const targetId = href.replace('#', ''); + targetElement = document.querySelector(`[id="${targetId}"]`); + + if (targetElement) { + let focusTarget = targetElement; + + // If we found a footnote definition container, focus on the parent li element + if (targetElement.id?.startsWith('bot-message-fn-')) { + // Find the parent li element that contains the footnote + const parentLi = targetElement.closest('li'); + if (parentLi) { + focusTarget = parentLi as HTMLElement; + } + } + + focusTarget.focus(); + + let elementToHighlight = targetElement; + + // If this is a backref link (going back to footnote reference), + // we want to highlight more of the ref line and not just the link itself + // since the target is so small + if (targetElement.id?.startsWith('bot-message-fnref-')) { + const refLink = targetElement; + + // Walk up the DOM to find a meaningful container + let parent = refLink.parentElement; + while (parent && parent.tagName.toLowerCase() !== 'p' && parent !== document.body) { + parent = parent.parentElement; + } + + // Use if found, otherwise use the immediate parent or target as a fallback + elementToHighlight = parent || refLink.parentElement || targetElement; + } + + // Briefly highlight the target element for fun to show what you can do + const originalBackground = elementToHighlight.style.backgroundColor; + const originalTransition = elementToHighlight.style.transition; + + elementToHighlight.style.transition = 'background-color 0.3s ease'; + elementToHighlight.style.backgroundColor = 'var(--pf-t--global--background--color--tertiary--default)'; + + setTimeout(() => { + elementToHighlight.style.backgroundColor = originalBackground; + setTimeout(() => { + elementToHighlight.style.transition = originalTransition; + }, 300); + }, 1000); + } + } + }; + + const onClick = (event: ReactMouseEvent | ReactKeyboardEvent) => { + handleFootnoteNavigation(event); + }; + return ( <> More complex list Table Image + Footnote Error @@ -265,6 +358,11 @@ _Italic text, formatted with single underscores_ // The purpose of this plugin is to provide unique link names for the code blocks // Because they are in the same message, this requires a custom plugin to parse the syntax tree additionalRehypePlugins={[rehypeCodeBlockToggle]} + linkProps={{ onClick }} + // clobberPrefix controls the label ids + reactMarkdownProps={{ + remarkRehypeOptions: { footnoteLabel: 'Bot message footnotes', clobberPrefix: 'bot-message-' } + }} /> ); diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/UserMessage.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/UserMessage.tsx index 482a207ff..36e571380 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/UserMessage.tsx +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/UserMessage.tsx @@ -1,4 +1,14 @@ -import { Fragment, useState, useRef, useEffect, CSSProperties, FunctionComponent, MouseEvent, Ref } from 'react'; +import { + Fragment, + useState, + useRef, + useEffect, + CSSProperties, + FunctionComponent, + MouseEvent as ReactMouseEvent, + KeyboardEvent as ReactKeyboardEvent, + Ref +} from 'react'; import Message from '@patternfly/chatbot/dist/dynamic/Message'; import userAvatar from './user_avatar.svg'; import { @@ -64,6 +74,8 @@ export const UserMessageExample: FunctionComponent = () => { return table; case 'Image': return image; + case 'Footnote': + return footnote; default: return ''; } @@ -170,6 +182,18 @@ _Italic text, formatted with single underscores_ const image = `![Multi-colored wavy lines on a black background](https://cdn.dribbble.com/userupload/10651749/file/original-8a07b8e39d9e8bf002358c66fce1223e.gif)`; + const footnote = `This is some text that has a short footnote[^1] and this is text with a longer footnote.[^bignote] + + [^1]: This is a short footnote. To return the highlight to the original message, click the arrow. + + [^bignote]: This is a long footnote with multiple paragraphs and formatting. + + To break long footnotes into paragraphs, indent the text. + + Add as many paragraphs as you like. You can include *italic text*, **bold text**, and \`code\`. + + > You can even include blockquotes in footnotes!`; + const error = { title: 'Could not load chat', children: 'Wait a few minutes and check your network settings. If the issue persists: ', @@ -185,7 +209,7 @@ _Italic text, formatted with single underscores_ ) }; - const onSelect = (_event: MouseEvent | undefined, value: string | number | undefined) => { + const onSelect = (_event: ReactMouseEvent | undefined, value: string | number | undefined) => { setVariant(value); setSelected(value as string); setIsOpen(false); @@ -221,6 +245,78 @@ _Italic text, formatted with single underscores_ ); + const handleFootnoteNavigation = (event: ReactMouseEvent | ReactKeyboardEvent) => { + const target = event.target as HTMLElement; + + // Depending on whether it is a click event or keyboard event, target may be a link or something like a span + // Look for the closest anchor element (could be a parent) + const anchorElement = target.closest('a'); + const href = anchorElement?.getAttribute('href'); + + // Check if this is a footnote link - we only have internal links in this example, so this is all we need here + if (href && href.startsWith('#')) { + // Prevent default behavior to avoid page re-render on click in PatternFly docs framework + event.preventDefault(); + + let targetElement: HTMLElement | null = null; + const targetId = href.replace('#', ''); + targetElement = document.querySelector(`[id="${targetId}"]`); + + if (targetElement) { + let focusTarget = targetElement; + + // If we found a footnote definition container, focus on the parent li element + if (targetElement.id?.startsWith('user-message-fn-')) { + // Find the parent li element that contains the footnote + const parentLi = targetElement.closest('li'); + if (parentLi) { + focusTarget = parentLi as HTMLElement; + } + } + + focusTarget.focus(); + + let elementToHighlight = targetElement; + const searchStartElement = targetElement; + let elementToHighlightContainer: HTMLElement | null = null; + + // For footnote references, look for an appropriate container + if (!targetElement.id?.startsWith('user-message-fn-')) { + let parent = searchStartElement.parentElement; + while ( + parent && + !(parent.tagName.toLowerCase() === 'span' && parent.classList.contains('pf-chatbot__message-text')) && + parent !== document.body + ) { + parent = parent.parentElement; + } + elementToHighlightContainer = parent; + } + + // Use the found container if available, otherwise fall back to the target element + elementToHighlight = elementToHighlightContainer || targetElement; + + // Briefly highlight the target element for fun to show what you can do + const originalBackground = elementToHighlight.style.backgroundColor; + const originalTransition = elementToHighlight.style.transition; + + elementToHighlight.style.transition = 'background-color 0.3s ease'; + elementToHighlight.style.backgroundColor = 'var(--pf-t--global--icon--color--brand--hover)'; + + setTimeout(() => { + elementToHighlight.style.backgroundColor = originalBackground; + setTimeout(() => { + elementToHighlight.style.transition = originalTransition; + }, 300); + }, 1000); + } + } + }; + + const onClick = (event: ReactMouseEvent | ReactKeyboardEvent) => { + handleFootnoteNavigation(event); + }; + return ( <> More complex list Table Image + Footnote Error @@ -287,6 +384,14 @@ _Italic text, formatted with single underscores_ // The purpose of this plugin is to provide unique link names for the code blocks // Because they are in the same message, this requires a custom plugin to parse the syntax tree additionalRehypePlugins={[rehypeCodeBlockToggle]} + linkProps={{ onClick }} + // clobberPrefix controls the label ids + reactMarkdownProps={{ + remarkRehypeOptions: { + footnoteLabel: 'User message footnotes', + clobberPrefix: 'user-message-' + } + }} /> ); diff --git a/packages/module/src/Message/CodeBlockMessage/CodeBlockMessage.scss b/packages/module/src/Message/CodeBlockMessage/CodeBlockMessage.scss index 50ece9bde..8d92fefb0 100644 --- a/packages/module/src/Message/CodeBlockMessage/CodeBlockMessage.scss +++ b/packages/module/src/Message/CodeBlockMessage/CodeBlockMessage.scss @@ -77,8 +77,9 @@ } .pf-chatbot__message-inline-code { + --pf-chatbot-message-text-inline-code-font-size: var(--pf-t--global--font--size--body--default); background-color: var(--pf-t--global--background--color--tertiary--default); - font-size: var(--pf-t--global--font--size--body--default); + font-size: var(--pf-chatbot-message-text-inline-code-font-size); } .pf-chatbot__message-code-toggle { diff --git a/packages/module/src/Message/LinkMessage/LinkMessage.tsx b/packages/module/src/Message/LinkMessage/LinkMessage.tsx index 45091bd8b..de32e46b6 100644 --- a/packages/module/src/Message/LinkMessage/LinkMessage.tsx +++ b/packages/module/src/Message/LinkMessage/LinkMessage.tsx @@ -4,8 +4,9 @@ import { Button, ButtonProps } from '@patternfly/react-core'; import { ExternalLinkSquareAltIcon } from '@patternfly/react-icons'; +import { ExtraProps } from 'react-markdown'; -const LinkMessage = ({ children, target, href, ...props }: ButtonProps) => { +const LinkMessage = ({ children, target, href, id, ...props }: ButtonProps & ExtraProps) => { if (target === '_blank') { return ( ); diff --git a/packages/module/src/Message/ListMessage/ListItemMessage.tsx b/packages/module/src/Message/ListMessage/ListItemMessage.tsx index 74872a95c..2762ba18f 100644 --- a/packages/module/src/Message/ListMessage/ListItemMessage.tsx +++ b/packages/module/src/Message/ListMessage/ListItemMessage.tsx @@ -5,6 +5,10 @@ import { ExtraProps } from 'react-markdown'; import { ListItem } from '@patternfly/react-core'; -const ListItemMessage = ({ children }: JSX.IntrinsicElements['li'] & ExtraProps) => {children}; +const ListItemMessage = ({ children, ...props }: JSX.IntrinsicElements['li'] & ExtraProps) => ( + + {children} + +); export default ListItemMessage; diff --git a/packages/module/src/Message/ListMessage/ListMessage.scss b/packages/module/src/Message/ListMessage/ListMessage.scss index 3f8e109ad..0dfb7fcd5 100644 --- a/packages/module/src/Message/ListMessage/ListMessage.scss +++ b/packages/module/src/Message/ListMessage/ListMessage.scss @@ -21,5 +21,22 @@ background-color: var(--pf-t--global--color--brand--default); color: var(--pf-t--global--text--color--on-brand--default); padding: var(--pf-t--global--spacer--sm); + + // prevents issues when highlighting things like footnotes - don't have blue on blue + .pf-chatbot__message-text { + background-color: initial; + } + } + + // targets footnotes specifically and prevents misalignment problems + .footnotes { + li > span { + display: inline-flex; + flex-direction: column; + } + } + + li a { + color: var(--pf-t--global--text--color--on-brand--default); } } diff --git a/packages/module/src/Message/Message.scss b/packages/module/src/Message/Message.scss index dd1c6e0c2..2481d7e9f 100644 --- a/packages/module/src/Message/Message.scss +++ b/packages/module/src/Message/Message.scss @@ -89,6 +89,49 @@ display: grid; gap: var(--pf-t--global--spacer--sm); } + + // targets footnotes specifically + .footnotes, + .pf-chatbot__message-text.footnotes { + padding: var(--pf-t--global--spacer--sm) var(--pf-t--global--spacer--sm) 0 var(--pf-t--global--spacer--sm); + --pf-chatbot-message-text-font-size: var(--pf-t--global--font--size--xs); + --pf-chatbot-message-text-inline-code-font-size: var(--pf-t--global--font--size--xs); + + .pf-chatbot__message-text h1, + h2, + h3, + h4, + h5, + h6 { + --pf-v6-c-content--h1--FontSize: var(--pf-t--global--font--size--md); + --pf-v6-c-content--h2--FontSize: var(--pf-t--global--font--size--md); + --pf-v6-c-content--h3--FontSize: var(--pf-t--global--font--size--md); + --pf-v6-c-content--h4--FontSize: var(--pf-t--global--font--size--md); + --pf-v6-c-content--h5--FontSize: var(--pf-t--global--font--size--md); + --pf-v6-c-content--h6--FontSize: var(--pf-t--global--font--size--md); + } + .pf-chatbot__message-text .pf-v6-c-content, + .pf-chatbot__message-text .pf-v6-c-content--small, + .pf-chatbot__message-text .pf-v6-c-content--blockquote, + .pf-chatbot__message-text p, + .pf-chatbot__message-text a { + --pf-v6-c-content--FontSize: var(--pf-t--global--font--size--xs); + } + .pf-chatbot__message-inline-code, + .pf-chatbot__message-text .pf-v6-c-button.pf-m-link, + .pf-chatbot__message-ordered-list .pf-v6-c-list, + .pf-chatbot__message-ordered-list ul, + .pf-chatbot__message-ordered-list li, + .pf-chatbot__message-unordered-list .pf-v6-c-list, + .pf-chatbot__message-unordered-list ul, + .pf-chatbot__message-unordered-list li { + font-size: var(--pf-t--global--font--size--xs); + } + } + + .footnotes { + background-color: var(--pf-t--global--background--color--tertiary--default); + } } // Attachments @@ -106,6 +149,7 @@ @import './MessageLoading'; @import './CodeBlockMessage/CodeBlockMessage'; @import './TextMessage/TextMessage'; +@import './SuperscriptMessage/SuperscriptMessage.scss'; // ============================================================================ // Information density styles diff --git a/packages/module/src/Message/Message.test.tsx b/packages/module/src/Message/Message.test.tsx index c05753475..0b3ee6c92 100644 --- a/packages/module/src/Message/Message.test.tsx +++ b/packages/module/src/Message/Message.test.tsx @@ -142,6 +142,20 @@ const EMPTY_TABLE = ` `; +const FOOTNOTE = `This is some text with a footnote[^1] and here's a longer one.[^bignote] + + You can also reference the same footnote multiple times[^1]. + + [^1]: This is the full footnote text. You can click the arrow to go back up. + + [^bignote]: Here's one with multiple paragraphs and **formatting**. + + Indent paragraphs to include them in the footnote. + + Add as many paragraphs as you like. You can include *italic text*, **bold text**, and even \`code\`. + + > You can even include blockquotes in footnotes!`; + const IMAGE = `![Multi-colored wavy lines on a black background](https://cdn.dribbble.com/userupload/10651749/file/original-8a07b8e39d9e8bf002358c66fce1223e.gif)`; const INLINE_IMAGE = `inline text ![Multi-colored wavy lines on a black background](https://cdn.dribbble.com/userupload/10651749/file/original-8a07b8e39d9e8bf002358c66fce1223e.gif)`; @@ -769,6 +783,28 @@ describe('Message', () => { render(); expect(screen.getByRole('grid', { name: /Test/i })).toBeTruthy(); }); + it('should render footnote correctly', () => { + render(); + expect(screen.getByText(/This is some text with a footnote/i)).toBeTruthy(); + expect(screen.getByText(/and here's a longer one./i)).toBeTruthy(); + expect(screen.getByText(/You can also reference the same footnote multiple times./i)).toBeTruthy(); + expect(screen.getByRole('heading', { name: /Footnotes/i })).toBeTruthy(); + expect(screen.getByText(/This is the full footnote text. You can click the arrow to go back up./i)).toBeTruthy(); + expect(screen.getByText(/Here's one with multiple paragraphs and/i)).toBeTruthy(); + expect(screen.getByText(/formatting/i)).toBeTruthy(); + expect(screen.getByText(/Indent paragraphs to include them in the footnote./i)).toBeTruthy(); + expect(screen.getByText(/Add as many paragraphs as you like. You can include/i)).toBeTruthy(); + expect(screen.getByText(/italic text/i)).toBeTruthy(); + expect(screen.getByText(/bold text/i)).toBeTruthy(); + expect(screen.getByText(/, and even/i)).toBeTruthy(); + expect(screen.getByText(/code/i)).toBeTruthy(); + expect(screen.getByText(/You can even include blockquotes in footnotes!/i)).toBeTruthy(); + expect(screen.getAllByRole('link', { name: '1' })).toHaveLength(2); + expect(screen.getAllByRole('link', { name: '2' })).toBeTruthy(); + expect(screen.getByRole('link', { name: 'Back to reference 1' })).toBeTruthy(); + expect(screen.getByRole('link', { name: 'Back to reference 1-2' })).toBeTruthy(); + expect(screen.getByRole('link', { name: /Back to reference 2/i })).toBeTruthy(); + }); it('should render beforeMainContent with main content', () => { const mainContent = 'Main message content'; const beforeMainContentText = 'Before main content'; diff --git a/packages/module/src/Message/Message.tsx b/packages/module/src/Message/Message.tsx index 067c04475..7d2dcc805 100644 --- a/packages/module/src/Message/Message.tsx +++ b/packages/module/src/Message/Message.tsx @@ -51,6 +51,7 @@ import MessageInput from './MessageInput'; import { rehypeMoveImagesOutOfParagraphs } from './Plugins/rehypeMoveImagesOutOfParagraphs'; import ToolResponse, { ToolResponseProps } from '../ToolResponse'; import DeepThinking, { DeepThinkingProps } from '../DeepThinking'; +import SuperscriptMessage from './SuperscriptMessage/SuperscriptMessage'; export interface MessageAttachment { /** Name of file attached to the message */ @@ -163,6 +164,8 @@ export interface MessageProps extends Omit, 'role'> { tableProps?: Required> & TableProps; /** Additional rehype plugins passed from the consumer */ additionalRehypePlugins?: PluggableList; + /** Additional remark plugins passed from the consumer */ + additionalRemarkPlugins?: PluggableList; /** Whether to open links in message in new tab. */ openLinkInNewTab?: boolean; /** Optional inline error message that can be displayed in the message */ @@ -195,6 +198,8 @@ export interface MessageProps extends Omit, 'role'> { toolResponse?: ToolResponseProps; /** Props for deep thinking card */ deepThinking?: DeepThinkingProps; + /** Allows passing additional props down to remark-gfm. See https://github.com/remarkjs/remark-gfm?tab=readme-ov-file#options for options */ + remarkGfmProps?: Options; } export const MessageBase: FunctionComponent = ({ @@ -223,6 +228,7 @@ export const MessageBase: FunctionComponent = ({ tableProps, openLinkInNewTab = true, additionalRehypePlugins = [], + additionalRemarkPlugins = [], linkProps, error, isEditable, @@ -238,6 +244,7 @@ export const MessageBase: FunctionComponent = ({ reactMarkdownProps, toolResponse, deepThinking, + remarkGfmProps, ...props }: MessageProps) => { const [messageText, setMessageText] = useState(content); @@ -275,43 +282,135 @@ export const MessageBase: FunctionComponent = ({ return ( , - code: ({ children, ...props }) => ( - - {children} - - ), - h1: (props) => , - h2: (props) => , - h3: (props) => , - h4: (props) => , - h5: (props) => , - h6: (props) => , - blockquote: (props) => , - ul: (props) => , - ol: (props) => , - li: (props) => , + section: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return
; + }, + p: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + }, + code: ({ children, ...props }) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...codeProps } = props; + return ( + + {children} + + ); + }, + h1: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + }, + h2: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + }, + h3: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + }, + h4: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + }, + h5: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + }, + h6: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + }, + blockquote: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + }, + ul: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + }, + ol: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + }, + li: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + }, + // table requires node attribute for calculating headers for mobile breakpoint table: (props) => , - tbody: (props) => , - thead: (props) => , - tr: (props) => , + tbody: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + }, + thead: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + }, + tr: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + }, td: (props) => { // Conflicts with Td type // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { width, ...rest } = props; + const { node, width, ...rest } = props; return ; }, - th: (props) => , - img: (props) => , - a: (props) => ( - - {props.children} - - ) + th: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + }, + img: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + }, + a: (props) => { + // node is just the details of the document structure - not needed + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ( + // some a types conflict with ButtonProps, but it's ok because we are using an a tag + // there are too many to handle manually + + {props.children} + + ); + }, + // used for footnotes + sup: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + } }} - remarkPlugins={[remarkGfm]} + remarkPlugins={[[remarkGfm, { ...remarkGfmProps }], ...additionalRemarkPlugins]} rehypePlugins={rehypePlugins} {...reactMarkdownProps} + remarkRehypeOptions={{ + // removes sr-only class from footnote labels applied by default + footnoteLabelProperties: { className: [''] }, + ...reactMarkdownProps?.remarkRehypeOptions + }} > {messageText} diff --git a/packages/module/src/Message/SuperscriptMessage/SuperscriptMessage.scss b/packages/module/src/Message/SuperscriptMessage/SuperscriptMessage.scss new file mode 100644 index 000000000..e7102591c --- /dev/null +++ b/packages/module/src/Message/SuperscriptMessage/SuperscriptMessage.scss @@ -0,0 +1,8 @@ +.pf-chatbot__message-superscript { + font-size: smaller; + vertical-align: super; + .pf-v6-c-button.pf-m-link.pf-m-inline { + font-size: inherit; + vertical-align: inherit; + } +} diff --git a/packages/module/src/Message/SuperscriptMessage/SuperscriptMessage.tsx b/packages/module/src/Message/SuperscriptMessage/SuperscriptMessage.tsx new file mode 100644 index 000000000..1cbcc4379 --- /dev/null +++ b/packages/module/src/Message/SuperscriptMessage/SuperscriptMessage.tsx @@ -0,0 +1,13 @@ +// ============================================================================ +// Chatbot Main - Message - Content - Superscript (like for footnotes) +// ============================================================================ + +import { ExtraProps } from 'react-markdown'; + +const SuperscriptMessage = ({ children }: JSX.IntrinsicElements['sup'] & ExtraProps) => ( + + {children} + +); + +export default SuperscriptMessage; diff --git a/packages/module/src/Message/TextMessage/TextMessage.scss b/packages/module/src/Message/TextMessage/TextMessage.scss index f229660b6..679e6e148 100644 --- a/packages/module/src/Message/TextMessage/TextMessage.scss +++ b/packages/module/src/Message/TextMessage/TextMessage.scss @@ -17,9 +17,10 @@ width: fit-content; padding: var(--pf-t--global--spacer--sm) 0 var(--pf-t--global--spacer--sm) 0; border-radius: var(--pf-t--global--border--radius--small); + --pf-chatbot-message-text-font-size: var(--pf-t--global--font--size--md); .pf-v6-c-button.pf-m-link { - font-size: var(--pf-t--global--font--size--md); + font-size: var(--pf-chatbot-message-text-font-size); } .pf-v6-c-content, @@ -27,15 +28,49 @@ .pf-v6-c-content--blockquote, p, a { - --pf-v6-c-content--FontSize: var(--pf-t--global--font--size--md); + --pf-v6-c-content--FontSize: var(--pf-chatbot-message-text-font-size); } code { background-color: var(--pf-t--global--background--color--tertiary--default); - font-size: var(--pf-t--global--font--size--body--default); + font-size: var(--pf-chatbot-message-text-inline-code-font-size); + } + + // Hide message text that contains sr-only content + // https://css-tricks.com/inclusively-hidden/ + &:has(.sr-only) { + clip: rect(0 0 0 0); + clip-path: inset(50%); + height: 1px; + overflow: hidden; + position: absolute; + white-space: nowrap; + width: 1px; } } +// ============================================================================ +// Footnote spacing styles +// ============================================================================ + +// Add spacing to paragraphs in multi-paragraph footnotes +// Only target p tags that are direct children of message-text spans (not inside blockquotes, etc.) +li[id*='user-content-fn-']:has(> span > .pf-chatbot__message-text + .pf-chatbot__message-text) + > span + > .pf-chatbot__message-text + > p { + margin-block-end: var(--pf-t--global--spacer--md); +} + +// Handle user message footnotes which may have extra span wrappers +li[id*='user-content-fn-']:has(> span > span > .pf-chatbot__message-text + .pf-chatbot__message-text) + > span + > span + > .pf-chatbot__message-text + > p { + margin-block-end: var(--pf-t--global--spacer--md); +} + .pf-chatbot__message--user { .pf-chatbot__message-text { background-color: var(--pf-t--global--color--brand--default); @@ -54,6 +89,11 @@ color: var(--pf-t--global--text--color--on-brand--default); } } + + .pf-chatbot__message-text > .pf-chatbot__message-text { + background-color: initial; + padding: initial; + } } // ============================================================================ @@ -62,8 +102,9 @@ .pf-chatbot.pf-m-compact { // Need to inline shorter text .pf-chatbot__message-text { + --pf-chatbot-message-text-font-size: var(--pf-t--global--font--size--sm); .pf-v6-c-button.pf-m-link { - font-size: var(--pf-t--global--font--size--sm); + font-size: var(--pf-chatbot-message-text-font-size); } .pf-v6-c-content, @@ -71,7 +112,7 @@ .pf-v6-c-content--blockquote, p, a { - --pf-v6-c-content--FontSize: var(--pf-t--global--font--size--sm); + --pf-v6-c-content--FontSize: var(--pf-chatbot-message-text-font-size); } .pf-v6-c-content--blockquote {