@linktr.ee/messaging-react 3.9.2 → 3.10.0-rc-1783605409

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/{Card-CVxFApFQ.cjs → Card-B8tKJ_Ax.cjs} +2 -2
  2. package/dist/{Card-CVxFApFQ.cjs.map → Card-B8tKJ_Ax.cjs.map} +1 -1
  3. package/dist/{Card-ckN-pJAf.js → Card-C_HNnA8B.js} +2 -2
  4. package/dist/{Card-ckN-pJAf.js.map → Card-C_HNnA8B.js.map} +1 -1
  5. package/dist/{Card-B83PIVO4.js → Card-CiuPUMBG.js} +2 -2
  6. package/dist/{Card-B83PIVO4.js.map → Card-CiuPUMBG.js.map} +1 -1
  7. package/dist/{Card-Cblg_OP2.js → Card-CpH1yJmm.js} +3 -3
  8. package/dist/{Card-Cblg_OP2.js.map → Card-CpH1yJmm.js.map} +1 -1
  9. package/dist/{Card-DyFTYx54.cjs → Card-DhXNjcpF.cjs} +2 -2
  10. package/dist/{Card-DyFTYx54.cjs.map → Card-DhXNjcpF.cjs.map} +1 -1
  11. package/dist/{Card-BcTjQXjg.cjs → Card-U-sU4Zv-.cjs} +2 -2
  12. package/dist/{Card-BcTjQXjg.cjs.map → Card-U-sU4Zv-.cjs.map} +1 -1
  13. package/dist/{LockedThumbnail-CZ8hj3VY.js → LockedThumbnail-BwSdYQrE.js} +2 -2
  14. package/dist/{LockedThumbnail-CZ8hj3VY.js.map → LockedThumbnail-BwSdYQrE.js.map} +1 -1
  15. package/dist/{LockedThumbnail-C-MNksZm.cjs → LockedThumbnail-CijnuXw_.cjs} +2 -2
  16. package/dist/{LockedThumbnail-C-MNksZm.cjs.map → LockedThumbnail-CijnuXw_.cjs.map} +1 -1
  17. package/dist/assets/index.css +1 -1
  18. package/dist/{index-CPbf44kW.js → index-DSY7bIL-.js} +650 -572
  19. package/dist/index-DSY7bIL-.js.map +1 -0
  20. package/dist/{index-C2hIrXzf.cjs → index-Du7nTLh4.cjs} +2 -2
  21. package/dist/index-Du7nTLh4.cjs.map +1 -0
  22. package/dist/index.cjs +1 -1
  23. package/dist/index.d.ts +55 -0
  24. package/dist/index.js +20 -19
  25. package/package.json +2 -2
  26. package/src/components/MessageBubble/MessageBubble.stories.tsx +98 -0
  27. package/src/components/MessageBubble/MessageBubble.test.tsx +71 -0
  28. package/src/components/MessageBubble/components/Received/Card.tsx +27 -0
  29. package/src/components/MessageBubble/components/Received/index.ts +1 -0
  30. package/src/components/MessageBubble/components/Sent/Card.tsx +23 -0
  31. package/src/components/MessageBubble/components/Sent/index.ts +1 -0
  32. package/src/components/MessageBubble/components/_shared/BubbleShell.tsx +77 -0
  33. package/src/components/MessageBubble/components/_shared/bubbleTail.tsx +48 -0
  34. package/src/components/MessageBubble/index.tsx +31 -0
  35. package/src/components/MessageBubble/types.ts +34 -0
  36. package/src/index.ts +7 -0
  37. package/src/styles.css +1 -1
  38. package/dist/index-C2hIrXzf.cjs.map +0 -1
  39. package/dist/index-CPbf44kW.js.map +0 -1
package/dist/index.cjs CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./index-C2hIrXzf.cjs");exports.ActionButton=e.ActionButton;exports.Avatar=e.Avatar;exports.ChannelEmptyState=e.ChannelEmptyState;exports.ChannelList=e.ChannelList;exports.ChannelView=e.ChannelView;exports.CustomMessageProvider=e.CustomMessageProvider;exports.FaqList=e.FaqList;exports.FaqListItem=e.FaqListItem;exports.LinkAttachment=e.LinkAttachment;exports.LockedAttachment=e.LockedAttachment;exports.MediaMessage=e.MediaMessage;exports.MessageAttachment=e.MessageAttachment;exports.MessageVoteButtons=e.MessageVoteButtons;exports.MessagingProvider=e.MessagingProvider;exports.MessagingShell=e.MessagingShell;exports.buildCompactMetaLabel=e.buildCompactMetaLabel;exports.formatFileSize=e.formatFileSize;exports.formatRelativeTime=e.formatRelativeTime;exports.getFileExtensionLabel=e.getFileExtensionLabel;exports.getMessageDisplayText=e.getMessageDisplayText;exports.isLinkAttachment=e.isLinkAttachment;exports.isUuidLike=e.isUuidLike;exports.messageAttachmentGroupPositionFromStream=e.bubbleGroupPositionFromStream;exports.normalizeLanguageCode=e.normalizeLanguageCode;exports.resolveConversationParticipant=e.resolveConversationParticipant;exports.resolveLinkAttachment=e.resolveLinkAttachment;exports.resolveMediaFromMessage=e.resolveMediaFromMessage;exports.resolveParticipantDisplayName=e.resolveParticipantDisplayName;exports.useComposerLocked=e.useComposerLocked;exports.useCustomMessage=e.useCustomMessage;exports.useMessageVote=e.useMessageVote;exports.useMessaging=e.useMessaging;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./index-Du7nTLh4.cjs");exports.ActionButton=e.ActionButton;exports.Avatar=e.Avatar;exports.ChannelEmptyState=e.ChannelEmptyState;exports.ChannelList=e.ChannelList;exports.ChannelView=e.ChannelView;exports.CustomMessageProvider=e.CustomMessageProvider;exports.FaqList=e.FaqList;exports.FaqListItem=e.FaqListItem;exports.LinkAttachment=e.LinkAttachment;exports.LockedAttachment=e.LockedAttachment;exports.MediaMessage=e.MediaMessage;exports.MessageAttachment=e.MessageAttachment;exports.MessageBubble=e.MessageBubble;exports.MessageVoteButtons=e.MessageVoteButtons;exports.MessagingProvider=e.MessagingProvider;exports.MessagingShell=e.MessagingShell;exports.buildCompactMetaLabel=e.buildCompactMetaLabel;exports.formatFileSize=e.formatFileSize;exports.formatRelativeTime=e.formatRelativeTime;exports.getFileExtensionLabel=e.getFileExtensionLabel;exports.getMessageDisplayText=e.getMessageDisplayText;exports.isLinkAttachment=e.isLinkAttachment;exports.isUuidLike=e.isUuidLike;exports.messageAttachmentGroupPositionFromStream=e.bubbleGroupPositionFromStream;exports.normalizeLanguageCode=e.normalizeLanguageCode;exports.resolveConversationParticipant=e.resolveConversationParticipant;exports.resolveLinkAttachment=e.resolveLinkAttachment;exports.resolveMediaFromMessage=e.resolveMediaFromMessage;exports.resolveParticipantDisplayName=e.resolveParticipantDisplayName;exports.useComposerLocked=e.useComposerLocked;exports.useCustomMessage=e.useCustomMessage;exports.useMessageVote=e.useMessageVote;exports.useMessaging=e.useMessaging;
2
2
  //# sourceMappingURL=index.cjs.map
package/dist/index.d.ts CHANGED
@@ -1166,6 +1166,50 @@ export declare type MessageAttachmentVideoReceivedProps = VideoAttachmentSharedP
1166
1166
 
1167
1167
  export declare type MessageAttachmentVideoSentProps = VideoAttachmentSharedProps;
1168
1168
 
1169
+ /**
1170
+ * A dumb, context-free chat message bubble for rendering a single message row
1171
+ * *outside* a live channel — previews, digests, confirmation dialogs, the
1172
+ * Stacks "responded" summary, etc. Unlike `ChannelView`, it needs no Stream
1173
+ * client or provider.
1174
+ *
1175
+ * Render `MessageBubble.Sent` for the sender's own treatment (dark, right
1176
+ * tail) and `MessageBubble.Received` for the recipient's (light, left tail,
1177
+ * optional header). Surface colours, radius, typography, and the tail
1178
+ * silhouette are shared with the in-channel bubble drawn by `CustomMessage`,
1179
+ * so previews stay pixel-identical and re-theme from one source of truth.
1180
+ *
1181
+ * The variant is a **visual treatment, not an authorship claim**: to preview
1182
+ * an outgoing message as the recipient will see it, render
1183
+ * `MessageBubble.Received` with your own text.
1184
+ */
1185
+ export declare const MessageBubble: {
1186
+ Sent: FC<SentBubbleProps>;
1187
+ Received: FC<ReceivedBubbleProps>;
1188
+ };
1189
+
1190
+ export declare interface MessageBubbleBaseProps {
1191
+ /**
1192
+ * Message body. Rendered verbatim with `whitespace-pre-wrap` (newlines
1193
+ * preserved); this is a dumb component, so no markdown / link parsing.
1194
+ */
1195
+ text: string;
1196
+ /**
1197
+ * Draw the bubble tail at the bottom corner. `sent` tails point
1198
+ * bottom-right, `received` bottom-left. Defaults to `true`.
1199
+ */
1200
+ tail?: boolean;
1201
+ /** Extra classes on the outer bubble (e.g. `max-w-[520px]`). */
1202
+ className?: string;
1203
+ }
1204
+
1205
+ /**
1206
+ * Which visual treatment the bubble paints. This is decoupled from message
1207
+ * *authorship*: it selects a side + colour, not who wrote the text. To preview
1208
+ * an outgoing message as the recipient will see it (e.g. a bulk-reply
1209
+ * confirmation), render the `received` treatment with your own text.
1210
+ */
1211
+ export declare type MessageBubbleVariant = 'sent' | 'received';
1212
+
1169
1213
  declare type MessageCustomType = 'MESSAGE_TIP' | 'MESSAGE_PAID' | 'MESSAGE_CHATBOT' | 'MESSAGE_ATTACHMENT' | AgeSafetySystemType | DmAgentSystemType;
1170
1214
 
1171
1215
  /**
@@ -1396,6 +1440,14 @@ declare interface PdfAttachmentSharedProps extends MessageAttachmentBaseProps {
1396
1440
  onClick?: (index: number) => boolean | void;
1397
1441
  }
1398
1442
 
1443
+ export declare interface ReceivedBubbleProps extends MessageBubbleBaseProps {
1444
+ /**
1445
+ * Optional header rendered above the text inside the bubble — used by the
1446
+ * Stacks message list to show the sender's name. Omit for a plain bubble.
1447
+ */
1448
+ header?: default_2.ReactNode;
1449
+ }
1450
+
1399
1451
  export declare interface ReceivedCardProps extends LockedAttachmentBaseProps {
1400
1452
  /**
1401
1453
  * Called when the recipient clicks Unlock on an unpaid attachment.
@@ -1443,6 +1495,9 @@ export declare function resolveMediaFromMessage(message: LocalMessage): MediaMes
1443
1495
  */
1444
1496
  export declare function resolveParticipantDisplayName(user?: ParticipantDisplayUser | null): string;
1445
1497
 
1498
+ export declare interface SentBubbleProps extends MessageBubbleBaseProps {
1499
+ }
1500
+
1446
1501
  export declare interface SentCardProps extends LockedAttachmentBaseProps {
1447
1502
  /** Placeholder shown in the title slot when no title is set. */
1448
1503
  placeholderTitle?: string;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as e, b as t, C as i, c as o, d as n, e as m, F as r, f as g, L as l, h as M, M as c, i as u, j as L, k as h, l as d, m as p, n as v, o as C, p as A, q as k, s as F, t as P, u as b, v as f, w as x, x as y, y as S, z as q, B as z, D as B, E as D, G as E } from "./index-CPbf44kW.js";
1
+ import { a as e, b as t, C as i, c as o, d as n, e as m, F as r, f as g, L as l, h as M, M as u, i as c, j as L, k as h, l as d, m as p, n as v, o as C, p as A, q as k, s as F, t as b, u as P, v as f, w as x, x as y, y as B, z as S, B as q, D as z, E as D, G as E, H as V } from "./index-DSY7bIL-.js";
2
2
  export {
3
3
  e as ActionButton,
4
4
  t as Avatar,
@@ -10,27 +10,28 @@ export {
10
10
  g as FaqListItem,
11
11
  l as LinkAttachment,
12
12
  M as LockedAttachment,
13
- c as MediaMessage,
14
- u as MessageAttachment,
15
- L as MessageVoteButtons,
16
- h as MessagingProvider,
17
- d as MessagingShell,
18
- p as buildCompactMetaLabel,
19
- v as formatFileSize,
20
- C as formatRelativeTime,
21
- A as getFileExtensionLabel,
22
- k as getMessageDisplayText,
23
- F as isLinkAttachment,
13
+ u as MediaMessage,
14
+ c as MessageAttachment,
15
+ L as MessageBubble,
16
+ h as MessageVoteButtons,
17
+ d as MessagingProvider,
18
+ p as MessagingShell,
19
+ v as buildCompactMetaLabel,
20
+ C as formatFileSize,
21
+ A as formatRelativeTime,
22
+ k as getFileExtensionLabel,
23
+ F as getMessageDisplayText,
24
+ b as isLinkAttachment,
24
25
  P as isUuidLike,
25
- b as messageAttachmentGroupPositionFromStream,
26
- f as normalizeLanguageCode,
27
- x as resolveConversationParticipant,
28
- y as resolveLinkAttachment,
26
+ f as messageAttachmentGroupPositionFromStream,
27
+ x as normalizeLanguageCode,
28
+ y as resolveConversationParticipant,
29
+ B as resolveLinkAttachment,
29
30
  S as resolveMediaFromMessage,
30
31
  q as resolveParticipantDisplayName,
31
32
  z as useComposerLocked,
32
- B as useCustomMessage,
33
- D as useMessageVote,
34
- E as useMessaging
33
+ D as useCustomMessage,
34
+ E as useMessageVote,
35
+ V as useMessaging
35
36
  };
36
37
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linktr.ee/messaging-react",
3
- "version": "3.9.2",
3
+ "version": "3.10.0-rc-1783605409",
4
4
  "description": "React messaging components built on messaging-core for web applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -50,7 +50,7 @@
50
50
  },
51
51
  "dependencies": {
52
52
  "@linktr.ee/component-library": "11.8.6",
53
- "@linktr.ee/messaging-core": "^2.2.0",
53
+ "@linktr.ee/messaging-core": "2.3.0-rc-1783605409",
54
54
  "@phosphor-icons/react": "^2.1.10"
55
55
  },
56
56
  "devDependencies": {
@@ -0,0 +1,98 @@
1
+ import type { Meta, StoryFn } from '@storybook/react'
2
+ import React from 'react'
3
+
4
+ import MessageBubble from '.'
5
+
6
+ const meta: Meta = {
7
+ title: 'MessageBubble',
8
+ parameters: { layout: 'fullscreen' },
9
+ }
10
+ export default meta
11
+
12
+ const Canvas = ({ children }: { children: React.ReactNode }) => (
13
+ <div className="min-h-screen w-full space-y-6 bg-[#F9F7F4] p-12">
14
+ {children}
15
+ </div>
16
+ )
17
+
18
+ const Row = ({
19
+ label,
20
+ align = 'start',
21
+ children,
22
+ }: {
23
+ label: string
24
+ align?: 'start' | 'end'
25
+ children: React.ReactNode
26
+ }) => (
27
+ <div>
28
+ <p className="mb-2 text-xs font-medium text-black/40">{label}</p>
29
+ <div
30
+ className={`flex ${align === 'end' ? 'justify-end' : 'justify-start'}`}
31
+ >
32
+ {children}
33
+ </div>
34
+ </div>
35
+ )
36
+
37
+ const SHORT = 'Hey! Thanks for reaching out 👋'
38
+ const LONG =
39
+ 'Absolutely — I have a couple of slots free next week. Happy to send over the full brief and pricing so you can take a look before we lock anything in.'
40
+ const MULTILINE = 'Line one\nLine two\nLine three'
41
+
42
+ /** Design-review matrix: both variants across tail, length, and header. */
43
+ export const Gallery: StoryFn = () => (
44
+ <Canvas>
45
+ <Row label="Received — short" align="start">
46
+ <MessageBubble.Received text={SHORT} />
47
+ </Row>
48
+ <Row label="Received — with header (sender name)" align="start">
49
+ <MessageBubble.Received header="Brie" text={LONG} className="max-w-[520px]" />
50
+ </Row>
51
+ <Row label="Received — no tail" align="start">
52
+ <MessageBubble.Received text={SHORT} tail={false} />
53
+ </Row>
54
+ <Row label="Sent — short" align="end">
55
+ <MessageBubble.Sent text={SHORT} />
56
+ </Row>
57
+ <Row label="Sent — long" align="end">
58
+ <MessageBubble.Sent text={LONG} className="max-w-[520px]" />
59
+ </Row>
60
+ <Row label="Sent — multiline" align="end">
61
+ <MessageBubble.Sent text={MULTILINE} />
62
+ </Row>
63
+ <Row label="Sent — no tail" align="end">
64
+ <MessageBubble.Sent text={SHORT} tail={false} />
65
+ </Row>
66
+ </Canvas>
67
+ )
68
+
69
+ /**
70
+ * Stacks bulk-reply confirmation: preview an outgoing message as the *visitor*
71
+ * will receive it — the recipient (grey, left) treatment despite being your
72
+ * own text.
73
+ */
74
+ export const PreviewToRecipient: StoryFn = () => (
75
+ <Canvas>
76
+ <Row label="How your reply will look to the visitor" align="start">
77
+ <MessageBubble.Received
78
+ text="Thanks so much for the support — I just dropped a new track, hope you love it! 🎶"
79
+ className="max-w-[520px]"
80
+ />
81
+ </Row>
82
+ </Canvas>
83
+ )
84
+
85
+ /**
86
+ * Stacks responded-summary: preview a sent reply as it looks *to you in the
87
+ * channel* — the sender (dark, right) treatment.
88
+ */
89
+ export const PreviewInChannel: StoryFn = () => (
90
+ <Canvas>
91
+ <Row label="How your reply looks to you in the channel" align="end">
92
+ <MessageBubble.Sent
93
+ text="Thanks so much for the support — I just dropped a new track, hope you love it! 🎶"
94
+ className="max-w-[520px]"
95
+ />
96
+ </Row>
97
+ </Canvas>
98
+ )
@@ -0,0 +1,71 @@
1
+ import React from 'react'
2
+ import { describe, expect, it } from 'vitest'
3
+
4
+ import { renderWithProviders, screen } from '../../test/utils'
5
+
6
+ import MessageBubble from '.'
7
+
8
+ describe('MessageBubble', () => {
9
+ it('renders the message text', () => {
10
+ renderWithProviders(<MessageBubble.Sent text="Hello there" />)
11
+ expect(screen.getByText('Hello there')).toBeInTheDocument()
12
+ })
13
+
14
+ it('preserves newlines with whitespace-pre-wrap', () => {
15
+ renderWithProviders(<MessageBubble.Sent text={'line one\nline two'} />)
16
+ const paragraph = screen.getByText(/line one/)
17
+ expect(paragraph.textContent).toBe('line one\nline two')
18
+ expect(paragraph.className).toContain('whitespace-pre-wrap')
19
+ })
20
+
21
+ it('draws the tail by default and omits it when tail is false', () => {
22
+ const { rerender } = renderWithProviders(
23
+ <MessageBubble.Sent text="tailed" />
24
+ )
25
+ // The bubble box wraps the text; its last child is the tail <span>.
26
+ const bubble = screen.getByText('tailed').parentElement as HTMLElement
27
+ expect(bubble.querySelector('span[aria-hidden="true"]')).not.toBeNull()
28
+
29
+ rerender(<MessageBubble.Sent text="tailed" tail={false} />)
30
+ const bubbleNoTail = screen.getByText('tailed').parentElement as HTMLElement
31
+ expect(bubbleNoTail.querySelector('span[aria-hidden="true"]')).toBeNull()
32
+ })
33
+
34
+ it('renders the optional header on the received variant', () => {
35
+ renderWithProviders(
36
+ <MessageBubble.Received header="Brie" text="How much for a feature?" />
37
+ )
38
+ expect(screen.getByTestId('message-bubble-header')).toHaveTextContent(
39
+ 'Brie'
40
+ )
41
+ })
42
+
43
+ it('does not render a header slot when no header is provided', () => {
44
+ renderWithProviders(<MessageBubble.Received text="no header" />)
45
+ expect(screen.queryByTestId('message-bubble-header')).toBeNull()
46
+ })
47
+
48
+ it('applies the sent and received surface treatments from theme vars', () => {
49
+ const { rerender } = renderWithProviders(
50
+ <MessageBubble.Sent text="mine" />
51
+ )
52
+ const sent = screen.getByText('mine').parentElement as HTMLElement
53
+ expect(sent.style.backgroundColor).toBe(
54
+ 'var(--str-chat__own-message-bubble-background-color, #1e2330)'
55
+ )
56
+
57
+ rerender(<MessageBubble.Received text="theirs" />)
58
+ const received = screen.getByText('theirs').parentElement as HTMLElement
59
+ expect(received.style.backgroundColor).toBe(
60
+ 'var(--str-chat__message-bubble-background-color, #f1f0ee)'
61
+ )
62
+ })
63
+
64
+ it('forwards className onto the bubble box', () => {
65
+ renderWithProviders(
66
+ <MessageBubble.Sent text="wide" className="max-w-[520px]" />
67
+ )
68
+ const bubble = screen.getByText('wide').parentElement as HTMLElement
69
+ expect(bubble.className).toContain('max-w-[520px]')
70
+ })
71
+ })
@@ -0,0 +1,27 @@
1
+ import React from 'react'
2
+
3
+ import type { ReceivedBubbleProps } from '../../types'
4
+ import BubbleShell from '../_shared/BubbleShell'
5
+
6
+ /**
7
+ * A message bubble in the recipient's treatment — light surface, left-side
8
+ * tail, with an optional header (e.g. sender name). Use to preview how a
9
+ * message looks *to the visitor* — including a preview of your own outgoing
10
+ * message as the recipient will receive it (e.g. a bulk-reply confirmation).
11
+ */
12
+ const ReceivedCard: React.FC<ReceivedBubbleProps> = ({
13
+ text,
14
+ tail,
15
+ header,
16
+ className,
17
+ }) => (
18
+ <BubbleShell
19
+ variant="received"
20
+ text={text}
21
+ tail={tail}
22
+ header={header}
23
+ className={className}
24
+ />
25
+ )
26
+
27
+ export default ReceivedCard
@@ -0,0 +1 @@
1
+ export { default } from './Card'
@@ -0,0 +1,23 @@
1
+ import React from 'react'
2
+
3
+ import type { SentBubbleProps } from '../../types'
4
+ import BubbleShell from '../_shared/BubbleShell'
5
+
6
+ /**
7
+ * A message bubble in the sender's own treatment — dark surface, right-side
8
+ * tail. Use to preview how a message looks *to you in the channel*.
9
+ *
10
+ * Note: the variant is a visual treatment, not an authorship claim. To preview
11
+ * an outgoing message as the *recipient* will see it, use
12
+ * `MessageBubble.Received` instead.
13
+ */
14
+ const SentCard: React.FC<SentBubbleProps> = ({ text, tail, className }) => (
15
+ <BubbleShell
16
+ variant="sent"
17
+ text={text}
18
+ tail={tail}
19
+ className={className}
20
+ />
21
+ )
22
+
23
+ export default SentCard
@@ -0,0 +1 @@
1
+ export { default } from './Card'
@@ -0,0 +1,77 @@
1
+ import classNames from 'classnames'
2
+ import React from 'react'
3
+
4
+ import type { MessageBubbleVariant } from '../../types'
5
+
6
+ import { BubbleTail } from './bubbleTail'
7
+
8
+ export interface BubbleShellProps {
9
+ variant: MessageBubbleVariant
10
+ text: string
11
+ /** Draw the bubble tail. Defaults to `true`. */
12
+ tail?: boolean
13
+ /** Optional header rendered above the text (e.g. sender name). */
14
+ header?: React.ReactNode
15
+ className?: string
16
+ }
17
+
18
+ /**
19
+ * Surface colours and metrics are pulled from the toolkit's Stream theme vars
20
+ * (see `styles.css`) so a standalone bubble is pixel-identical to the
21
+ * in-channel one and re-themes from the same single source of truth.
22
+ */
23
+ const SURFACE_STYLE_BY_VARIANT: Record<
24
+ MessageBubbleVariant,
25
+ React.CSSProperties
26
+ > = {
27
+ sent: {
28
+ backgroundColor:
29
+ 'var(--str-chat__own-message-bubble-background-color, #1e2330)',
30
+ color: 'var(--str-chat__own-message-bubble-color, #fff)',
31
+ },
32
+ received: {
33
+ backgroundColor:
34
+ 'var(--str-chat__message-bubble-background-color, #f1f0ee)',
35
+ color: 'var(--str-chat__message-bubble-color, #000000)',
36
+ },
37
+ }
38
+
39
+ /**
40
+ * The rounded bubble box: theme-driven surface, channel-matched typography
41
+ * (`0.875rem / 1.3125rem`, `0.14px` tracking, `0.75rem 1rem` padding), an
42
+ * optional header slot, and an optional tail.
43
+ *
44
+ * `isolation: isolate` forms a stacking context so the `z-index: -1` tail
45
+ * paints behind the bubble's own opaque, same-colour background but in front
46
+ * of the surrounding page — matching the in-channel `::after` tail.
47
+ */
48
+ const BubbleShell: React.FC<BubbleShellProps> = ({
49
+ variant,
50
+ text,
51
+ tail = true,
52
+ header,
53
+ className,
54
+ }) => (
55
+ <div
56
+ className={classNames(
57
+ 'relative w-fit max-w-full px-4 py-3',
58
+ 'text-[0.875rem] leading-[1.3125rem] tracking-[0.14px]',
59
+ className
60
+ )}
61
+ style={{
62
+ ...SURFACE_STYLE_BY_VARIANT[variant],
63
+ borderRadius: 'var(--str-chat__message-bubble-border-radius, 1.5rem)',
64
+ isolation: 'isolate',
65
+ }}
66
+ >
67
+ {header != null && (
68
+ <div className="mb-1 font-medium" data-testid="message-bubble-header">
69
+ {header}
70
+ </div>
71
+ )}
72
+ <p className="m-0 whitespace-pre-wrap break-words">{text}</p>
73
+ {tail && <BubbleTail variant={variant} />}
74
+ </div>
75
+ )
76
+
77
+ export default BubbleShell
@@ -0,0 +1,48 @@
1
+ import React from 'react'
2
+
3
+ import type { MessageBubbleVariant } from '../../types'
4
+
5
+ /**
6
+ * Canonical chat-bubble tail (20×23). Kept identical to the in-channel tail
7
+ * drawn by `CustomMessage` in `styles.css` so standalone previews and real
8
+ * messages share one silhouette — the single source of truth for the shape.
9
+ *
10
+ * The path points to the bottom-*right*; the `received` variant mirrors it
11
+ * with `scaleX(-1)`, matching the in-channel convention.
12
+ */
13
+ const TAIL_MASK =
14
+ "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='23' fill='none'%3E%3Cpath fill='black' d='M0 0H15C15 10.1934 15.859 15.2345 19.168 21.4893C19.527 22.168 19.039 22.9917 18.274 22.9178C5.176 21.6506 0.32 6.0737 0 0Z'/%3E%3C/svg%3E\") no-repeat center / 100% 100%"
15
+
16
+ /**
17
+ * Bubble surface colours, reused from the toolkit's Stream theme vars so the
18
+ * tail is always tinted the same colour as its bubble. Those vars are declared
19
+ * on `.str-chat`, so we supply the canonical values as `var()` fallbacks —
20
+ * that keeps the tail correct when rendered *outside* a channel (e.g. a
21
+ * standalone preview) while still honouring the var (or a consumer override)
22
+ * wherever it's defined.
23
+ */
24
+ const BG_VAR_BY_VARIANT: Record<MessageBubbleVariant, string> = {
25
+ sent: 'var(--str-chat__own-message-bubble-background-color, #1e2330)',
26
+ received: 'var(--str-chat__message-bubble-background-color, #f1f0ee)',
27
+ }
28
+
29
+ /**
30
+ * The tail element. Absolutely positioned at the bubble's bottom corner and
31
+ * painted behind the bubble body (`z-index: -1`), so the parent bubble must be
32
+ * `position: relative` with `isolation: isolate` (see {@link BubbleShell}).
33
+ */
34
+ export const BubbleTail = ({ variant }: { variant: MessageBubbleVariant }) => (
35
+ <span
36
+ aria-hidden="true"
37
+ className="pointer-events-none absolute bottom-0 z-[-1] h-[1.4375rem] w-5"
38
+ style={{
39
+ insetInlineEnd: variant === 'sent' ? 'calc(-1 * 0.25rem)' : undefined,
40
+ insetInlineStart: variant === 'received' ? 'calc(-1 * 0.25rem)' : undefined,
41
+ transform: variant === 'received' ? 'scaleX(-1)' : undefined,
42
+ transformOrigin: 'center',
43
+ backgroundColor: BG_VAR_BY_VARIANT[variant],
44
+ WebkitMask: TAIL_MASK,
45
+ mask: TAIL_MASK,
46
+ }}
47
+ />
48
+ )
@@ -0,0 +1,31 @@
1
+ import ReceivedCard from './components/Received/Card'
2
+ import SentCard from './components/Sent/Card'
3
+
4
+ /**
5
+ * A dumb, context-free chat message bubble for rendering a single message row
6
+ * *outside* a live channel — previews, digests, confirmation dialogs, the
7
+ * Stacks "responded" summary, etc. Unlike `ChannelView`, it needs no Stream
8
+ * client or provider.
9
+ *
10
+ * Render `MessageBubble.Sent` for the sender's own treatment (dark, right
11
+ * tail) and `MessageBubble.Received` for the recipient's (light, left tail,
12
+ * optional header). Surface colours, radius, typography, and the tail
13
+ * silhouette are shared with the in-channel bubble drawn by `CustomMessage`,
14
+ * so previews stay pixel-identical and re-theme from one source of truth.
15
+ *
16
+ * The variant is a **visual treatment, not an authorship claim**: to preview
17
+ * an outgoing message as the recipient will see it, render
18
+ * `MessageBubble.Received` with your own text.
19
+ */
20
+ const MessageBubble = {
21
+ Sent: SentCard,
22
+ Received: ReceivedCard,
23
+ }
24
+
25
+ export default MessageBubble
26
+ export type {
27
+ MessageBubbleBaseProps,
28
+ MessageBubbleVariant,
29
+ SentBubbleProps,
30
+ ReceivedBubbleProps,
31
+ } from './types'
@@ -0,0 +1,34 @@
1
+ import type React from 'react'
2
+
3
+ /**
4
+ * Which visual treatment the bubble paints. This is decoupled from message
5
+ * *authorship*: it selects a side + colour, not who wrote the text. To preview
6
+ * an outgoing message as the recipient will see it (e.g. a bulk-reply
7
+ * confirmation), render the `received` treatment with your own text.
8
+ */
9
+ export type MessageBubbleVariant = 'sent' | 'received'
10
+
11
+ export interface MessageBubbleBaseProps {
12
+ /**
13
+ * Message body. Rendered verbatim with `whitespace-pre-wrap` (newlines
14
+ * preserved); this is a dumb component, so no markdown / link parsing.
15
+ */
16
+ text: string
17
+ /**
18
+ * Draw the bubble tail at the bottom corner. `sent` tails point
19
+ * bottom-right, `received` bottom-left. Defaults to `true`.
20
+ */
21
+ tail?: boolean
22
+ /** Extra classes on the outer bubble (e.g. `max-w-[520px]`). */
23
+ className?: string
24
+ }
25
+
26
+ export interface SentBubbleProps extends MessageBubbleBaseProps {}
27
+
28
+ export interface ReceivedBubbleProps extends MessageBubbleBaseProps {
29
+ /**
30
+ * Optional header rendered above the text inside the bubble — used by the
31
+ * Stacks message list to show the sender's name. Omit for a plain bubble.
32
+ */
33
+ header?: React.ReactNode
34
+ }
package/src/index.ts CHANGED
@@ -10,6 +10,7 @@ export { default as LockedAttachment } from './components/LockedAttachment'
10
10
  export { default as LinkAttachment } from './components/LinkAttachment'
11
11
  export { default as MessageAttachment } from './components/MessageAttachment'
12
12
  export { Avatar } from './components/Avatar'
13
+ export { default as MessageBubble } from './components/MessageBubble'
13
14
  export { FaqList } from './components/FaqList'
14
15
  export { FaqListItem } from './components/FaqList/FaqListItem'
15
16
  export { ChannelEmptyState } from './components/MessagingShell/ChannelEmptyState'
@@ -67,6 +68,12 @@ export type {
67
68
  OfficialCtaAttachment,
68
69
  } from './stream-custom-data'
69
70
  export type { AvatarProps } from './components/Avatar'
71
+ export type {
72
+ MessageBubbleBaseProps,
73
+ MessageBubbleVariant,
74
+ SentBubbleProps,
75
+ ReceivedBubbleProps,
76
+ } from './components/MessageBubble'
70
77
  export type { ActionButtonProps } from './components/ActionButton'
71
78
  export type {
72
79
  ComposerCardProps,
package/src/styles.css CHANGED
@@ -8,7 +8,7 @@
8
8
  --str-chat__font-family: inherit;
9
9
 
10
10
  --str-chat__message-bubble-border-radius: 1.5rem;
11
- --str-chat__message-bubble-background-color: #f2f1ef;
11
+ --str-chat__message-bubble-background-color: #f1f0ee;
12
12
  --str-chat__message-bubble-color: #000000;
13
13
 
14
14
  --str-chat__own-message-bubble-background-color: #1e2330;