chore: Update markdown component + create avatar component + refactor (#36832)

/ok-to-test tags="@tag.Anvil"

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
	- Introduced the `Avatar` component for displaying user avatars.
- Added the `Markdown` component for rendering Markdown content with
GitHub Flavored Markdown support.
- Enhanced `AIChat` component with a new modular input system and
improved message display.

- **Bug Fixes**
	- Updated CSS for better styling consistency across components.

- **Refactor**
- Removed outdated components (`ChatDescriptionModal` and
`ThreadMessage`) to streamline the codebase.
	- Simplified the structure of `AIChat` by incorporating new components.

- **Documentation**
- Updated Storybook stories for `AIChat`, `Avatar`, and `Markdown`
components to showcase new features.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

<!-- This is an auto-generated comment: Cypress test results  -->
> [!TIP]
> 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/11320929997>
> Commit: db2bc5c9a0a0a65b3d9dd4f53e9f100beb3af3a7
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=11320929997&attempt=1"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.Anvil`
> Spec:
> <hr>Mon, 14 Oct 2024 04:42:54 UTC
<!-- end of auto-generated comment: Cypress test results  -->
This commit is contained in:
Pawan Kumar 2024-10-14 11:20:21 +05:30 committed by GitHub
parent ef5a253a92
commit a7bf302f9a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
52 changed files with 1009 additions and 463 deletions

View File

@ -1,3 +1,5 @@
const esModules = ["remark-gfm"].join("|");
module.exports = {
preset: "ts-jest",
roots: ["<rootDir>/src"],
@ -6,6 +8,9 @@ module.exports = {
moduleNameMapper: {
"\\.(css)$": "<rootDir>../../../test/__mocks__/styleMock.js",
},
transformIgnorePatterns: [
`[/\\\\]node_modules[/\\\\](?!${esModules}).+\\.(js|jsx|mjs|cjs|ts|tsx)$`,
],
globals: {
"ts-jest": {
useESM: true,

View File

@ -28,7 +28,8 @@
"lodash": "*",
"react-aria-components": "^1.2.1",
"react-markdown": "^9.0.1",
"react-syntax-highlighter": "^15.5.0"
"react-syntax-highlighter": "^15.5.0",
"remark-gfm": "^4.0.0"
},
"devDependencies": {
"@types/fs-extra": "^11.0.4",

View File

@ -1,14 +1,11 @@
import { Button, Flex, Text, TextArea } from "@appsmith/wds";
import type { FormEvent, ForwardedRef, KeyboardEvent } from "react";
import React, { forwardRef, useCallback } from "react";
import { ChatDescriptionModal } from "./ChatDescriptionModal";
import { ChatTitle } from "./ChatTitle";
import styles from "./styles.module.css";
import { ThreadMessage } from "./ThreadMessage";
import type { AIChatProps, ChatMessage } from "./types";
import { UserAvatar } from "./UserAvatar";
import type { ForwardedRef } from "react";
import React, { forwardRef } from "react";
const MIN_PROMPT_LENGTH = 3;
import styles from "./styles.module.css";
import { ChatHeader } from "./ChatHeader";
import { ChatThread } from "./ChatThread";
import type { AIChatProps } from "./types";
import { ChatInputSection } from "./ChatInputSection";
const _AIChat = (props: AIChatProps, ref: ForwardedRef<HTMLDivElement>) => {
const {
@ -25,81 +22,28 @@ const _AIChat = (props: AIChatProps, ref: ForwardedRef<HTMLDivElement>) => {
username,
...rest
} = props;
const [isChatDescriptionModalOpen, setIsChatDescriptionModalOpen] =
React.useState(false);
const handleFormSubmit = useCallback(
(event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
onSubmit?.();
},
[onSubmit],
);
const handlePromptInputKeyDown = useCallback(
(event: KeyboardEvent<HTMLTextAreaElement>) => {
if (event.key === "Enter" && event.shiftKey) {
event.preventDefault();
onSubmit?.();
}
},
[onSubmit],
);
return (
<div className={styles.root} ref={ref} {...rest}>
<ChatDescriptionModal
isOpen={isChatDescriptionModalOpen}
setOpen={() =>
setIsChatDescriptionModalOpen(!isChatDescriptionModalOpen)
}
>
{chatDescription}
</ChatDescriptionModal>
<ChatHeader
chatDescription={chatDescription}
chatTitle={chatTitle}
username={username}
/>
<div className={styles.header}>
<Flex alignItems="center" gap="spacing-2">
<ChatTitle title={chatTitle} />
<Button
icon="info-square-rounded"
onPress={() => setIsChatDescriptionModalOpen(true)}
variant="ghost"
/>
</Flex>
<ChatThread
onApplyAssistantSuggestion={onApplyAssistantSuggestion}
thread={thread}
username={username}
/>
<Flex alignItems="center" gap="spacing-2">
<UserAvatar username={username} />
<Text data-testid="t--aichat-username" size="body">
{username}
</Text>
</Flex>
</div>
<ul className={styles.thread} data-testid="t--aichat-thread">
{thread.map((message: ChatMessage) => (
<ThreadMessage
{...message}
key={message.id}
onApplyAssistantSuggestion={onApplyAssistantSuggestion}
username={username}
/>
))}
</ul>
<form className={styles.promptForm} onSubmit={handleFormSubmit}>
<TextArea
// TODO: Handle isWaitingForResponse: true state
isDisabled={isWaitingForResponse}
name="prompt"
onChange={onPromptChange}
onKeyDown={handlePromptInputKeyDown}
placeholder={promptInputPlaceholder}
value={prompt}
/>
<Button isDisabled={prompt.length < MIN_PROMPT_LENGTH} type="submit">
Send
</Button>
</form>
<ChatInputSection
isWaitingForResponse={isWaitingForResponse}
onPromptChange={onPromptChange}
onSubmit={onSubmit}
prompt={prompt}
promptInputPlaceholder={promptInputPlaceholder}
/>
</div>
);
};

View File

@ -1,17 +0,0 @@
import { Modal, ModalBody, ModalContent, ModalHeader } from "@appsmith/wds";
import React from "react";
import type { ChatDescriptionModalProps } from "./types";
export const ChatDescriptionModal = ({
children,
...rest
}: ChatDescriptionModalProps) => {
return (
<Modal {...rest}>
<ModalContent>
<ModalHeader title="Information about the bot" />
<ModalBody>{children}</ModalBody>
</ModalContent>
</Modal>
);
};

View File

@ -1,2 +0,0 @@
export * from "./ChatDescriptionModal";
export * from "./types";

View File

@ -1,3 +0,0 @@
import type { ModalProps } from "@appsmith/wds";
export interface ChatDescriptionModalProps extends ModalProps {}

View File

@ -0,0 +1,62 @@
import React, { useState } from "react";
import {
Avatar,
Button,
Flex,
Modal,
ModalBody,
ModalContent,
ModalHeader,
Text,
} from "@appsmith/wds";
import styles from "./styles.module.css";
// this value might come from props in future. So keeping a temporary value here.
const LOGO =
"https://app.appsmith.com/static/media/appsmith_logo_square.3867b1959653dabff8dc.png";
export const ChatHeader: React.FC<{
chatTitle?: string;
username: string;
chatDescription?: string;
}> = ({ chatDescription, chatTitle, username }) => {
const [isChatDescriptionModalOpen, setIsChatDescriptionModalOpen] =
useState(false);
return (
<>
<div className={styles.header}>
<Flex alignItems="center" gap="spacing-2">
<Flex alignItems="center" gap="spacing-3">
<Avatar label="Appsmith AI" size="large" src={LOGO} />
<Text fontWeight={600} size="subtitle">
{chatTitle}
</Text>
</Flex>
<Button
icon="info-square-rounded"
onPress={() => setIsChatDescriptionModalOpen(true)}
variant="ghost"
/>
</Flex>
<Flex alignItems="center" gap="spacing-2">
<Avatar label={username} />
<Text data-testid="t--aichat-username" size="body">
{username}
</Text>
</Flex>
</div>
<Modal
isOpen={isChatDescriptionModalOpen}
setOpen={setIsChatDescriptionModalOpen}
>
<ModalContent>
<ModalHeader title="Information about the bot" />
<ModalBody>{chatDescription}</ModalBody>
</ModalContent>
</Modal>
</>
);
};

View File

@ -0,0 +1,48 @@
import React from "react";
import { Flex, ChatInput, Icon, Text } from "@appsmith/wds";
const MIN_PROMPT_LENGTH = 3;
export const ChatInputSection: React.FC<{
isWaitingForResponse: boolean;
prompt: string;
promptInputPlaceholder?: string;
onPromptChange: (value: string) => void;
onSubmit?: () => void;
}> = ({
isWaitingForResponse,
onPromptChange,
onSubmit,
prompt,
promptInputPlaceholder,
}) => (
<Flex
direction="column"
gap="spacing-3"
paddingBottom="spacing-4"
paddingLeft="spacing-6"
paddingRight="spacing-6"
paddingTop="spacing-4"
>
<ChatInput
isLoading={isWaitingForResponse}
isSubmitDisabled={prompt.length < MIN_PROMPT_LENGTH}
onChange={onPromptChange}
onSubmit={onSubmit}
placeholder={promptInputPlaceholder}
value={prompt}
/>
<Flex
alignItems="center"
flexGrow={1}
gap="spacing-1"
justifyContent="center"
>
<Icon name="alert-circle" size="small" />
<Text color="neutral" size="caption" textAlign="center">
LLM assistant can make mistakes. Answers should be verified before they
are trusted.
</Text>
</Flex>
</Flex>
);

View File

@ -0,0 +1,53 @@
import React from "react";
import { Avatar, Flex, Markdown } from "@appsmith/wds";
import styles from "./styles.module.css";
import type { ChatMessage } from "./types";
import { AssistantSuggestionButton } from "./AssistantSuggestionButton";
export const ChatThread: React.FC<{
thread: ChatMessage[];
onApplyAssistantSuggestion?: (suggestion: string) => void;
username: string;
}> = ({ onApplyAssistantSuggestion, thread, username }) => (
<Flex direction="column" gap="spacing-3" padding="spacing-6">
{thread.map((message: ChatMessage) => {
const { content, isAssistant, promptSuggestions = [] } = message;
return (
<Flex direction={isAssistant ? "row" : "row-reverse"} key={message.id}>
{isAssistant && (
<div>
<Markdown>{content}</Markdown>
{promptSuggestions.length > 0 && (
<Flex
className={styles.suggestions}
gap="spacing-5"
paddingTop="spacing-4"
wrap="wrap"
>
{promptSuggestions.map((suggestion) => (
<AssistantSuggestionButton
key={suggestion}
// eslint-disable-next-line react-perf/jsx-no-new-function-as-prop
onPress={() => onApplyAssistantSuggestion?.(suggestion)}
>
{suggestion}
</AssistantSuggestionButton>
))}
</Flex>
)}
</div>
)}
{!isAssistant && (
<Flex direction="row-reverse" gap="spacing-3">
<Avatar label={username} />
<div>{content}</div>
</Flex>
)}
</Flex>
);
})}
</Flex>
);

View File

@ -1,17 +0,0 @@
import { clsx } from "clsx";
import React from "react";
import styles from "./styles.module.css";
import type { ChatTitleProps } from "./types";
export const ChatTitle = ({ className, title, ...rest }: ChatTitleProps) => {
return (
<div
className={clsx(styles.root, className)}
{...rest}
data-testid="t--aichat-chat-title"
>
<div className={styles.logo} />
{title}
</div>
);
};

View File

@ -1,2 +0,0 @@
export * from "./ChatTitle";
export * from "./types";

View File

@ -1,22 +0,0 @@
.root {
display: flex;
gap: var(--inner-spacing-3);
align-items: center;
/* TODO: --type-title doesn't exists. Define it */
font-size: var(--type-title, 22.499px);
font-style: normal;
font-weight: 500;
/* TODO: --type-title-lineheight doesn't exists. Define it */
line-height: var(--type-title-lineheight, 31.7px);
}
.logo {
display: inline-block;
width: 48px;
min-width: 48px;
height: 48px;
border-radius: var(--border-radius-elevation-2);
/* TODO: --bd-neutral doesn't exists. Define it */
border: 1px solid var(--bd-neutral, #81858b);
background: #f8f8f8;
}

View File

@ -1,5 +0,0 @@
import type { HTMLProps } from "react";
export interface ChatTitleProps extends HTMLProps<HTMLDivElement> {
title?: string;
}

View File

@ -1,86 +0,0 @@
import { Flex, Text } from "@appsmith/wds";
import { clsx } from "clsx";
import React from "react";
import Markdown from "react-markdown";
import SyntaxHighlighter from "react-syntax-highlighter";
import { monokai } from "react-syntax-highlighter/dist/cjs/styles/hljs";
import { AssistantSuggestionButton } from "../AssistantSuggestionButton";
import { UserAvatar } from "../UserAvatar";
import styles from "./styles.module.css";
import type { ThreadMessageProps } from "./types";
export const ThreadMessage = ({
className,
content,
isAssistant,
onApplyAssistantSuggestion,
promptSuggestions = [],
username,
...rest
}: ThreadMessageProps) => {
return (
<li
className={clsx(styles.root, className)}
data-assistant={isAssistant}
{...rest}
>
{isAssistant ? (
<div>
<Text className={styles.content}>
<Markdown
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
components={{
code(props) {
const { children, className, ...rest } = props;
const match = /language-(\w+)/.exec(className ?? "");
return match ? (
<SyntaxHighlighter
PreTag="div"
language={match[1]}
style={monokai}
>
{String(children).replace(/\n$/, "")}
</SyntaxHighlighter>
) : (
<code {...rest} className={className}>
{children}
</code>
);
},
}}
>
{content}
</Markdown>
</Text>
{promptSuggestions.length > 0 && (
<Flex
className={styles.suggestions}
gap="spacing-5"
paddingTop="spacing-4"
wrap="wrap"
>
{promptSuggestions.map((suggestion) => (
<AssistantSuggestionButton
key={suggestion}
// eslint-disable-next-line react-perf/jsx-no-new-function-as-prop
onPress={() => onApplyAssistantSuggestion?.(suggestion)}
>
{suggestion}
</AssistantSuggestionButton>
))}
</Flex>
)}
</div>
) : (
<>
<UserAvatar className={styles.userAvatar} username={username} />
<div>
<Text className={styles.content}>{content}</Text>
</div>
</>
)}
</li>
);
};

View File

@ -1,2 +0,0 @@
export * from "./ThreadMessage";
export * from "./types";

View File

@ -1,27 +0,0 @@
.root {
display: flex;
gap: var(--inner-spacing-4);
padding: var(--inner-spacing-3) 0;
}
@container (min-width: 700px) {
.root {
padding: var(--inner-spacing-5) 0;
}
}
.root[data-assistant="false"] {
flex-direction: row-reverse;
}
.root[data-assistant="false"] .sentTime {
text-align: right;
}
.sentTime {
margin: 0 0 var(--outer-spacing-3);
/* TODO: --type-caption doesn't exists. Define it */
font-size: var(--type-caption, 12.247px);
/* TODO: --type-caption-lineheight doesn't exists. Define it */
line-height: var(--type-caption-lineheight, 17.25px);
}

View File

@ -1,9 +0,0 @@
import type { HTMLProps } from "react";
export interface ThreadMessageProps extends HTMLProps<HTMLLIElement> {
content: string;
isAssistant: boolean;
username: string;
promptSuggestions?: string[];
onApplyAssistantSuggestion?: (suggestion: string) => void;
}

View File

@ -1,27 +0,0 @@
import { clsx } from "clsx";
import React from "react";
import styles from "./styles.module.css";
import type { UserAvatarProps } from "./types";
export const UserAvatar = ({
className,
username,
...rest
}: UserAvatarProps) => {
const getNameInitials = (username: string) => {
const names = username.split(" ");
// If there is only one name, return the first character of the name.
if (names.length === 1) {
return `${names[0].charAt(0)}`;
}
return `${names[0].charAt(0)}${names[1]?.charAt(0)}`;
};
return (
<span className={clsx(styles.root, className)} {...rest}>
{getNameInitials(username)}
</span>
);
};

View File

@ -1,2 +0,0 @@
export * from "./UserAvatar";
export * from "./types";

View File

@ -1,13 +0,0 @@
.root {
display: inline-block;
width: 28px;
min-width: 28px;
height: 28px;
color: var(--bg-elevation-2, #fff);
text-align: center;
font-size: 14px;
font-weight: 500;
line-height: 28px;
border-radius: var(--inner-spacing-1, 4px);
background: #000;
}

View File

@ -1,5 +0,0 @@
import type { HTMLProps } from "react";
export interface UserAvatarProps extends HTMLProps<HTMLSpanElement> {
username: string;
}

View File

@ -1,9 +1,9 @@
.root {
width: 100%;
border-radius: var(--border-radius-elevation-1);
border: 1px solid var(--color-bd-elevation-1);
/* TODO: --bg-elevation-1 doesn't exists. Define it */
background: var(--bg-elevation-1, #fbfcfd);
background-color: var(--color-bg-elevation-3);
border-radius: var(--border-radius-elevation-3);
outline: var(--border-width-1) solid var(--color-bd-elevation-3);
overflow: hidden;
}
.header {
@ -13,7 +13,6 @@
padding: var(--inner-spacing-5) var(--inner-spacing-6);
align-items: flex-start;
border-bottom: 1px solid var(--color-bd-elevation-1);
background: rgba(255, 255, 255, 0.45);
}
@container (min-width: 700px) {
@ -23,21 +22,3 @@
align-items: center;
}
}
.thread {
display: flex;
flex-direction: column;
gap: var(--inner-spacing-3);
align-self: stretch;
padding: 0px var(--inner-spacing-6) var(--inner-spacing-5)
var(--inner-spacing-6);
}
.promptForm {
display: flex;
align-items: flex-start;
margin: var(--outer-spacing-4) 0 0 0;
padding: 0 var(--inner-spacing-5) var(--inner-spacing-5)
var(--inner-spacing-5);
gap: var(--outer-spacing-3);
}

View File

@ -1,4 +1,6 @@
import React from "react";
import { AIChat } from "@appsmith/wds";
import { useState } from "react";
import type { Meta, StoryObj } from "@storybook/react";
const meta: Meta<typeof AIChat> = {
@ -19,6 +21,12 @@ export const Main: Story = {
assistantName: "",
isWaitingForResponse: false,
},
render: (args) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const [prompt, setPrompt] = useState(args.prompt);
return <AIChat {...args} onPromptChange={setPrompt} prompt={prompt} />;
},
};
export const EmptyHistory: Story = {
@ -69,6 +77,21 @@ export const WithHistory: Story = {
content: "Thank you",
isAssistant: false,
},
{
id: "5",
content: `Here's an example of markdown code:
\`\`\`javascript
function greet(name) {
console.log(\`Hello, \${name}!\`);
}
greet('World');
\`\`\`
This code defines a function that greets the given name.`,
isAssistant: true,
},
],
prompt: "",
username: "John Doe",

View File

@ -1,113 +0,0 @@
import React from "react";
import "@testing-library/jest-dom";
import { render, screen, within } from "@testing-library/react";
import { faker } from "@faker-js/faker";
import { AIChat, type AIChatProps } from "..";
import userEvent from "@testing-library/user-event";
const renderComponent = (props: Partial<AIChatProps> = {}) => {
const defaultProps: AIChatProps = {
username: "",
thread: [],
prompt: "",
onPromptChange: jest.fn(),
};
return render(<AIChat {...defaultProps} {...props} />);
};
describe("@appsmith/wds/AIChat", () => {
it("should render chat's title", () => {
const chatTitle = faker.lorem.words(2);
renderComponent({ chatTitle });
expect(screen.getByTestId("t--aichat-chat-title")).toHaveTextContent(
chatTitle,
);
});
it("should render username", () => {
const username = faker.name.firstName();
renderComponent({ username });
expect(screen.getByTestId("t--aichat-username")).toHaveTextContent(
username,
);
});
it("should render thread", () => {
const thread = [
{
id: faker.datatype.uuid(),
content: faker.lorem.paragraph(1),
isAssistant: false,
},
{
id: faker.datatype.uuid(),
content: faker.lorem.paragraph(2),
isAssistant: true,
},
];
renderComponent({ thread });
const messages = within(
screen.getByTestId("t--aichat-thread"),
).getAllByRole("listitem");
expect(messages).toHaveLength(thread.length);
expect(messages[0]).toHaveTextContent(thread[0].content);
expect(messages[1]).toHaveTextContent(thread[1].content);
});
it("should render prompt input placeholder", () => {
const promptInputPlaceholder = faker.lorem.words(3);
renderComponent({
promptInputPlaceholder,
});
expect(screen.getByRole("textbox")).toHaveAttribute(
"placeholder",
promptInputPlaceholder,
);
});
it("should render prompt input value", () => {
const prompt = faker.lorem.words(3);
renderComponent({
prompt,
});
expect(screen.getByRole("textbox")).toHaveValue(prompt);
});
it("should trigger user's prompt", async () => {
const onPromptChange = jest.fn();
renderComponent({
onPromptChange,
});
await userEvent.type(screen.getByRole("textbox"), "A");
expect(onPromptChange).toHaveBeenCalledWith("A");
});
it("should submit user's prompt", async () => {
const onSubmit = jest.fn();
renderComponent({
prompt: "ABCD",
onSubmit,
});
await userEvent.click(screen.getByRole("button", { name: "Send" }));
expect(onSubmit).toHaveBeenCalled();
});
});

View File

@ -0,0 +1 @@
export * from "./src";

View File

@ -0,0 +1,37 @@
import React from "react";
import { clsx } from "clsx";
import { Text } from "@appsmith/wds";
import styles from "./styles.module.css";
import type { AvatarProps } from "./types";
import { getTypographyClassName } from "@appsmith/wds-theming";
export const Avatar = (props: AvatarProps) => {
const { className, label, size, src, ...rest } = props;
const getLabelInitials = (label: string) => {
const names = label.split(" ");
if (names.length === 1) {
return `${names[0].charAt(0)}`;
}
return `${names[0].charAt(0)}${names[1]?.charAt(0)}`;
};
return (
<span
className={clsx(styles.avatar, className)}
{...rest}
data-size={size ? size : undefined}
>
{Boolean(src) ? (
<img alt={label} className={styles.avatarImage} src={src} />
) : (
<Text className={getTypographyClassName("body")} fontWeight={500}>
{getLabelInitials(label)}
</Text>
)}
</span>
);
};

View File

@ -0,0 +1,2 @@
export * from "./types";
export * from "./Avatar";

View File

@ -0,0 +1,31 @@
.avatar {
display: inline-flex;
align-items: center;
justify-content: center;
width: var(--sizing-8);
height: var(--sizing-8);
}
.avatar[data-size="small"] {
width: var(--sizing-6);
height: var(--sizing-6);
}
.avatar[data-size="large"] {
width: var(--sizing-10);
height: var(--sizing-10);
}
/* if the avatar has div, that means no source is provided. For this case, we want to add a background color and border radius */
.avatar:has(div) {
background-color: var(--color-bg-assistive);
color: var(--color-fg-on-assistive);
border-radius: var(--border-radius-elevation-3);
}
.avatarImage {
border-radius: inherit;
height: 100%;
object-fit: contain;
aspect-ratio: 1 / 1;
}

View File

@ -0,0 +1,13 @@
import type { HTMLProps } from "react";
export interface AvatarProps extends Omit<HTMLProps<HTMLSpanElement>, "size"> {
/** The label of the avatar */
label: string;
/** The image source of the avatar */
src?: string;
/** The size of the avatar
*
* @default "medium"
*/
size?: "small" | "medium" | "large";
}

View File

@ -0,0 +1,43 @@
import React from "react";
import { Avatar, Flex } from "@appsmith/wds";
import type { Meta, StoryObj } from "@storybook/react";
const meta: Meta<typeof Avatar> = {
title: "WDS/Widgets/Avatar",
component: Avatar,
};
export default meta;
type Story = StoryObj<typeof Avatar>;
export const Default: Story = {
args: {
label: "John Doe",
},
};
export const WithImage: Story = {
args: {
label: "Jane Smith",
src: "https://assets.appsmith.com/integrations/25720743.png",
},
};
export const SingleInitial: Story = {
args: {
label: "Alice",
},
};
export const Sizes: Story = {
args: {
label: "Alice",
},
render: (args) => (
<Flex gap="spacing-2">
<Avatar {...args} size="small" />
<Avatar {...args} size="medium" />
<Avatar {...args} size="large" />
</Flex>
),
};

View File

@ -6,10 +6,10 @@ import {
IconButton,
TextAreaInput,
} from "@appsmith/wds";
import React, { useCallback, useRef, useEffect, useState } from "react";
import { useControlledState } from "@react-stately/utils";
import { chain, useLayoutEffect } from "@react-aria/utils";
import { TextField as HeadlessTextField } from "react-aria-components";
import React, { useCallback, useRef, useEffect, useState } from "react";
import type { ChatInputProps } from "./types";
@ -22,6 +22,7 @@ export function ChatInput(props: ChatInputProps) {
isLoading,
isReadOnly,
isRequired,
isSubmitDisabled,
label,
onChange,
onSubmit,
@ -91,12 +92,14 @@ export function ChatInput(props: ChatInputProps) {
const handleKeyDown = useCallback(
(event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (Boolean(isSubmitDisabled)) return;
if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
onSubmit?.();
}
},
[onSubmit],
[onSubmit, isSubmitDisabled],
);
useLayoutEffect(() => {
@ -112,14 +115,18 @@ export function ChatInput(props: ChatInputProps) {
return (
<IconButton
icon="player-stop-filled"
isDisabled={isDisabled}
isDisabled={Boolean(isDisabled) || Boolean(isSubmitDisabled)}
onPress={onSubmit}
/>
);
}
return (
<IconButton icon="arrow-up" isDisabled={isDisabled} onPress={onSubmit} />
<IconButton
icon="arrow-up"
isDisabled={Boolean(isDisabled) || Boolean(isSubmitDisabled)}
onPress={onSubmit}
/>
);
})();

View File

@ -1,5 +1,8 @@
import type { TextAreaProps } from "@appsmith/wds";
export interface ChatInputProps extends TextAreaProps {
/** callback function when the user submits the chat input */
onSubmit?: () => void;
/** flag for disable the submit button */
isSubmitDisabled?: boolean;
}

View File

@ -66,3 +66,9 @@ export const Validation: Story = {
</Form>
),
};
export const SubmitDisabled: Story = {
args: {
isSubmitDisabled: true,
},
};

View File

@ -1 +1,2 @@
export * from "./Link";
export { default as linkStyles } from "./styles.module.css";

View File

@ -21,7 +21,7 @@ export const Main: Story = {
args: {
target: "_blank",
href: "https://appsmith.com",
children: "This is a link.",
children: "Appsmith.",
},
};

View File

@ -0,0 +1 @@
export * from "./src";

View File

@ -0,0 +1,32 @@
import React from "react";
import { clsx } from "clsx";
import remarkGfm from "remark-gfm";
import ReactMarkdown from "react-markdown";
import { getTypographyClassName } from "@appsmith/wds-theming";
import styles from "./styles.module.css";
import { components } from "./components";
import type { MarkdownProps } from "./types";
export const Markdown = (props: MarkdownProps) => {
const { children, className, options, ...rest } = props;
return (
<div
className={clsx(
styles.markdown,
getTypographyClassName("body"),
className,
)}
{...rest}
>
<ReactMarkdown
components={components}
remarkPlugins={[remarkGfm]}
{...options}
>
{children}
</ReactMarkdown>
</div>
);
};

View File

@ -0,0 +1,22 @@
import type { Components } from "react-markdown";
import { a } from "./mdComponents/Link";
import { code } from "./mdComponents/Code";
import { p } from "./mdComponents/Paragraph";
import { ul, ol, li } from "./mdComponents/List";
import { h1, h2, h3, h4, h5, h6 } from "./mdComponents/Heading";
export const components: Components = {
a,
h1,
h2,
h3,
h4,
h5,
h6,
p,
ul,
ol,
li,
code,
};

View File

@ -0,0 +1,2 @@
export * from "./types";
export * from "./Markdown";

View File

@ -0,0 +1,59 @@
import { Button, Flex, Text } from "@appsmith/wds";
import type { ExtraProps } from "react-markdown";
import React, { useState, useCallback } from "react";
import { useThemeContext } from "@appsmith/wds-theming";
import {
atomOneDark as darkTheme,
atomOneLight as lightTheme,
} from "react-syntax-highlighter/dist/cjs/styles/hljs";
import SyntaxHighlighter from "react-syntax-highlighter";
type CodeProps = React.ClassAttributes<HTMLElement> &
React.HTMLAttributes<HTMLElement> &
ExtraProps;
export const Code = (props: CodeProps) => {
const { children, className, ...rest } = props;
const match = /language-(\w+)/.exec(className ?? "");
const theme = useThemeContext();
const [copied, setCopied] = useState(false);
const handleCopy = useCallback(() => {
navigator.clipboard.writeText(String(children)).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
});
}, [children]);
return match ? (
<div data-component="code">
<Flex
alignItems="center"
justifyContent="space-between"
padding="spacing-1"
>
<Text size="caption">{match[1]}</Text>
<Button icon="copy" onPress={handleCopy} size="small" variant="ghost">
{copied ? "Copied!" : "Copy"}
</Button>
</Flex>
<SyntaxHighlighter
PreTag="div"
customStyle={{
backgroundColor: "var(--color-bg-neutral-subtle)",
}}
language={match[1]}
style={theme.colorMode === "dark" ? darkTheme : lightTheme}
useInlineStyles
>
{String(children).replace(/\n$/, "")}
</SyntaxHighlighter>
</div>
) : (
<code {...rest} className={className}>
{children}
</code>
);
};
export { Code as code };

View File

@ -0,0 +1,37 @@
import React from "react";
import type { Ref } from "react";
import type { ExtraProps } from "react-markdown";
import { Text, type TextProps } from "@appsmith/wds";
type HeadingProps = React.ClassAttributes<HTMLDivElement> &
React.HTMLAttributes<HTMLDivElement> &
ExtraProps;
const createHeading = (
size: TextProps["size"],
level: 1 | 2 | 3 | 4 | 5 | 6,
fontWeight: TextProps["fontWeight"] = 700,
) => {
const HeadingComponent = ({ children, ref }: HeadingProps) => (
<Text
color="neutral"
data-component={`h${level}`}
fontWeight={fontWeight}
ref={ref as Ref<HTMLDivElement>}
size={size}
>
{children}
</Text>
);
HeadingComponent.displayName = `Heading${level}`;
return HeadingComponent;
};
export const h1 = createHeading("heading", 1);
export const h2 = createHeading("title", 2);
export const h3 = createHeading("subtitle", 3);
export const h4 = createHeading("body", 4);
export const h5 = createHeading("body", 5, 500);
export const h6 = createHeading("body", 6, 300);

View File

@ -0,0 +1,17 @@
import React from "react";
import { Link } from "@appsmith/wds";
import type { ExtraProps } from "react-markdown";
type LinkProps = React.ClassAttributes<HTMLAnchorElement> &
React.AnchorHTMLAttributes<HTMLAnchorElement> &
ExtraProps;
export const a = (props: LinkProps) => {
const { children, href } = props;
return (
<Link data-component="a" href={href} rel="noreferrer" target="_blank">
{children}
</Link>
);
};

View File

@ -0,0 +1,28 @@
import React from "react";
import type { ExtraProps } from "react-markdown";
type ULProps = React.ClassAttributes<HTMLUListElement> &
React.HTMLAttributes<HTMLUListElement> &
ExtraProps;
export const ul = (props: ULProps) => {
const { children } = props;
return <ul data-component="ul">{children}</ul>;
};
type LIProps = React.ClassAttributes<HTMLLIElement> &
React.HTMLAttributes<HTMLLIElement> &
ExtraProps;
export const li = (props: LIProps) => {
const { children } = props;
return <li>{children}</li>;
};
export const ol = (props: ULProps) => {
const { children } = props;
return <ol data-component="ol">{children}</ol>;
};

View File

@ -0,0 +1,22 @@
import React, { type Ref } from "react";
import { Text } from "@appsmith/wds";
import type { ExtraProps } from "react-markdown";
type ParagraphProps = React.ClassAttributes<HTMLDivElement> &
React.HTMLAttributes<HTMLDivElement> &
ExtraProps;
export const p = (props: ParagraphProps) => {
const { children, ref } = props;
return (
<Text
color="neutral"
data-component="p"
ref={ref as Ref<HTMLDivElement>}
size="body"
>
{children}
</Text>
);
};

View File

@ -0,0 +1,118 @@
.markdown {
color: var(--color-fg);
&::after,
&::before {
/* This is required to remove the compensators of capsizing that comes up due to use of `wds-body-text` class */
content: none !important;
}
table {
border: var(--border-width-1) solid var(--color-bd);
border-collapse: separate;
border-radius: var(--border-radius-elevation-3);
border-spacing: 0px;
overflow: hidden;
}
tr {
display: table-row;
vertical-align: inherit;
border-color: inherit;
}
th,
td {
padding: var(--inner-spacing-1) var(--inner-spacing-2);
text-align: left;
vertical-align: top;
border-bottom: var(--border-width-1) solid var(--color-bd);
border-right: var(--border-width-1) solid var(--color-bd);
}
:is(td, th):last-child {
border-right: none;
}
th {
background-color: var(--color-bg-neutral-subtle);
}
thead:last-child tr:last-child th,
tbody:last-child tr:last-child td {
border-bottom: none;
}
/* Headings */
[data-component="h1"] {
margin-top: var(--inner-spacing-7);
margin-bottom: var(--inner-spacing-4);
}
[data-component="h2"] {
margin-top: var(--inner-spacing-6);
margin-bottom: var(--inner-spacing-3);
}
[data-component="h3"] {
margin-top: var(--inner-spacing-5);
margin-bottom: var(--inner-spacing-3);
}
[data-component="h4"] {
margin-top: var(--inner-spacing-4);
margin-bottom: var(--inner-spacing-2);
}
[data-component="h5"] {
margin-top: var(--inner-spacing-3);
margin-bottom: var(--inner-spacing-2);
}
[data-component="h6"] {
margin-top: var(--inner-spacing-2);
margin-bottom: var(--inner-spacing-1);
}
[data-component="p"] {
margin-bottom: var(--inner-spacing-4);
}
/* Lists */
:is(ul, ol) {
margin-top: var(--inner-spacing-2);
margin-bottom: var(--inner-spacing-4);
padding-left: 0;
}
li {
margin-bottom: var(--inner-spacing-2);
margin-left: 1em;
position: relative;
}
[data-component="a"]:before,
[data-component="a"]:after {
content: none;
}
[data-component="code"] {
background-color: var(--color-bg-elevation-2);
border-radius: var(--border-radius-elevation-3);
outline: var(--border-width-1) solid var(--color-bg-neutral-subtle);
margin-bottom: var(--spacing-2);
overflow: auto;
}
pre {
margin-top: var(--inner-spacing-4);
margin-bottom: var(--inner-spacing-4);
}
blockquote {
padding-left: var(--inner-spacing-3);
padding-right: var(--inner-spacing-3);
margin-left: 0;
border-left: var(--border-width-2) solid var(--color-bd-neutral);
}
}

View File

@ -0,0 +1,10 @@
import type { Options } from "react-markdown";
export interface MarkdownProps {
/** The markdown content to render */
children: string;
/** Options for react-markdown */
options?: Options;
/** Additional CSS classes to apply to the component */
className?: string;
}

View File

@ -0,0 +1,62 @@
import { Markdown } from "@appsmith/wds";
import type { Meta, StoryObj } from "@storybook/react";
const meta: Meta<typeof Markdown> = {
title: "WDS/Widgets/Markdown",
component: Markdown,
};
export default meta;
type Story = StoryObj<typeof Markdown>;
export const Default: Story = {
args: {
children: `# Hello, Markdown!
This is a paragraph with **bold** and *italic* text.
## Code Example
\`\`\`javascript
const greeting = "Hello, World!";
console.log(greeting);
\`\`\`
- List item 1
- List item 2
- List item 3
- List item 3.1
- List item 3.2
- List item 3.3
- List item 3.3.1
- List item 3.3.2
1. List item 1
2. List item 2
3. List item 3
[Visit Appsmith](https://www.appsmith.com)
# Heading 1
## Heading 2
### Heading 3
#### Heading 4
##### Heading 5
###### Heading 6
## Table Example
| Column 1 | Column 2 | Column 3 |
|----------|----------|----------|
| Row 1, Cell 1 | Row 1, Cell 2 | Row 1, Cell 3 |
| Row 2, Cell 1 | Row 2, Cell 2 | Row 2, Cell 3 |
| Row 3, Cell 1 | Row 3, Cell 2 | Row 3, Cell 3 |
## Blockquote Example
> This is a blockquote.
>
> It can span multiple lines.
`,
},
};

View File

@ -9,7 +9,7 @@ import React, { forwardRef } from "react";
import type { TextProps } from "./types";
import styles from "./styles.module.css";
const _Text = (props: TextProps, ref: Ref<HTMLParagraphElement>) => {
const _Text = (props: TextProps, ref: Ref<HTMLDivElement>) => {
const {
children,
className,

View File

@ -31,6 +31,8 @@ export * from "./components/ListBox";
export * from "./components/ListBoxItem";
export * from "./components/MenuItem";
export * from "./components/ChatInput";
export * from "./components/Avatar";
export * from "./components/Markdown";
export * from "./utils";
export * from "./hooks";

View File

@ -21,7 +21,7 @@
background-color: var(--color-bg-elevation-1) !important;
}
code,
code:not(:has(span)),
.css-o1d7ko {
background-color: var(--color-bg-accent-subtle) !important;
color: var(--color-fg-on-accent-subtle) !important;

View File

@ -3,3 +3,6 @@ import React from "react";
jest.mock("react-markdown", () => (props: { children: unknown }) => {
return <>{props.children}</>;
});
jest.mock("remark-gfm", () => () => {
})

View File

@ -257,6 +257,7 @@ __metadata:
react-aria-components: ^1.2.1
react-markdown: ^9.0.1
react-syntax-highlighter: ^15.5.0
remark-gfm: ^4.0.0
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
languageName: unknown
@ -17791,6 +17792,13 @@ __metadata:
languageName: node
linkType: hard
"escape-string-regexp@npm:^5.0.0":
version: 5.0.0
resolution: "escape-string-regexp@npm:5.0.0"
checksum: 20daabe197f3cb198ec28546deebcf24b3dbb1a5a269184381b3116d12f0532e06007f4bc8da25669d6a7f8efb68db0758df4cd981f57bc5b57f521a3e12c59e
languageName: node
linkType: hard
"escodegen@npm:^2.0.0, escodegen@npm:^2.1.0":
version: 2.1.0
resolution: "escodegen@npm:2.1.0"
@ -23933,6 +23941,13 @@ __metadata:
languageName: node
linkType: hard
"markdown-table@npm:^3.0.0":
version: 3.0.3
resolution: "markdown-table@npm:3.0.3"
checksum: 8fcd3d9018311120fbb97115987f8b1665a603f3134c93fbecc5d1463380c8036f789e2a62c19432058829e594fff8db9ff81c88f83690b2f8ed6c074f8d9e10
languageName: node
linkType: hard
"markdown-to-jsx@npm:^7.4.5":
version: 7.4.7
resolution: "markdown-to-jsx@npm:7.4.7"
@ -23969,6 +23984,18 @@ __metadata:
languageName: node
linkType: hard
"mdast-util-find-and-replace@npm:^3.0.0":
version: 3.0.1
resolution: "mdast-util-find-and-replace@npm:3.0.1"
dependencies:
"@types/mdast": ^4.0.0
escape-string-regexp: ^5.0.0
unist-util-is: ^6.0.0
unist-util-visit-parents: ^6.0.0
checksum: 05d5c4ff02e31db2f8a685a13bcb6c3f44e040bd9dfa54c19a232af8de5268334c8755d79cb456ed4cced1300c4fb83e88444c7ae8ee9ff16869a580f29d08cd
languageName: node
linkType: hard
"mdast-util-from-markdown@npm:^2.0.0":
version: 2.0.1
resolution: "mdast-util-from-markdown@npm:2.0.1"
@ -23989,6 +24016,83 @@ __metadata:
languageName: node
linkType: hard
"mdast-util-gfm-autolink-literal@npm:^2.0.0":
version: 2.0.1
resolution: "mdast-util-gfm-autolink-literal@npm:2.0.1"
dependencies:
"@types/mdast": ^4.0.0
ccount: ^2.0.0
devlop: ^1.0.0
mdast-util-find-and-replace: ^3.0.0
micromark-util-character: ^2.0.0
checksum: 5630b12e072d7004cb132231c94f667fb5813486779cb0dfb0a196d7ae0e048897a43b0b37e080017adda618ddfcbea1d7bf23c0fa31c87bfc683e0898ea1cfe
languageName: node
linkType: hard
"mdast-util-gfm-footnote@npm:^2.0.0":
version: 2.0.0
resolution: "mdast-util-gfm-footnote@npm:2.0.0"
dependencies:
"@types/mdast": ^4.0.0
devlop: ^1.1.0
mdast-util-from-markdown: ^2.0.0
mdast-util-to-markdown: ^2.0.0
micromark-util-normalize-identifier: ^2.0.0
checksum: 45d26b40e7a093712e023105791129d76e164e2168d5268e113298a22de30c018162683fb7893cdc04ab246dac0087eed708b2a136d1d18ed2b32b3e0cae4a79
languageName: node
linkType: hard
"mdast-util-gfm-strikethrough@npm:^2.0.0":
version: 2.0.0
resolution: "mdast-util-gfm-strikethrough@npm:2.0.0"
dependencies:
"@types/mdast": ^4.0.0
mdast-util-from-markdown: ^2.0.0
mdast-util-to-markdown: ^2.0.0
checksum: fe9b1d0eba9b791ff9001c008744eafe3dd7a81b085f2bf521595ce4a8e8b1b44764ad9361761ad4533af3e5d913d8ad053abec38172031d9ee32a8ebd1c7dbd
languageName: node
linkType: hard
"mdast-util-gfm-table@npm:^2.0.0":
version: 2.0.0
resolution: "mdast-util-gfm-table@npm:2.0.0"
dependencies:
"@types/mdast": ^4.0.0
devlop: ^1.0.0
markdown-table: ^3.0.0
mdast-util-from-markdown: ^2.0.0
mdast-util-to-markdown: ^2.0.0
checksum: 063a627fd0993548fd63ca0c24c437baf91ba7d51d0a38820bd459bc20bf3d13d7365ef8d28dca99176dd5eb26058f7dde51190479c186dfe6af2e11202957c9
languageName: node
linkType: hard
"mdast-util-gfm-task-list-item@npm:^2.0.0":
version: 2.0.0
resolution: "mdast-util-gfm-task-list-item@npm:2.0.0"
dependencies:
"@types/mdast": ^4.0.0
devlop: ^1.0.0
mdast-util-from-markdown: ^2.0.0
mdast-util-to-markdown: ^2.0.0
checksum: 37db90c59b15330fc54d790404abf5ef9f2f83e8961c53666fe7de4aab8dd5e6b3c296b6be19797456711a89a27840291d8871ff0438e9b4e15c89d170efe072
languageName: node
linkType: hard
"mdast-util-gfm@npm:^3.0.0":
version: 3.0.0
resolution: "mdast-util-gfm@npm:3.0.0"
dependencies:
mdast-util-from-markdown: ^2.0.0
mdast-util-gfm-autolink-literal: ^2.0.0
mdast-util-gfm-footnote: ^2.0.0
mdast-util-gfm-strikethrough: ^2.0.0
mdast-util-gfm-table: ^2.0.0
mdast-util-gfm-task-list-item: ^2.0.0
mdast-util-to-markdown: ^2.0.0
checksum: 62039d2f682ae3821ea1c999454863d31faf94d67eb9b746589c7e136076d7fb35fabc67e02f025c7c26fd7919331a0ee1aabfae24f565d9a6a9ebab3371c626
languageName: node
linkType: hard
"mdast-util-mdx-expression@npm:^2.0.0":
version: 2.0.1
resolution: "mdast-util-mdx-expression@npm:2.0.1"
@ -24232,6 +24336,99 @@ __metadata:
languageName: node
linkType: hard
"micromark-extension-gfm-autolink-literal@npm:^2.0.0":
version: 2.1.0
resolution: "micromark-extension-gfm-autolink-literal@npm:2.1.0"
dependencies:
micromark-util-character: ^2.0.0
micromark-util-sanitize-uri: ^2.0.0
micromark-util-symbol: ^2.0.0
micromark-util-types: ^2.0.0
checksum: e00a570c70c837b9cbbe94b2c23b787f44e781cd19b72f1828e3453abca2a9fb600fa539cdc75229fa3919db384491063645086e02249481e6ff3ec2c18f767c
languageName: node
linkType: hard
"micromark-extension-gfm-footnote@npm:^2.0.0":
version: 2.1.0
resolution: "micromark-extension-gfm-footnote@npm:2.1.0"
dependencies:
devlop: ^1.0.0
micromark-core-commonmark: ^2.0.0
micromark-factory-space: ^2.0.0
micromark-util-character: ^2.0.0
micromark-util-normalize-identifier: ^2.0.0
micromark-util-sanitize-uri: ^2.0.0
micromark-util-symbol: ^2.0.0
micromark-util-types: ^2.0.0
checksum: ac6fb039e98395d37b71ebff7c7a249aef52678b5cf554c89c4f716111d4be62ef99a5d715a5bd5d68fa549778c977d85cb671d1d8506dc8a3a1b46e867ae52f
languageName: node
linkType: hard
"micromark-extension-gfm-strikethrough@npm:^2.0.0":
version: 2.1.0
resolution: "micromark-extension-gfm-strikethrough@npm:2.1.0"
dependencies:
devlop: ^1.0.0
micromark-util-chunked: ^2.0.0
micromark-util-classify-character: ^2.0.0
micromark-util-resolve-all: ^2.0.0
micromark-util-symbol: ^2.0.0
micromark-util-types: ^2.0.0
checksum: cdb7a38dd6eefb6ceb6792a44a6796b10f951e8e3e45b8579f599f43e7ae26ccd048c0aa7e441b3c29dd0c54656944fe6eb0098de2bc4b5106fbc0a42e9e016c
languageName: node
linkType: hard
"micromark-extension-gfm-table@npm:^2.0.0":
version: 2.1.0
resolution: "micromark-extension-gfm-table@npm:2.1.0"
dependencies:
devlop: ^1.0.0
micromark-factory-space: ^2.0.0
micromark-util-character: ^2.0.0
micromark-util-symbol: ^2.0.0
micromark-util-types: ^2.0.0
checksum: 249d695f5f8bd222a0d8a774ec78ea2a2d624cb50a4d008092a54aa87dad1f9d540e151d29696cf849eb1cee380113c4df722aebb3b425a214832a2de5dea1d7
languageName: node
linkType: hard
"micromark-extension-gfm-tagfilter@npm:^2.0.0":
version: 2.0.0
resolution: "micromark-extension-gfm-tagfilter@npm:2.0.0"
dependencies:
micromark-util-types: ^2.0.0
checksum: cf21552f4a63592bfd6c96ae5d64a5f22bda4e77814e3f0501bfe80e7a49378ad140f827007f36044666f176b3a0d5fea7c2e8e7973ce4b4579b77789f01ae95
languageName: node
linkType: hard
"micromark-extension-gfm-task-list-item@npm:^2.0.0":
version: 2.1.0
resolution: "micromark-extension-gfm-task-list-item@npm:2.1.0"
dependencies:
devlop: ^1.0.0
micromark-factory-space: ^2.0.0
micromark-util-character: ^2.0.0
micromark-util-symbol: ^2.0.0
micromark-util-types: ^2.0.0
checksum: b1ad86a4e9d68d9ad536d94fb25a5182acbc85cc79318f4a6316034342f6a71d67983cc13f12911d0290fd09b2bda43cdabe8781a2d9cca2ebe0d421e8b2b8a4
languageName: node
linkType: hard
"micromark-extension-gfm@npm:^3.0.0":
version: 3.0.0
resolution: "micromark-extension-gfm@npm:3.0.0"
dependencies:
micromark-extension-gfm-autolink-literal: ^2.0.0
micromark-extension-gfm-footnote: ^2.0.0
micromark-extension-gfm-strikethrough: ^2.0.0
micromark-extension-gfm-table: ^2.0.0
micromark-extension-gfm-tagfilter: ^2.0.0
micromark-extension-gfm-task-list-item: ^2.0.0
micromark-util-combine-extensions: ^2.0.0
micromark-util-types: ^2.0.0
checksum: 2060fa62666a09532d6b3a272d413bc1b25bbb262f921d7402795ac021e1362c8913727e33d7528d5b4ccaf26922ec51208c43f795a702964817bc986de886c9
languageName: node
linkType: hard
"micromark-factory-destination@npm:^2.0.0":
version: 2.0.0
resolution: "micromark-factory-destination@npm:2.0.0"
@ -29372,6 +29569,20 @@ __metadata:
languageName: node
linkType: hard
"remark-gfm@npm:^4.0.0":
version: 4.0.0
resolution: "remark-gfm@npm:4.0.0"
dependencies:
"@types/mdast": ^4.0.0
mdast-util-gfm: ^3.0.0
micromark-extension-gfm: ^3.0.0
remark-parse: ^11.0.0
remark-stringify: ^11.0.0
unified: ^11.0.0
checksum: 84bea84e388061fbbb697b4b666089f5c328aa04d19dc544c229b607446bc10902e46b67b9594415a1017bbbd7c811c1f0c30d36682c6d1a6718b66a1558261b
languageName: node
linkType: hard
"remark-parse@npm:^11.0.0":
version: 11.0.0
resolution: "remark-parse@npm:11.0.0"
@ -29397,6 +29608,17 @@ __metadata:
languageName: node
linkType: hard
"remark-stringify@npm:^11.0.0":
version: 11.0.0
resolution: "remark-stringify@npm:11.0.0"
dependencies:
"@types/mdast": ^4.0.0
mdast-util-to-markdown: ^2.0.0
unified: ^11.0.0
checksum: 59e07460eb629d6c3b3c0f438b0b236e7e6858fd5ab770303078f5a556ec00354d9c7fb9ef6d5f745a4617ac7da1ab618b170fbb4dac120e183fecd9cc86bce6
languageName: node
linkType: hard
"remixicon-react@npm:^1.0.0":
version: 1.0.0
resolution: "remixicon-react@npm:1.0.0"