From 4dfaf9c5e75b99dfcd5aa2b42c7b469895de175a Mon Sep 17 00:00:00 2001 From: Lewis Tierney Date: Wed, 24 Jun 2026 15:07:50 +0100 Subject: [PATCH 1/2] feat: components v2 --- package.json | 2 +- pnpm-lock.yaml | 10 +- src/Message/Components/ActionRow.tsx | 4 +- src/Message/Components/ButtonComponent.tsx | 1 + src/Message/Components/Component.tsx | 28 +- src/Message/Components/ContainerComponent.tsx | 36 +++ src/Message/Components/FileComponent.tsx | 50 ++++ .../Components/MediaGalleryComponent.tsx | 29 ++ src/Message/Components/MediaItem.tsx | 48 ++++ src/Message/Components/SectionComponent.tsx | 27 ++ src/Message/Components/SeparatorComponent.tsx | 21 ++ src/Message/Components/Spoiler.tsx | 18 ++ .../Components/TextDisplayComponent.tsx | 23 ++ src/Message/Components/ThumbnailComponent.tsx | 23 ++ src/Message/Components/index.tsx | 6 +- src/Message/Components/style.ts | 269 ++++++++++++++++++ src/stories/ComponentsV2.stories.tsx | 203 +++++++++++++ 17 files changed, 785 insertions(+), 13 deletions(-) create mode 100644 src/Message/Components/ContainerComponent.tsx create mode 100644 src/Message/Components/FileComponent.tsx create mode 100644 src/Message/Components/MediaGalleryComponent.tsx create mode 100644 src/Message/Components/MediaItem.tsx create mode 100644 src/Message/Components/SectionComponent.tsx create mode 100644 src/Message/Components/SeparatorComponent.tsx create mode 100644 src/Message/Components/Spoiler.tsx create mode 100644 src/Message/Components/TextDisplayComponent.tsx create mode 100644 src/Message/Components/ThumbnailComponent.tsx create mode 100644 src/stories/ComponentsV2.stories.tsx diff --git a/package.json b/package.json index 66155e2..cf86d7e 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "@stitches/react": "^1.3.1-1", "autobind-decorator": "^2.4.0", "color": "^4.2.3", - "discord-api-types": "^0.37.52", + "discord-api-types": "^0.38.49", "filesize": "^10.0.6", "highlight.js": "^11.7.0", "i18next": "^23.4.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7bce501..ded4871 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,8 +37,8 @@ importers: specifier: ^4.2.3 version: 4.2.3 discord-api-types: - specifier: ^0.37.52 - version: 0.37.120 + specifier: ^0.38.49 + version: 0.38.49 filesize: specifier: ^10.0.6 version: 10.1.6 @@ -3173,8 +3173,8 @@ packages: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} - discord-api-types@0.37.120: - resolution: {integrity: sha512-7xpNK0EiWjjDFp2nAhHXezE4OUWm7s1zhc/UXXN6hnFFU8dfoPHgV0Hx0RPiCa3ILRpdeh152icc68DGCyXYIw==} + discord-api-types@0.38.49: + resolution: {integrity: sha512-XnqcWmnFZFAE8ZM8SHAw9DIV8D3Or00rMQ8iQLotrEA2PmXhl+ykaf6L6q4l474hrSUH1JaYcv+iOMRWp2p6Tg==} doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} @@ -10410,7 +10410,7 @@ snapshots: dependencies: path-type: 4.0.0 - discord-api-types@0.37.120: {} + discord-api-types@0.38.49: {} doctrine@2.1.0: dependencies: diff --git a/src/Message/Components/ActionRow.tsx b/src/Message/Components/ActionRow.tsx index 1c74d4d..224c7d3 100644 --- a/src/Message/Components/ActionRow.tsx +++ b/src/Message/Components/ActionRow.tsx @@ -1,6 +1,6 @@ import type { APIActionRowComponent, - APIMessageActionRowComponent, + APIComponentInMessageActionRow, } from "discord-api-types/v10"; import * as Styles from "./style"; import React from "react"; @@ -8,7 +8,7 @@ import Component from "./Component"; import type { ChatMessage } from "../../types"; interface ActionRowProps { - actionRow: APIActionRowComponent; + actionRow: APIActionRowComponent; message: ChatMessage; } diff --git a/src/Message/Components/ButtonComponent.tsx b/src/Message/Components/ButtonComponent.tsx index 35e15f7..e4fe77d 100644 --- a/src/Message/Components/ButtonComponent.tsx +++ b/src/Message/Components/ButtonComponent.tsx @@ -19,6 +19,7 @@ const buttonStyleMap: Record< [ButtonStyle.Primary]: "primary", [ButtonStyle.Secondary]: "secondary", [ButtonStyle.Link]: "secondary", + [ButtonStyle.Premium]: "secondary", [ButtonStyle.Danger]: "danger", [ButtonStyle.Success]: "success", }; diff --git a/src/Message/Components/Component.tsx b/src/Message/Components/Component.tsx index 16c95fe..4e3a3ed 100644 --- a/src/Message/Components/Component.tsx +++ b/src/Message/Components/Component.tsx @@ -1,19 +1,43 @@ -import type { APIMessageActionRowComponent } from "discord-api-types/v10"; +import type { APIMessageComponent } from "discord-api-types/v10"; import { ComponentType } from "discord-api-types/v10"; +import ActionRow from "./ActionRow"; import ButtonComponent from "./ButtonComponent"; +import ContainerComponent from "./ContainerComponent"; +import FileComponent from "./FileComponent"; +import MediaGalleryComponent from "./MediaGalleryComponent"; import React from "react"; +import SectionComponent from "./SectionComponent"; +import SeparatorComponent from "./SeparatorComponent"; +import TextDisplayComponent from "./TextDisplayComponent"; +import ThumbnailComponent from "./ThumbnailComponent"; import type { ChatMessage } from "../../types"; interface ComponentProps { - component: APIMessageActionRowComponent; + component: APIMessageComponent; message: ChatMessage; } function Component({ component, message }: ComponentProps) { switch (component.type) { + case ComponentType.ActionRow: + return ; case ComponentType.Button: // @ts-expect-error TS2322 Type error not applicable (tl;dr: button emoji is not optional) return ; + case ComponentType.Section: + return ; + case ComponentType.TextDisplay: + return ; + case ComponentType.Thumbnail: + return ; + case ComponentType.MediaGallery: + return ; + case ComponentType.File: + return ; + case ComponentType.Separator: + return ; + case ComponentType.Container: + return ; default: return null; } diff --git a/src/Message/Components/ContainerComponent.tsx b/src/Message/Components/ContainerComponent.tsx new file mode 100644 index 0000000..925a56f --- /dev/null +++ b/src/Message/Components/ContainerComponent.tsx @@ -0,0 +1,36 @@ +import type { APIContainerComponent } from "discord-api-types/v10"; +import Component from "./Component"; +import React from "react"; +import Spoiler from "./Spoiler"; +import * as Styles from "./style"; +import { colorIntToRgba } from "../../utils/colorIntToCss"; +import type { ChatMessage } from "../../types"; + +interface ContainerComponentProps { + container: APIContainerComponent; + message: ChatMessage; +} + +function ContainerComponent({ container, message }: ContainerComponentProps) { + const accentColor = + container.accent_color === null ? undefined : container.accent_color; + + const content = ( + + {container.components.map((component, index) => ( + + ))} + + ); + + return container.spoiler ? {content} : content; +} + +export default ContainerComponent; diff --git a/src/Message/Components/FileComponent.tsx b/src/Message/Components/FileComponent.tsx new file mode 100644 index 0000000..2f10705 --- /dev/null +++ b/src/Message/Components/FileComponent.tsx @@ -0,0 +1,50 @@ +import type { APIFileComponent } from "discord-api-types/v10"; +import { filesize } from "filesize"; +import React from "react"; +import Spoiler from "./Spoiler"; +import SvgFromUrl from "../../SvgFromUrl"; +import * as Styles from "./style"; + +interface FileComponentProps { + file: APIFileComponent; +} + +function getFileName(file: APIFileComponent): string { + if (file.name) return file.name; + + const path = file.file.url.split("?")[0]; + return path.split("/").pop() || file.file.url; +} + +function FileComponent({ file }: FileComponentProps) { + const card = ( + + + + + {getFileName(file)} + + {file.size !== undefined && ( + + {filesize(file.size, { base: 2 }) as string} + + )} + + + + + + ); + + return file.spoiler ? {card} : card; +} + +export default FileComponent; diff --git a/src/Message/Components/MediaGalleryComponent.tsx b/src/Message/Components/MediaGalleryComponent.tsx new file mode 100644 index 0000000..82b711d --- /dev/null +++ b/src/Message/Components/MediaGalleryComponent.tsx @@ -0,0 +1,29 @@ +import type { APIMediaGalleryComponent } from "discord-api-types/v10"; +import MediaItem from "./MediaItem"; +import React from "react"; +import Spoiler from "./Spoiler"; +import * as Styles from "./style"; + +interface MediaGalleryComponentProps { + mediaGallery: APIMediaGalleryComponent; +} + +function MediaGalleryComponent({ mediaGallery }: MediaGalleryComponentProps) { + return ( + + {mediaGallery.items.map((item, index) => { + const media = ( + + ); + + return ( + + {item.spoiler ? {media} : media} + + ); + })} + + ); +} + +export default MediaGalleryComponent; diff --git a/src/Message/Components/MediaItem.tsx b/src/Message/Components/MediaItem.tsx new file mode 100644 index 0000000..e0073aa --- /dev/null +++ b/src/Message/Components/MediaItem.tsx @@ -0,0 +1,48 @@ +import type { APIUnfurledMediaItem } from "discord-api-types/v10"; +import useSize from "../../Content/Attachment/useSize"; +import React from "react"; +import * as Styles from "./style"; +import { t } from "i18next"; + +interface MediaItemProps { + media: APIUnfurledMediaItem; + description?: string | null; +} + +function MediaItem({ media, description }: MediaItemProps) { + const hasDimensions = + typeof media.width === "number" && typeof media.height === "number"; + const { width, height } = useSize( + media.width ?? 0, + media.height ?? 0, + !hasDimensions + ); + + if (media.content_type?.startsWith("video/")) { + return ( + + ); + } + + return ( + + {t("loading")} + + } + /> + ); +} + +export default MediaItem; diff --git a/src/Message/Components/SectionComponent.tsx b/src/Message/Components/SectionComponent.tsx new file mode 100644 index 0000000..8220629 --- /dev/null +++ b/src/Message/Components/SectionComponent.tsx @@ -0,0 +1,27 @@ +import type { APISectionComponent } from "discord-api-types/v10"; +import Component from "./Component"; +import React from "react"; +import * as Styles from "./style"; +import type { ChatMessage } from "../../types"; + +interface SectionComponentProps { + section: APISectionComponent; + message: ChatMessage; +} + +function SectionComponent({ section, message }: SectionComponentProps) { + return ( + + + {section.components.map((component, index) => ( + + ))} + + + + + + ); +} + +export default SectionComponent; diff --git a/src/Message/Components/SeparatorComponent.tsx b/src/Message/Components/SeparatorComponent.tsx new file mode 100644 index 0000000..0593666 --- /dev/null +++ b/src/Message/Components/SeparatorComponent.tsx @@ -0,0 +1,21 @@ +import type { APISeparatorComponent } from "discord-api-types/v10"; +import { SeparatorSpacingSize } from "discord-api-types/v10"; +import React from "react"; +import * as Styles from "./style"; + +interface SeparatorComponentProps { + separator: APISeparatorComponent; +} + +function SeparatorComponent({ separator }: SeparatorComponentProps) { + return ( + + ); +} + +export default SeparatorComponent; diff --git a/src/Message/Components/Spoiler.tsx b/src/Message/Components/Spoiler.tsx new file mode 100644 index 0000000..ddf272f --- /dev/null +++ b/src/Message/Components/Spoiler.tsx @@ -0,0 +1,18 @@ +import React, { useState } from "react"; +import * as Styles from "./style"; + +interface SpoilerProps { + children: React.ReactNode; +} + +function Spoiler({ children }: SpoilerProps) { + const [shown, setShown] = useState(false); + + return ( + setShown(true)}> + {children} + + ); +} + +export default Spoiler; diff --git a/src/Message/Components/TextDisplayComponent.tsx b/src/Message/Components/TextDisplayComponent.tsx new file mode 100644 index 0000000..ad7e912 --- /dev/null +++ b/src/Message/Components/TextDisplayComponent.tsx @@ -0,0 +1,23 @@ +import type { APITextDisplayComponent } from "discord-api-types/v10"; +import { Markdown } from "../../markdown/render"; +import React from "react"; +import * as Styles from "./style"; +import type { ChatMessage } from "../../types"; + +interface TextDisplayComponentProps { + textDisplay: APITextDisplayComponent; + message: ChatMessage; +} + +function TextDisplayComponent({ + textDisplay, + message, +}: TextDisplayComponentProps) { + return ( + + {textDisplay.content} + + ); +} + +export default TextDisplayComponent; diff --git a/src/Message/Components/ThumbnailComponent.tsx b/src/Message/Components/ThumbnailComponent.tsx new file mode 100644 index 0000000..34c422e --- /dev/null +++ b/src/Message/Components/ThumbnailComponent.tsx @@ -0,0 +1,23 @@ +import type { APIThumbnailComponent } from "discord-api-types/v10"; +import MediaItem from "./MediaItem"; +import React from "react"; +import Spoiler from "./Spoiler"; +import * as Styles from "./style"; + +interface ThumbnailComponentProps { + thumbnail: APIThumbnailComponent; +} + +function ThumbnailComponent({ thumbnail }: ThumbnailComponentProps) { + const media = ( + + ); + + return ( + + {thumbnail.spoiler ? {media} : media} + + ); +} + +export default ThumbnailComponent; diff --git a/src/Message/Components/index.tsx b/src/Message/Components/index.tsx index a886ec6..2c0ed05 100644 --- a/src/Message/Components/index.tsx +++ b/src/Message/Components/index.tsx @@ -1,4 +1,4 @@ -import ActionRow from "./ActionRow"; +import Component from "./Component"; import React from "react"; import * as Styles from "./style"; import type { ChatMessage } from "../../types"; @@ -13,8 +13,8 @@ function Components({ components, message }: ComponentsProps) { return ( - {components.map((actionRow, index) => ( - + {components.map((component, index) => ( + ))} ); diff --git a/src/Message/Components/style.ts b/src/Message/Components/style.ts index 3730ff6..523ec99 100644 --- a/src/Message/Components/style.ts +++ b/src/Message/Components/style.ts @@ -3,6 +3,8 @@ import { styled, theme, } from "../../Stitches/stitches.config"; +import { LazyLoadImage } from "react-lazy-load-image-component"; +import ExternalLink from "../../ExternalLink"; export const Components = styled.withConfig({ displayName: "message-components", @@ -88,3 +90,270 @@ export const ActionRow = styled.withConfig({ flexDirection: "row", gap: theme.space.large, }); + +export const TextDisplay = styled.withConfig({ + displayName: "text-display", + componentId: commonComponentId, +})("div", { + fontSize: theme.fontSizes.m, + lineHeight: "1.375rem", + color: theme.colors.primaryOpacity100, + whiteSpace: "pre-wrap", + wordWrap: "break-word", +}); + +export const Separator = styled.withConfig({ + displayName: "separator", + componentId: commonComponentId, +})("div", { + display: "flex", + alignItems: "center", + + variants: { + spacing: { + small: { + paddingBlock: theme.space.large, + }, + large: { + paddingBlock: theme.space.xl, + }, + }, + divider: { + true: { + "&::before": { + content: '""', + width: "100%", + height: 1, + backgroundColor: theme.colors.primaryOpacity10, + }, + }, + false: {}, + }, + }, +}); + +export const Section = styled.withConfig({ + displayName: "section", + componentId: commonComponentId, +})("div", { + display: "flex", + flexDirection: "row", + alignItems: "flex-start", + justifyContent: "space-between", + gap: theme.space.xl, +}); + +export const SectionContent = styled.withConfig({ + displayName: "section-content", + componentId: commonComponentId, +})("div", { + display: "flex", + flexDirection: "column", + gap: theme.space.large, + flex: 1, + minWidth: 0, +}); + +export const SectionAccessory = styled.withConfig({ + displayName: "section-accessory", + componentId: commonComponentId, +})("div", { + display: "flex", + flexShrink: 0, +}); + +export const Container = styled.withConfig({ + displayName: "container", + componentId: commonComponentId, +})("div", { + display: "flex", + flexDirection: "column", + gap: theme.space.large, + padding: theme.space.xl, + borderRadius: 8, + backgroundColor: theme.colors.backgroundSecondary, + maxWidth: 520, + boxSizing: "border-box", + + variants: { + hasAccent: { + true: { + borderLeftStyle: "solid", + borderLeftWidth: 4, + }, + false: {}, + }, + }, +}); + +export const Thumbnail = styled.withConfig({ + displayName: "thumbnail", + componentId: commonComponentId, +})("div", { + display: "flex", + flexShrink: 0, + maxWidth: 120, +}); + +export const MediaGallery = styled.withConfig({ + displayName: "media-gallery", + componentId: commonComponentId, +})("div", { + display: "grid", + gridTemplateColumns: "repeat(auto-fit, minmax(120px, 1fr))", + gap: theme.space.small, + maxWidth: 400, +}); + +export const MediaGalleryItem = styled.withConfig({ + displayName: "media-gallery-item", + componentId: commonComponentId, +})("div", { + display: "flex", + overflow: "hidden", + borderRadius: 8, +}); + +export const MediaImage = styled.withConfig({ + displayName: "media-image", + componentId: commonComponentId, +})(LazyLoadImage, { + maxWidth: "100%", + height: "auto", + display: "block", + borderRadius: 8, +}); + +export const MediaVideo = styled.withConfig({ + displayName: "media-video", + componentId: commonComponentId, +})("video", { + maxWidth: "100%", + height: "auto", + display: "block", + borderRadius: 8, + outline: "none", +}); + +export const LazyImagePlaceholder = styled.withConfig({ + displayName: "lazy-image-placeholder", + componentId: commonComponentId, +})("div", { + backgroundColor: theme.colors.lazyImageBackground, + color: theme.colors.primaryOpacity100, + display: "flex", + alignItems: "center", + justifyContent: "center", + maxWidth: "100%", + height: "auto", +}); + +export const Spoiler = styled.withConfig({ + displayName: "spoiler", + componentId: commonComponentId, +})("div", { + overflow: "hidden", + position: "relative", + width: "fit-content", + height: "fit-content", + borderRadius: 8, + + "& > *": { + transition: "filter 0.2s ease-in-out", + filter: "blur(44px)", + }, + + "&[data-show='true'] > *": { + filter: "blur(0)", + }, + + "&[data-show='false']": { + cursor: "pointer", + + "& > *": { + pointerEvents: "none", + }, + + "&:after": { + content: "SPOILER", + backgroundColor: theme.colors.transparentBlack, + color: theme.colors.primaryOpacity100, + borderRadius: 100, + padding: `${theme.space.large} ${theme.space.xl}`, + position: "absolute", + left: "50%", + top: "50%", + translate: "-50% -50%", + fontWeight: 600, + fontSize: theme.fontSizes.l, + letterSpacing: 0.5, + }, + + "&:hover:after": { + backgroundColor: theme.colors.blackSpoilerHover, + }, + }, +}); + +export const FileContainer = styled.withConfig({ + displayName: "file-component", + componentId: commonComponentId, +})("div", { + display: "flex", + alignItems: "center", + gap: theme.space.large, + backgroundColor: theme.colors.backgroundSecondary, + padding: 10, + border: `1px solid ${theme.colors.attachmentBorder}`, + borderRadius: 8, + width: "fit-content", + maxWidth: "100%", + boxSizing: "border-box", +}); + +export const FileMetadata = styled.withConfig({ + displayName: "file-metadata", + componentId: commonComponentId, +})("div", { + display: "flex", + flexDirection: "column", + minWidth: 0, +}); + +export const FileName = styled.withConfig({ + displayName: "file-name", + componentId: commonComponentId, +})(ExternalLink, { + color: theme.colors.link, + maxWidth: "40vw", + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + textDecoration: "none", + + "&:hover": { + textDecoration: "underline", + }, +}); + +export const FileSize = styled.withConfig({ + displayName: "file-size", + componentId: commonComponentId, +})("span", { + fontSize: theme.fontSizes.s, + color: theme.colors.primaryDark, +}); + +export const FileDownload = styled.withConfig({ + displayName: "file-download", + componentId: commonComponentId, +})(ExternalLink, { + opacity: 0.8, + marginLeft: "auto", + color: theme.colors.primaryOpacity100, + display: "flex", + + "&:hover": { + opacity: 1, + }, +}); diff --git a/src/stories/ComponentsV2.stories.tsx b/src/stories/ComponentsV2.stories.tsx new file mode 100644 index 0000000..d395b13 --- /dev/null +++ b/src/stories/ComponentsV2.stories.tsx @@ -0,0 +1,203 @@ +import { MessageGroup } from "../index"; +import React from "react"; +import type { APIMessage } from "discord-api-types/v10"; +import type { Meta, StoryFn } from "@storybook/react"; +import Wrapper from "./Wrapper"; + +const meta: Meta = { + title: "Message Types/Normal/Components V2", + component: MessageGroup, + parameters: { + layout: "centered", + }, + decorators: [Wrapper], +}; + +export default meta; + +function message(components: APIMessage["components"], content = ""): APIMessage { + return { + type: 0, + tts: false, + timestamp: "2021-05-19T02:12:51.710000+00:00", + pinned: false, + mentions: [], + mention_roles: [], + mention_everyone: false, + id: "844397162624450620", + flags: 0, + embeds: [], + edited_timestamp: null, + content, + channel_id: "697138785317814292", + author: { + username: "mason", + global_name: "Mason", + public_flags: 4325445, + id: "53908232506183680", + discriminator: "0", + avatar: "a_d5efa99b3eeaa7dd43acca82f5692432", + }, + attachments: [], + components, + }; +} + +// eslint-disable-next-line func-style +const Template: StoryFn = (args) => ( + +); + +export const Container: StoryFn = Template.bind({}); +Container.args = { + messages: [ + message([ + { + type: 17, + accent_color: 0x5865f2, + components: [ + { + type: 10, + content: + "# Components V2\nA **container** groups related content together with an optional accent colour.", + }, + { type: 14, divider: true, spacing: 1 }, + { + type: 9, + components: [ + { + type: 10, + content: + "A **section** pairs text content with an accessory on the right.", + }, + ], + accessory: { + type: 11, + media: { + url: "https://picsum.photos/seed/widgetbot-thumb/160/160", + width: 160, + height: 160, + }, + }, + }, + { + type: 1, + components: [ + { type: 2, style: 1, label: "Primary", custom_id: "primary" }, + { type: 2, style: 5, label: "Open", url: "https://widgetbot.io" }, + ], + }, + ], + }, + ]), + ], +}; + +export const TextDisplay: StoryFn = Template.bind({}); +TextDisplay.args = { + messages: [ + message([ + { + type: 10, + content: + "# Text display\nText displays render **markdown**, including lists:\n- One\n- Two\n- Three", + }, + { type: 10, content: "Each text display is its own block." }, + ]), + ], +}; + +export const Section: StoryFn = Template.bind({}); +Section.args = { + messages: [ + message([ + { + type: 9, + components: [{ type: 10, content: "A section with a thumbnail accessory." }], + accessory: { + type: 11, + media: { + url: "https://picsum.photos/seed/widgetbot-section/160/160", + width: 160, + height: 160, + }, + }, + }, + { + type: 9, + components: [{ type: 10, content: "A section with a button accessory." }], + accessory: { + type: 2, + style: 1, + label: "Click me", + custom_id: "section-button", + }, + }, + ]), + ], +}; + +export const MediaGallery: StoryFn = Template.bind({}); +MediaGallery.args = { + messages: [ + message([ + { + type: 12, + items: [ + { + media: { + url: "https://picsum.photos/seed/widgetbot-1/300/200", + width: 300, + height: 200, + }, + description: "First image", + }, + { + media: { + url: "https://picsum.photos/seed/widgetbot-2/300/200", + width: 300, + height: 200, + }, + }, + { + media: { + url: "https://picsum.photos/seed/widgetbot-3/300/200", + width: 300, + height: 200, + }, + spoiler: true, + }, + ], + }, + ]), + ], +}; + +export const File: StoryFn = Template.bind({}); +File.args = { + messages: [ + message([ + { + type: 13, + file: { url: "https://example.com/manual.pdf" }, + name: "manual.pdf", + size: 1048576, + }, + ]), + ], +}; + +export const Separators: StoryFn = Template.bind({}); +Separators.args = { + messages: [ + message([ + { type: 10, content: "Above a small divider." }, + { type: 14, divider: true, spacing: 1 }, + { type: 10, content: "Between dividers." }, + { type: 14, divider: true, spacing: 2 }, + { type: 10, content: "Below a large divider, above invisible spacing." }, + { type: 14, divider: false, spacing: 2 }, + { type: 10, content: "After spacing without a divider." }, + ]), + ], +}; From e9b9fbb221bcfab962d8c99e8ca0595cac5879c4 Mon Sep 17 00:00:00 2001 From: Lewis Date: Wed, 24 Jun 2026 15:11:28 +0100 Subject: [PATCH 2/2] Bump version from v2.4.1 to v3.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cf86d7e..b92e02b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@widgetbot/message-renderer", - "version": "v2.4.1", + "version": "v3.0.0", "description": "", "module": "dist/message-renderer.mjs", "files": [