@linktr.ee/messaging-react 4.4.1-rc-1787614892 → 4.4.1-rc-1787616779

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linktr.ee/messaging-react",
3
- "version": "4.4.1-rc-1787614892",
3
+ "version": "4.4.1-rc-1787616779",
4
4
  "description": "React messaging components built on messaging-core for web applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -50,8 +50,8 @@
50
50
  "chromatic:local": "yarn storybook:build && chromatic"
51
51
  },
52
52
  "dependencies": {
53
- "@linktr.ee/messaging-core": "2.6.0-rc-1787614892",
54
- "@linktr.ee/messaging-taxonomy": "0.11.0-rc-1787614892",
53
+ "@linktr.ee/messaging-core": "2.6.0-rc-1787616779",
54
+ "@linktr.ee/messaging-taxonomy": "0.11.0-rc-1787616779",
55
55
  "@phosphor-icons/react": "^2.1.10"
56
56
  },
57
57
  "devDependencies": {
@@ -10,7 +10,7 @@ const meta: Meta = {
10
10
  export default meta
11
11
 
12
12
  const Canvas = ({ children }: { children: React.ReactNode }) => (
13
- <div className="min-h-screen w-full space-y-6 bg-[#F9F7F4] p-12">
13
+ <div className="str-chat min-h-screen w-full space-y-6 bg-[#F9F7F4] p-12">
14
14
  {children}
15
15
  </div>
16
16
  )
@@ -27,7 +27,9 @@ const Row = ({
27
27
  <div>
28
28
  <p className="mb-2 text-xs font-medium text-black/40">{label}</p>
29
29
  <div
30
- className={`flex ${align === 'end' ? 'justify-end' : 'justify-start'}`}
30
+ className={`str-chat__li str-chat__li--single ${
31
+ align === 'end' ? 'flex justify-end' : 'flex justify-start'
32
+ }`}
31
33
  >
32
34
  {children}
33
35
  </div>
@@ -43,29 +45,25 @@ const MULTILINE = 'Line one\nLine two\nLine three'
43
45
  export const Gallery: StoryFn = () => (
44
46
  <Canvas>
45
47
  <Row label="Received — short" align="start">
46
- <MessageBubble.Received text={SHORT} className="w-fit" />
48
+ <MessageBubble.Received text={SHORT} />
47
49
  </Row>
48
50
  <Row label="Received — with header (sender name)" align="start">
49
- <MessageBubble.Received
50
- header="Brie"
51
- text={LONG}
52
- className="w-fit max-w-[520px]"
53
- />
51
+ <MessageBubble.Received header="Brie" text={LONG} />
54
52
  </Row>
55
53
  <Row label="Received — no tail" align="start">
56
- <MessageBubble.Received text={SHORT} tail={false} className="w-fit" />
54
+ <MessageBubble.Received text={SHORT} tail={false} />
57
55
  </Row>
58
56
  <Row label="Sent — short" align="end">
59
- <MessageBubble.Sent text={SHORT} className="w-fit" />
57
+ <MessageBubble.Sent text={SHORT} />
60
58
  </Row>
61
59
  <Row label="Sent — long" align="end">
62
- <MessageBubble.Sent text={LONG} className="w-fit max-w-[520px]" />
60
+ <MessageBubble.Sent text={LONG} />
63
61
  </Row>
64
62
  <Row label="Sent — multiline" align="end">
65
- <MessageBubble.Sent text={MULTILINE} className="w-fit" />
63
+ <MessageBubble.Sent text={MULTILINE} />
66
64
  </Row>
67
65
  <Row label="Sent — no tail" align="end">
68
- <MessageBubble.Sent text={SHORT} tail={false} className="w-fit" />
66
+ <MessageBubble.Sent text={SHORT} tail={false} />
69
67
  </Row>
70
68
  </Canvas>
71
69
  )
@@ -78,10 +76,7 @@ export const Gallery: StoryFn = () => (
78
76
  export const PreviewToRecipient: StoryFn = () => (
79
77
  <Canvas>
80
78
  <Row label="How your reply will look to the visitor" align="start">
81
- <MessageBubble.Received
82
- text="Thanks so much for the support — I just dropped a new track, hope you love it! 🎶"
83
- className="w-fit max-w-[520px]"
84
- />
79
+ <MessageBubble.Received text="Thanks so much for the support — I just dropped a new track, hope you love it! 🎶" />
85
80
  </Row>
86
81
  </Canvas>
87
82
  )
@@ -93,10 +88,7 @@ export const PreviewToRecipient: StoryFn = () => (
93
88
  export const PreviewInChannel: StoryFn = () => (
94
89
  <Canvas>
95
90
  <Row label="How your reply looks to you in the channel" align="end">
96
- <MessageBubble.Sent
97
- text="Thanks so much for the support — I just dropped a new track, hope you love it! 🎶"
98
- className="w-fit max-w-[520px]"
99
- />
91
+ <MessageBubble.Sent text="Thanks so much for the support — I just dropped a new track, hope you love it! 🎶" />
100
92
  </Row>
101
93
  </Canvas>
102
94
  )
@@ -22,12 +22,15 @@ describe('MessageBubble', () => {
22
22
  const { rerender } = renderWithProviders(
23
23
  <MessageBubble.Sent text="tailed" />
24
24
  )
25
- // The bubble box wraps the text; its last child is the tail <span>.
26
- const bubble = screen.getByText('tailed').parentElement as HTMLElement
25
+ const bubble = screen
26
+ .getByText('tailed')
27
+ .closest('.str-chat__message-bubble') as HTMLElement
27
28
  expect(bubble.querySelector('span[aria-hidden="true"]')).not.toBeNull()
28
29
 
29
30
  rerender(<MessageBubble.Sent text="tailed" tail={false} />)
30
- const bubbleNoTail = screen.getByText('tailed').parentElement as HTMLElement
31
+ const bubbleNoTail = screen
32
+ .getByText('tailed')
33
+ .closest('.str-chat__message-bubble') as HTMLElement
31
34
  expect(bubbleNoTail.querySelector('span[aria-hidden="true"]')).toBeNull()
32
35
  })
33
36
 
@@ -47,13 +50,17 @@ describe('MessageBubble', () => {
47
50
 
48
51
  it('applies the sent and received surface treatments from theme vars', () => {
49
52
  const { rerender } = renderWithProviders(<MessageBubble.Sent text="mine" />)
50
- const sent = screen.getByText('mine').parentElement as HTMLElement
53
+ const sent = screen
54
+ .getByText('mine')
55
+ .closest('.str-chat__message-bubble') as HTMLElement
51
56
  expect(sent.style.backgroundColor).toBe(
52
57
  'var(--str-chat__own-message-bubble-background-color, #1e2330)'
53
58
  )
54
59
 
55
60
  rerender(<MessageBubble.Received text="theirs" />)
56
- const received = screen.getByText('theirs').parentElement as HTMLElement
61
+ const received = screen
62
+ .getByText('theirs')
63
+ .closest('.str-chat__message-bubble') as HTMLElement
57
64
  expect(received.style.backgroundColor).toBe(
58
65
  'var(--str-chat__message-bubble-background-color, #f1f0ee)'
59
66
  )
@@ -61,16 +68,20 @@ describe('MessageBubble', () => {
61
68
 
62
69
  it('masks message content from session replay by default', () => {
63
70
  renderWithProviders(<MessageBubble.Received header="Brie" text="secret" />)
64
- const bubble = screen.getByText('secret').parentElement as HTMLElement
71
+ const bubble = screen
72
+ .getByText('secret')
73
+ .closest('.str-chat__message-bubble') as HTMLElement
65
74
  expect(bubble.getAttribute('data-dd-privacy')).toBe('mask')
66
75
  })
67
76
 
68
- it('forwards width/className onto the bubble box (width is caller-controlled)', () => {
77
+ it('forwards width/className onto the outermost message wrapper', () => {
69
78
  renderWithProviders(
70
79
  <MessageBubble.Sent text="wide" className="w-fit max-w-[520px]" />
71
80
  )
72
- const bubble = screen.getByText('wide').parentElement as HTMLElement
73
- expect(bubble.classList.contains('w-fit')).toBe(true)
74
- expect(bubble.className).toContain('max-w-[520px]')
81
+ const message = screen
82
+ .getByText('wide')
83
+ .closest('.str-chat__message') as HTMLElement
84
+ expect(message.classList.contains('w-fit')).toBe(true)
85
+ expect(message.className).toContain('max-w-[520px]')
75
86
  })
76
87
  })
@@ -15,8 +15,8 @@ export interface BubbleShellProps {
15
15
  /**
16
16
  * Width is caller-controlled — pass a width class in `className`
17
17
  * (`w-fit` to hug the text for chat/preview bubbles, `w-full` to fill the
18
- * container for card-like list rows). With no width class the bubble is a
19
- * block element and fills its container.
18
+ * container for card-like list rows). Applied to the outermost Stream
19
+ * message wrapper. With no width class the bubble fills its container.
20
20
  */
21
21
  className?: string
22
22
  }
@@ -65,25 +65,42 @@ const BubbleShell: React.FC<BubbleShellProps> = ({
65
65
  className,
66
66
  }) => (
67
67
  <div
68
- data-dd-privacy="mask"
69
68
  className={classNames(
70
- 'relative px-4 py-3',
71
- 'text-[0.875rem] leading-[1.3125rem] tracking-[0.14px]',
69
+ 'str-chat__message',
70
+ variant === 'sent' ? 'str-chat__message--me' : 'str-chat__message--other',
72
71
  className
73
72
  )}
74
- style={{
75
- ...SURFACE_STYLE_BY_VARIANT[variant],
76
- borderRadius: 'var(--str-chat__message-bubble-border-radius, 1.5rem)',
77
- isolation: 'isolate',
78
- }}
79
73
  >
80
- {header != null && (
81
- <div className="mb-1 font-medium" data-testid="message-bubble-header">
82
- {header}
74
+ <div className="str-chat__message-inner">
75
+ <div
76
+ data-dd-privacy="mask"
77
+ className={classNames(
78
+ 'str-chat__message-bubble',
79
+ 'relative',
80
+ 'text-[0.875rem] leading-[1.3125rem] tracking-[0.14px]'
81
+ )}
82
+ style={{
83
+ ...SURFACE_STYLE_BY_VARIANT[variant],
84
+ borderRadius: 'var(--str-chat__message-bubble-border-radius, 1.5rem)',
85
+ isolation: 'isolate',
86
+ }}
87
+ >
88
+ <div className="str-chat__message-text">
89
+ {header != null && (
90
+ <div
91
+ className="mb-1 font-medium"
92
+ data-testid="message-bubble-header"
93
+ >
94
+ {header}
95
+ </div>
96
+ )}
97
+ <p className="m-0 whitespace-pre-wrap break-words hyphens-manual">
98
+ {text}
99
+ </p>
100
+ {tail && <BubbleTail variant={variant} />}
101
+ </div>
83
102
  </div>
84
- )}
85
- <p className="m-0 whitespace-pre-wrap break-words hyphens-manual">{text}</p>
86
- {tail && <BubbleTail variant={variant} />}
103
+ </div>
87
104
  </div>
88
105
  )
89
106
 
@@ -36,9 +36,9 @@ export const BubbleTail = ({ variant }: { variant: MessageBubbleVariant }) => (
36
36
  aria-hidden="true"
37
37
  className="pointer-events-none absolute bottom-0 z-[-1] h-[1.4375rem] w-5"
38
38
  style={{
39
- insetInlineEnd: variant === 'sent' ? 'calc(-1 * 0.25rem)' : undefined,
39
+ insetInlineEnd: variant === 'sent' ? 'calc(-1 * 0.3125rem)' : undefined,
40
40
  insetInlineStart:
41
- variant === 'received' ? 'calc(-1 * 0.25rem)' : undefined,
41
+ variant === 'received' ? 'calc(-1 * 0.3125rem)' : undefined,
42
42
  transform: variant === 'received' ? 'scaleX(-1)' : undefined,
43
43
  transformOrigin: 'center',
44
44
  backgroundColor: BG_VAR_BY_VARIANT[variant],
@@ -20,10 +20,10 @@ export interface MessageBubbleBaseProps {
20
20
  */
21
21
  tail?: boolean
22
22
  /**
23
- * Extra classes on the outer bubble. Width is caller-controlled: pass
24
- * `w-fit` to hug the text (chat/preview) or `w-full` to fill the container
25
- * (card-like list rows). With no width class the bubble fills its
26
- * container. Also where you'd set `max-w-[…]`.
23
+ * Extra classes on the outermost Stream message wrapper. Width is
24
+ * caller-controlled: pass `w-fit` to hug the text (chat/preview) or
25
+ * `w-full` to fill the container (card-like list rows). With no width
26
+ * class the bubble fills its container. Also where you'd set `max-w-[…]`.
27
27
  */
28
28
  className?: string
29
29
  }
@@ -95,6 +95,13 @@ const COLLECTION: RichCardBaseProps = {
95
95
  imagePresentation: 'fan',
96
96
  actions: [secondaryLink('View collection', 'tr.ee/briesummer')],
97
97
  }
98
+ const NEWLINE_SUBTITLE: RichCardBaseProps = {
99
+ ...OFFICIAL_WELCOME,
100
+ title: 'Start every conversation with a personal welcome message!',
101
+ subtitle:
102
+ '👋 Say hello as people arrive\n✨ Spark conversations\n💬 Add automated FAQs',
103
+ subtitleLines: 6,
104
+ }
98
105
 
99
106
  const bothSides = (props: RichCardBaseProps) => (
100
107
  <>
@@ -113,7 +120,8 @@ const bothSides = (props: RichCardBaseProps) => (
113
120
  * The three real surfaces that converge on `linktree_rich_card`, each shown
114
121
  * Received (light) and Sent (dark). They differ only by knobs: the official
115
122
  * welcome card is `hero + primary`; a single-link broadcast is `hero +
116
- * secondary`; a collection is `fan + secondary`.
123
+ * secondary`; a collection is `fan + secondary`. A fourth row shows a
124
+ * newline-separated subtitle (the onboarding result-card shape).
117
125
  */
118
126
  export const Instances: StoryFn = () => (
119
127
  <Table>
@@ -131,6 +139,10 @@ export const Instances: StoryFn = () => (
131
139
  <RowLabel>Collection — fan · secondary</RowLabel>
132
140
  {bothSides(COLLECTION)}
133
141
  </tr>
142
+ <tr>
143
+ <RowLabel>Subtitle with newlines</RowLabel>
144
+ {bothSides(NEWLINE_SUBTITLE)}
145
+ </tr>
134
146
  </tbody>
135
147
  </Table>
136
148
  )
@@ -41,6 +41,26 @@ describe('RichCard', () => {
41
41
  expect(card).toHaveTextContent('👋 Say hello as people arrive')
42
42
  expect(card).toHaveTextContent('✨ Spark conversations')
43
43
  expect(card).toHaveTextContent('💬 Add automated FAQs')
44
+ expect(card.querySelectorAll('br')).toHaveLength(2)
45
+ })
46
+
47
+ it('does not insert line breaks when the subtitle is clamped to one line', async () => {
48
+ renderWithProviders(
49
+ <RichCard.Received
50
+ title="Welcome"
51
+ subtitle={
52
+ '👋 Say hello as people arrive\n✨ Spark conversations\n💬 Add automated FAQs'
53
+ }
54
+ subtitleLines={1}
55
+ images={[HERO]}
56
+ />
57
+ )
58
+
59
+ const card = await screen.findByTestId('rich-card')
60
+ expect(card.querySelectorAll('br')).toHaveLength(0)
61
+ expect(screen.getByText(/Say hello as people arrive/)).toHaveClass(
62
+ 'truncate'
63
+ )
44
64
  })
45
65
 
46
66
  it('renders a single hero image for the hero presentation', async () => {
@@ -18,17 +18,27 @@ export interface RichCardBodyProps {
18
18
  actions?: RichCardActionButton[]
19
19
  }
20
20
 
21
- // Mirror LinkAttachment's `CardBody` type scales so the two cards read
22
- // identically (14/20 medium title, 12/16 subtitle). Colour is always supplied
23
- // via `chinTextColor`, so no per-variant text class is needed here.
24
- // `[text-wrap:balance]` evens the title's line lengths so a two-line title
25
- // doesn't drop a single word onto its own line; `[text-wrap:pretty]` does the
26
- // lighter last-line-widow fix for the longer subtitle prose. Arbitrary-property
27
- // form so the utilities generate regardless of the consumer's Tailwind version.
21
+ // body/small from Figma. Title is medium; subtitle is regular. No
22
+ // `text-wrap:balance` — it evened two-line titles by breaking mid-phrase.
28
23
  const TITLE_CLASS =
29
- 'line-clamp-2 text-[14px] font-medium leading-5 tracking-[0.28px] [text-wrap:balance]'
30
- const SUBTITLE_CLASS =
31
- 'text-[12px] leading-4 tracking-[0.24px] [text-wrap:pretty]'
24
+ 'line-clamp-2 text-[14px] font-medium leading-5 tracking-[0.02em]'
25
+ const SUBTITLE_CLASS = 'text-[14px] leading-5 tracking-[0.02em]'
26
+
27
+ /** `<br>` ignores vertical margin; the spacer is the 6px Figma gap. */
28
+ const LineBreak: React.FC = () => (
29
+ <>
30
+ <br />
31
+ <span aria-hidden className="block h-1.5" />
32
+ </>
33
+ )
34
+
35
+ const withLineBreaks = (text: string) =>
36
+ text.split(/\r?\n/).map((line, index, lines) => (
37
+ <React.Fragment key={index}>
38
+ {line}
39
+ {index < lines.length - 1 ? <LineBreak /> : null}
40
+ </React.Fragment>
41
+ ))
32
42
 
33
43
  /**
34
44
  * The action footer. `layout` (chosen by the first action's `variant` in
@@ -56,7 +66,7 @@ const RichCardFooter: React.FC<{
56
66
  // card's width; the button truncates within it and the copy yields
57
67
  // first. `stacked` keeps the full-width buttons-below layout.
58
68
  stacked
59
- ? 'mt-3 flex-col'
69
+ ? 'mt-4 flex-col'
60
70
  : 'min-w-0 max-w-[60%] flex-row flex-wrap justify-end'
61
71
  )}
62
72
  >
@@ -111,7 +121,7 @@ const RichCardBody: React.FC<RichCardBodyProps> = ({
111
121
  hasActions && (actions?.[0]?.variant ?? 'primary') === 'secondary'
112
122
 
113
123
  const textBlock = (
114
- <div className="flex min-w-0 flex-1 flex-col gap-[2px]">
124
+ <div className="flex min-w-0 flex-1 flex-col gap-4">
115
125
  {hasTitle && (
116
126
  <div className="flex min-w-0 items-center gap-2">
117
127
  {appIcon ? <span className="shrink-0">{appIcon}</span> : null}
@@ -128,12 +138,11 @@ const RichCardBody: React.FC<RichCardBodyProps> = ({
128
138
  <p
129
139
  className={classNames(
130
140
  SUBTITLE_CLASS,
131
- subtitleLines > 1 && 'whitespace-pre-line',
132
141
  subtitleLines <= 1 && 'truncate'
133
142
  )}
134
143
  style={subtitleStyle}
135
144
  >
136
- {subtitle}
145
+ {subtitleLines > 1 ? withLineBreaks(subtitle ?? '') : subtitle}
137
146
  </p>
138
147
  )}
139
148
  </div>