diff --git a/__visual_tests__/__snapshots__/workflow-tests.spec.ts/101-1-chromium-darwin.png b/__visual_tests__/__snapshots__/workflow-tests.spec.ts/101-1-chromium-darwin.png index b043e0e6a..2cfe98a59 100644 Binary files a/__visual_tests__/__snapshots__/workflow-tests.spec.ts/101-1-chromium-darwin.png and b/__visual_tests__/__snapshots__/workflow-tests.spec.ts/101-1-chromium-darwin.png differ diff --git a/__visual_tests__/__snapshots__/workflow-tests.spec.ts/101-1-firefox-darwin.png b/__visual_tests__/__snapshots__/workflow-tests.spec.ts/101-1-firefox-darwin.png index bb3000e21..52d4aa319 100644 Binary files a/__visual_tests__/__snapshots__/workflow-tests.spec.ts/101-1-firefox-darwin.png and b/__visual_tests__/__snapshots__/workflow-tests.spec.ts/101-1-firefox-darwin.png differ diff --git a/__visual_tests__/__snapshots__/workflow-tests.spec.ts/104-1-chromium-darwin.png b/__visual_tests__/__snapshots__/workflow-tests.spec.ts/104-1-chromium-darwin.png new file mode 100644 index 000000000..498f455b2 Binary files /dev/null and b/__visual_tests__/__snapshots__/workflow-tests.spec.ts/104-1-chromium-darwin.png differ diff --git a/__visual_tests__/__snapshots__/workflow-tests.spec.ts/104-1-firefox-darwin.png b/__visual_tests__/__snapshots__/workflow-tests.spec.ts/104-1-firefox-darwin.png new file mode 100644 index 000000000..9f22cb611 Binary files /dev/null and b/__visual_tests__/__snapshots__/workflow-tests.spec.ts/104-1-firefox-darwin.png differ diff --git a/__visual_tests__/__snapshots__/workflow-tests.spec.ts/104-2-chromium-darwin.png b/__visual_tests__/__snapshots__/workflow-tests.spec.ts/104-2-chromium-darwin.png new file mode 100644 index 000000000..529a8b9a3 Binary files /dev/null and b/__visual_tests__/__snapshots__/workflow-tests.spec.ts/104-2-chromium-darwin.png differ diff --git a/__visual_tests__/__snapshots__/workflow-tests.spec.ts/104-2-firefox-darwin.png b/__visual_tests__/__snapshots__/workflow-tests.spec.ts/104-2-firefox-darwin.png new file mode 100644 index 000000000..1d3d96bcf Binary files /dev/null and b/__visual_tests__/__snapshots__/workflow-tests.spec.ts/104-2-firefox-darwin.png differ diff --git a/__visual_tests__/__snapshots__/workflow-tests.spec.ts/104-3-chromium-darwin.png b/__visual_tests__/__snapshots__/workflow-tests.spec.ts/104-3-chromium-darwin.png new file mode 100644 index 000000000..50d29846c Binary files /dev/null and b/__visual_tests__/__snapshots__/workflow-tests.spec.ts/104-3-chromium-darwin.png differ diff --git a/__visual_tests__/__snapshots__/workflow-tests.spec.ts/104-3-firefox-darwin.png b/__visual_tests__/__snapshots__/workflow-tests.spec.ts/104-3-firefox-darwin.png new file mode 100644 index 000000000..08959ae57 Binary files /dev/null and b/__visual_tests__/__snapshots__/workflow-tests.spec.ts/104-3-firefox-darwin.png differ diff --git a/__visual_tests__/__snapshots__/workflow-tests.spec.ts/104-4-chromium-darwin.png b/__visual_tests__/__snapshots__/workflow-tests.spec.ts/104-4-chromium-darwin.png new file mode 100644 index 000000000..85527d14c Binary files /dev/null and b/__visual_tests__/__snapshots__/workflow-tests.spec.ts/104-4-chromium-darwin.png differ diff --git a/__visual_tests__/__snapshots__/workflow-tests.spec.ts/104-4-firefox-darwin.png b/__visual_tests__/__snapshots__/workflow-tests.spec.ts/104-4-firefox-darwin.png new file mode 100644 index 000000000..5cf2a8bd2 Binary files /dev/null and b/__visual_tests__/__snapshots__/workflow-tests.spec.ts/104-4-firefox-darwin.png differ diff --git a/__visual_tests__/__snapshots__/workflow-tests.spec.ts/104-5-chromium-darwin.png b/__visual_tests__/__snapshots__/workflow-tests.spec.ts/104-5-chromium-darwin.png new file mode 100644 index 000000000..e5ae655d6 Binary files /dev/null and b/__visual_tests__/__snapshots__/workflow-tests.spec.ts/104-5-chromium-darwin.png differ diff --git a/__visual_tests__/__snapshots__/workflow-tests.spec.ts/104-5-firefox-darwin.png b/__visual_tests__/__snapshots__/workflow-tests.spec.ts/104-5-firefox-darwin.png new file mode 100644 index 000000000..4c10443d9 Binary files /dev/null and b/__visual_tests__/__snapshots__/workflow-tests.spec.ts/104-5-firefox-darwin.png differ diff --git a/__visual_tests__/__snapshots__/workflow-tests.spec.ts/104-6-chromium-darwin.png b/__visual_tests__/__snapshots__/workflow-tests.spec.ts/104-6-chromium-darwin.png new file mode 100644 index 000000000..452c1f1a4 Binary files /dev/null and b/__visual_tests__/__snapshots__/workflow-tests.spec.ts/104-6-chromium-darwin.png differ diff --git a/__visual_tests__/__snapshots__/workflow-tests.spec.ts/104-6-firefox-darwin.png b/__visual_tests__/__snapshots__/workflow-tests.spec.ts/104-6-firefox-darwin.png new file mode 100644 index 000000000..0fd7ad383 Binary files /dev/null and b/__visual_tests__/__snapshots__/workflow-tests.spec.ts/104-6-firefox-darwin.png differ diff --git a/__visual_tests__/const.ts b/__visual_tests__/const.ts index b8cbde4f5..5ebc1074e 100644 --- a/__visual_tests__/const.ts +++ b/__visual_tests__/const.ts @@ -15,4 +15,5 @@ export const WidgetComponentIds = { INPUT: '.sendbird-input__input', CHIPS_CONTAINER: '.sendbird-form-chip__container', FORM: '#aichatbot-widget-form', + MARKDOWN: '.widget-markdown', }; diff --git a/__visual_tests__/utils/testUtils.ts b/__visual_tests__/utils/testUtils.ts index c603f0e12..e47a7d8d4 100644 --- a/__visual_tests__/utils/testUtils.ts +++ b/__visual_tests__/utils/testUtils.ts @@ -2,7 +2,7 @@ import { expect, Page } from '@playwright/test'; import { getWidgetSessionCache } from './localStorageUtils'; import { deleteChannel, deleteUser } from './requestUtils'; -import { AppId, BotId, WidgetComponentIds } from '../const'; +import { AppId, BotId, TestUrl, WidgetComponentIds } from '../const'; export async function assertScreenshot(page: Page, screenshotName: string, browserName: string) { const name = `${screenshotName}.${browserName}.${process.platform}.png`; // Include the browser and OS architecture info in the filename @@ -12,12 +12,15 @@ export async function assertScreenshot(page: Page, screenshotName: string, brows }); } -export async function loadWidget(page: Page) { +export async function loadWidget(page: Page, testUrl = TestUrl) { + await page.goto(testUrl); + const widgetWindow = page.locator(WidgetComponentIds.WIDGET_BUTTON); + await widgetWindow.waitFor({ state: 'visible' }); + await page.click(WidgetComponentIds.WIDGET_BUTTON); // NOTE: below fails sometimes in CI. - const widgetWindow = page.locator(WidgetComponentIds.SUGGESTED_REPLIES_OPTIONS); - await widgetWindow.waitFor({ state: 'visible' }); - // await page.waitForTimeout(3000); + const replies = page.locator(WidgetComponentIds.SUGGESTED_REPLIES_OPTIONS); + await replies.waitFor({ state: 'visible' }); } export async function sendTextMessage(page: Page, text: string, waitTime = 1000) { diff --git a/__visual_tests__/workflow-tests.spec.ts b/__visual_tests__/workflow-tests.spec.ts index 49778c090..024c6d817 100644 --- a/__visual_tests__/workflow-tests.spec.ts +++ b/__visual_tests__/workflow-tests.spec.ts @@ -1,15 +1,8 @@ import { test } from '@playwright/test'; -import { TestUrl, WidgetComponentIds } from './const'; +import { WidgetComponentIds } from './const'; import { assertScreenshot, clickNthChip, deleteTestResources, loadWidget, sendTextMessage } from './utils/testUtils'; -test.beforeEach(async ({ page }) => { - await page.goto(TestUrl); - - const widgetWindow = page.locator(WidgetComponentIds.WIDGET_BUTTON); - await widgetWindow.waitFor({ state: 'visible' }); -}); - test.afterEach(async ({ page }) => { await deleteTestResources(page); /** @@ -58,7 +51,7 @@ test('100', async ({ page, browserName }) => { await inputs.nth(3).fill('guy.ordering.food@food.com'); await inputs.nth(4).fill('123-456-7890'); await submitButton.click(); - await page.waitForTimeout(1000); + await page.waitForTimeout(2000); await assertScreenshot(page, '100-4', browserName); }); @@ -137,3 +130,69 @@ test('103', async ({ page, browserName }) => { await page.waitForTimeout(2000); await assertScreenshot(page, '103-6', browserName); }); + +/** + * 104 + * Workflow - Markdown response + * Steps: + * 1. Send the trigger message: "give me a markdown message" + * 2. Click "Part 2" + * 3. Click "Back" + * 4. Click "Part 3" + * 5. Click "Back" + * 6. Click "Part 4" + */ +test('104', async ({ page, browserName }) => { + await loadWidget(page); + // 1 + await sendTextMessage(page, 'give me a markdown message', 2000); + + // Check if the fallback component is visible + const fallback = page.locator(WidgetComponentIds.MARKDOWN); + await fallback.first().waitFor({ state: 'visible' }); + let options = page.locator(WidgetComponentIds.SUGGESTED_REPLIES_OPTIONS); + await options.nth(4).waitFor({ state: 'visible' }); + await assertScreenshot(page, '104-1', browserName); + + // 2 + await options.nth(0).click(); + options = page.locator(WidgetComponentIds.SUGGESTED_REPLIES_OPTIONS); + await options.nth(0).waitFor({ state: 'visible' }); // Wait for go back button to show + await assertScreenshot(page, '104-2', browserName); + await options.nth(0).click(); // Go back + options = page.locator(WidgetComponentIds.SUGGESTED_REPLIES_OPTIONS); + await options.nth(4).waitFor({ state: 'visible' }); + + // 3 + await options.nth(1).click(); + options = page.locator(WidgetComponentIds.SUGGESTED_REPLIES_OPTIONS); + await options.nth(0).waitFor({ state: 'visible' }); // Wait for go back button to show + await assertScreenshot(page, '104-3', browserName); + await options.nth(0).click(); // Go back + options = page.locator(WidgetComponentIds.SUGGESTED_REPLIES_OPTIONS); + await options.nth(4).waitFor({ state: 'visible' }); + + // 4 + await options.nth(2).click(); + options = page.locator(WidgetComponentIds.SUGGESTED_REPLIES_OPTIONS); + await options.nth(0).waitFor({ state: 'visible' }); // Wait for go back button to show + await assertScreenshot(page, '104-4', browserName); + await options.nth(0).click(); // Go back + options = page.locator(WidgetComponentIds.SUGGESTED_REPLIES_OPTIONS); + await options.nth(4).waitFor({ state: 'visible' }); + + // 5 + await options.nth(3).click(); + options = page.locator(WidgetComponentIds.SUGGESTED_REPLIES_OPTIONS); + await options.nth(0).waitFor({ state: 'visible' }); // Wait for go back button to show + await assertScreenshot(page, '104-5', browserName); + await options.nth(0).click(); // Go back + options = page.locator(WidgetComponentIds.SUGGESTED_REPLIES_OPTIONS); + await options.nth(4).waitFor({ state: 'visible' }); + + // 6 + await options.nth(4).click(); + options = page.locator(WidgetComponentIds.SUGGESTED_REPLIES_OPTIONS); + await options.nth(0).waitFor({ state: 'visible' }); // Wait for go back button to show + await assertScreenshot(page, '104-6', browserName); +}); diff --git a/package.json b/package.json index 81bbf6b5d..74b7ddace 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-styled-components-a11y": "^2.1.32", "jsdom": "^24.1.0", + "markdown-to-jsx": "^7.7.0", "prettier": "^3.3.3", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/src/components/ParsedBotMessageBody.tsx b/src/components/ParsedBotMessageBody.tsx index 2a80eff4b..706b56c70 100644 --- a/src/components/ParsedBotMessageBody.tsx +++ b/src/components/ParsedBotMessageBody.tsx @@ -1,13 +1,26 @@ +import { lazy, Suspense } from 'react'; +import styled from 'styled-components'; + import { Source } from './SourceContainer'; -import TokensBody, { TextContainer } from './TokensBody'; import { Token } from '../utils'; +const TokensBody = lazy(() => import('./TokensBody')); + type Props = { text: string; - tokens?: Token[]; + tokens: Token[]; sources?: Source[]; }; +const TextContainer = styled.div` + width: inherit; + text-align: start; + word-break: break-word; + padding: 8px 12px; + gap: 12px; + white-space: pre-wrap; +`; + /** * Parses bot message text to process code snippets within the text. * @param props @@ -15,12 +28,16 @@ type Props = { */ export default function ParsedBotMessageBody(props: Props) { const { text, tokens, sources } = props; - if (tokens && tokens.length > 0) { - return ; - } + return ( - - {text} - + + {text} + + } + > + + ); } diff --git a/src/components/TokensBody.tsx b/src/components/TokensBody.tsx index c77ccc9fb..19d0cd0c9 100644 --- a/src/components/TokensBody.tsx +++ b/src/components/TokensBody.tsx @@ -1,70 +1,36 @@ -import { ReactNode } from 'react'; +import DOMPurify from 'dompurify'; +import Markdown from 'markdown-to-jsx'; import styled from 'styled-components'; import BotMessageBottom from './BotMessageBottom'; import SourceContainer, { Source } from './SourceContainer'; import { CodeBlock } from './ui/CodeBlock'; import { useConstantState } from '../context/ConstantContext'; -import { asSafeURL, replaceWithRegex, Token, TokenType } from '../utils'; +import { Token, TokenType } from '../utils'; -const urlRegex = - /(?:https?:\/\/|www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.(xn--)?[a-z]{2,20}\b([-a-zA-Z0-9@:%_+[\],.~#?&/=]*[-a-zA-Z0-9@:%_+~#?&/=])*/g; -const markdownUrlRegex = /\[(.*?)\]\((.*?)\)/g; -const markdownBoldRegex = /\*\*(.*?)\*\*/g; +import './markdown.scss'; type TokensBodyProps = { tokens: Token[]; sources?: Source[]; }; -interface RegexTextPattern { - regex: RegExp; - replacer(params: { match: string; groups: string[]; index: number }): string | ReactNode; -} - const BlockContainer = styled.div` width: 100%; + /* + Note this was added because following element doest not have top margin due to it being the first element + of its markdown div. + */ + margin: 0.5em 0; `; const MultipleTokenTypeContainer = styled.div` + padding: 8px 0; // Bubble top and bottom padding. Side padding is applied for token containers. border-radius: 16px; overflow: auto; background-color: ${({ theme }) => theme.bgColor.incomingMessage}; `; -export const TextContainer = styled.div` - width: inherit; - text-align: start; - word-break: break-word; - padding: 8px 12px; - gap: 12px; - white-space: pre-wrap; -`; - -const RegexText = ({ children, patterns }: { children: string; patterns: RegexTextPattern[] }) => { - if (patterns.length === 0 || typeof children !== 'string') { - return <>{children}; - } - - const convertedNodes: Array = [children]; - patterns.forEach(({ regex, replacer }) => { - const node = convertedNodes.concat(); - let offset = 0; - node.forEach((text, index) => { - if (typeof text === 'string' && text) { - const children = replaceWithRegex(text, regex, replacer); - - if (children.length > 1) { - convertedNodes.splice(index + offset, 1, ...children); - offset += children.length - 1; - } - } - }); - }); - - return {convertedNodes}; -}; - export default function TokensBody({ tokens, sources }: TokensBodyProps) { const { enableSourceMessage } = useConstantState(); @@ -74,51 +40,42 @@ export default function TokensBody({ tokens, sources }: TokensBodyProps) { // Normal text part of the message. if (token.type === TokenType.string) { return ( - {groups[1]}; - }, - }, - { - regex: markdownUrlRegex, - replacer({ match, groups, index }) { - return ( - - {groups[1]} - - ); +
+ { + return DOMPurify.sanitize(value); }, - }, - { - regex: urlRegex, - replacer({ match, index }) { - return ( - - {match} - - ); + overrides: { + // Note that this is to remove text-align: right by the library. + td: { + component: ({ children, ...props }) => ( + + {children} + + ), + }, + // Note that this is to remove text-align: right by the library. + th: { + component: ({ children, ...props }) => ( + + {children} + + ), + }, + a: { + component: ({ children, ...props }) => ( + + {children} + + ), + }, }, - }, - ]} - > - {token.value} - + }} + > + {token.value} + +
); } // Code part of the message. diff --git a/src/components/markdown.scss b/src/components/markdown.scss new file mode 100644 index 000000000..a9fafdc78 --- /dev/null +++ b/src/components/markdown.scss @@ -0,0 +1,295 @@ +.widget-markdown { + padding: 0 12px; /* apply side padding of the bubble */ +} + +.widget-markdown * { + color: var(--sb-on-content-inverse-1); +} + +.widget-markdown *:first-child { + /* First child element should have no top margin */ + margin-top: 0; +} + +.widget-markdown *:last-child { + /* Last child element should have no bottom margin */ + margin-bottom: 0; +} + +.widget-markdown hr { + border-top: 1px solid var(--sb-on-bg-4); + margin-bottom: 21px; /* 1.5em (3em in ChatGPT) */ + margin-top: 21px; /* 1.5em (3em in ChatGPT) */ +} + +.widget-markdown h1 { + margin-top: 0; + margin-bottom: 12.4px; /* 0.8888889em */ + padding: 0; + font-size: 22.4px; /* 1.6em (2.25em in ChatGPT, reduced for visual appeal) */ + line-height: 27.6px; + font-weight: 700; + letter-spacing: -0.6px; /* -0.04rem */ +} + +.widget-markdown h2 { + font-weight: 600; + margin-bottom: 12px; /* 0.75rem (1em in ChatGPT) */ + margin-top: 24px; /* 1.5rem (2em in ChatGPT) */ + font-size: 18.2px; /* 1.3em (1.5em in ChatGPT) */ + line-height: 22.3px; /* 1.3333333em */ +} + +.widget-markdown h3, +h4, +h5, +h6 { + font-weight: 600; + margin-bottom: 8px; /* 0.5rem */ + margin-top: 16px; /* 1rem */ + font-size: 15.4px; /* 1.1em (1.25em in ChatGPT) */ + line-height: 22.4px; /* 1.6em */ +} + +.widget-markdown p:not(:first-child) { + margin-top: 8px; /* 0.5rem */ +} + +.widget-markdown p { + font-size: 14px; /* 1em */ + line-height: 20px; /* 1.43em (1.75em in ChatGPT) */ + margin-bottom: 8px; /* 0.5rem */ + margin-top: 0; +} + +.widget-markdown code { + border-radius: 0.25rem; + font-size: 0.875em; // 12.3px; /* 0.875em */ + font-weight: 500; + padding: 2.4px 4.8px; /* 0.15rem 0.3rem */ + background-color: var(--sb-on-bg-4); +} + +.widget-markdown menu, +ol, +ul { + list-style: none; +} + +/* +Below stuffs are from ChatGPT. I asked Widget to send me a random message with +bunch of widget-markdown syntaxes. Then I asked ChatGPT to send me the exact same message. +I then benchmarked the style ChatGPT used. Below classes are the exact same classes. +*/ + +.widget-markdown :where(ol):not(:where([class~='not-widget-markdown'] *)) { + padding-inline-start: 16.8px; /* 1.2em (1.625em in ChatGPT) */ + list-style-type: decimal; + margin-top: 20px; /* 1.25em */ + margin-bottom: 20px; /* 1.25em */ +} + +.widget-markdown :where(ul):not(:where([class~='not-widget-markdown'] *)) { + padding-inline-start: 16.8px; /* 1.2em (1.625em in ChatGPT) */ + list-style-type: disc; + margin-top: 20px; /* 1.25em */ + margin-bottom: 20px; /* 1.25em */ +} + +/* +Any element inside .widget-markdown will have a margin-top: 0 applied if it directly follows an h3. +Any element inside .not-widget-markdown will not be affected. +widget-markdown.widget-markdown is to increase specificity. Refer to: https://www.w3.org/TR/CSS21/cascade.html#specificity +*/ +.widget-markdown.widget-markdown :where(h3 + *):not(:where([class~='not-widget-markdown'] *)) { + margin-top: 0; +} + +.widget-markdown :where(ol, ul) > li > :first-child { + margin-bottom: 0; + margin-top: 0; +} + +.widget-markdown :where(ol, ul) > li > :last-child { + margin-bottom: 0; +} + +.widget-markdown p + :where(ol, ul) { + margin-top: 0; +} + +.widget-markdown :where(ul ul, ul ol, ol ul, ol ol):not(:where([class~='not-widget-markdown'] *)) { + margin-bottom: 10.5px; /* 0.75em */ + margin-top: 10.5px; /* 0.75em */ +} + +.widget-markdown :where(li):not(:where([class~='not-widget-markdown'] *)) { + padding-inline-start: 0; /* 0.375rem */ + margin-bottom: 5.6px; /* 0.4em (0.5em in ChatGPT) */ + margin-top: 5.6px; /* 0.4em (0.5em in ChatGPT) */ +} + +.widget-markdown blockquote { + line-height: 19.2px; /* 1.2rem (1.5rem in ChatGPT) */ + margin: 0; + padding-bottom: 4.8px; /* 0.3rem (0.5rem in ChatGPT) */ + padding-top: 4.8px; /* 0.3rem (0.5rem in ChatGPT) */ + margin-inline-start: 8px; + padding-inline-start: 16px; /* 12px + block width 4px (1rem in ChatGPT) */ + box-sizing: border-box; + position: relative; + + ::before { + border-radius: 100px; + + content: ''; /* Empty content to create the rounded bar effect */ + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 4px; + background-color: var(--sb-on-bg-4); + } +} + +.widget-markdown :where(blockquote):not(:where([class~='not-widget-markdown'] *)) { + font-style: normal; + font-weight: 500; + margin-bottom: 16.8px; /* 1.2em (1.6rem in ChatGPT) */ + margin-top: 16.8px; /* 1.2em (1.6rem in ChatGPT) */ + quotes: '“' '”' '‘' '’'; + + border-left-width: 4px; /* 0.25rem */ + padding-inline-start: 14px; /* 1em */ +} + +[type='checkbox'] { + vertical-align: middle; + margin: 0; +} + +.widget-markdown :where(table):not(:where([class~='not-widget-markdown'] *)) { + text-align: left; +} + +.widget-markdown table { + unicode-bidi: isolate; + overflow-wrap: break-word; + white-space: normal; + + display: block; + overflow-x: scroll; + max-width: 100%; + text-indent: 0; + + --tw-border-spacing-x: 0px; + --tw-border-spacing-y: 0px; + border-collapse: separate; + border-spacing: 0 0; + margin-bottom: 4px; /* 0.25rem */ + margin-top: 4px; /* 0.25rem */ + *, + :after, + :before { + /* Need this for table borders to be displayed */ + border: 0 solid; + box-sizing: border-box; + } +} + +.widget-markdown :where(table):not(:where([class~='not-widget-markdown'] *)) { + font-size: 12.3px; /* 0.875em */ + margin-bottom: 28px; /* 2em */ + margin-top: 28px; /* 2em */ + table-layout: auto; + width: 100%; +} + +.widget-markdown th:first-child { + border-top-left-radius: 6px; /* 0.375rem */ + padding-inline-start: 10.5px; /* 0.75rem */ +} + +.widget-markdown th:last-child { + border-right-width: 1px; + border-top-right-radius: 6px; /* 0.375rem */ + padding-inline-end: 10.5px; /* 0.75rem */ +} + +.widget-markdown th { + border-left-width: 1px; + background-color: rgba(0, 0, 0, 0.1); + border-bottom-width: 1px; + border-color: var(--sb-on-bg-4); + border-top-width: 1px; + padding: 4px 10.5px; /* 0.25rem 0.75rem */ +} + +.widget-markdown :where(thead th:first-child):not(:where([class~='not-widget-markdown'] *)) { + padding-inline-start: 0; +} + +.widget-markdown :where(thead th:last-child):not(:where([class~='not-widget-markdown'] *)) { + padding-inline-end: 0; +} + +.widget-markdown :where(thead th):not(:where([class~='not-widget-markdown'] *)) { + padding-bottom: 8px; /* 0.5714286em */ + padding-inline-start: 8px; /* 0.5714286em */ + padding-inline-end: 8px; /* 0.5714286em */ + vertical-align: bottom; +} + +.widget-markdown :where(tbody tr):not(:where([class~='not-widget-markdown'] *)) { + border-bottom-width: 1px; +} + +.widget-markdown :where(tbody tr:last-child):not(:where([class~='not-widget-markdown'] *)) { + border-bottom-width: 0; +} + +.widget-markdown td:first-child { + padding-inline-start: 10.5px; /* 0.75rem */ +} + +.widget-markdown td:last-child { + border-right-width: 1px; + padding-inline-end: 10.5px; /* 0.75rem */ +} + +.widget-markdown :where(tbody td:first-child, tfoot td:first-child):not(:where([class~='not-widget-markdown'] *)) { + padding-inline-start: 0; +} + +.widget-markdown :where(tbody td:last-child, tfoot td:last-child):not(:where([class~='not-widget-markdown'] *)) { + padding-inline-end: 0; +} + +.widget-markdown td { + border-left-width: 1px; + border-bottom-width: 1px; + border-color: var(--sb-on-bg-4); + padding: 4px 10.5px; /* 0.25rem 0.75rem */ + text-align: left; +} + +.widget-markdown :where(tbody td, tfoot td):not(:where([class~='not-widget-markdown'] *)) { + padding: 8px; /* 0.5714286em */ +} + +.widget-markdown :where(tbody td):not(:where([class~='not-widget-markdown'] *)) { + vertical-align: baseline; +} + +.widget-markdown tbody tr:last-child td:first-child { + border-bottom-left-radius: 6px; /* 0.375rem */ +} + +.widget-markdown tbody tr:last-child td:last-child { + border-bottom-right-radius: 6px; /* 0.375rem */ +} + +.widget-markdown a { + color: var(--sb-on-bg-1); + font-weight: 700; +} diff --git a/src/components/ui/CodeBlock.tsx b/src/components/ui/CodeBlock.tsx index 949db652a..50e1447fe 100644 --- a/src/components/ui/CodeBlock.tsx +++ b/src/components/ui/CodeBlock.tsx @@ -7,7 +7,7 @@ import { Token } from '../../utils'; const CodeContainer = styled.div` position: relative; padding: 20px; - background: #000; + background: var(--sendbird-dark-background-600); `; const CodeContent = styled.div` diff --git a/yarn.lock b/yarn.lock index 35edad956..e3b16340f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3298,6 +3298,7 @@ __metadata: eslint-plugin-react-hooks: "npm:^4.6.0" eslint-plugin-styled-components-a11y: "npm:^2.1.32" jsdom: "npm:^24.1.0" + markdown-to-jsx: "npm:^7.7.0" prettier: "npm:^3.3.3" react: "npm:^18.2.0" react-dom: "npm:^18.2.0" @@ -12768,6 +12769,15 @@ __metadata: languageName: node linkType: hard +"markdown-to-jsx@npm:^7.7.0": + version: 7.7.0 + resolution: "markdown-to-jsx@npm:7.7.0" + peerDependencies: + react: ">= 0.14.0" + checksum: 10c0/ffaad99232a802f86be350458c969b50dfaf2e13597b89ce61ada2354ddb8c97f77ed82d5779d91ebc27a765ae4fdcf58fb078266260f3db2fe7f5fe797c2a81 + languageName: node + linkType: hard + "marked@npm:^4.3.0": version: 4.3.0 resolution: "marked@npm:4.3.0"