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"