@linktr.ee/messaging-react 4.4.0 โ†’ 4.4.1-rc-1787614546

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.0",
3
+ "version": "4.4.1-rc-1787614546",
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.5.0",
54
- "@linktr.ee/messaging-taxonomy": "^0.11.0",
53
+ "@linktr.ee/messaging-core": "2.6.0-rc-1787614546",
54
+ "@linktr.ee/messaging-taxonomy": "0.11.0-rc-1787614546",
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
  )
@@ -26,10 +26,14 @@ const Row = ({
26
26
  }) => (
27
27
  <div>
28
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}
29
+ <div className="str-chat__li str-chat__li--single">
30
+ <div
31
+ className={`str-chat__message ${
32
+ align === 'end' ? 'str-chat__message--me' : 'str-chat__message--other'
33
+ }`}
34
+ >
35
+ <div className="str-chat__message-inner">{children}</div>
36
+ </div>
33
37
  </div>
34
38
  </div>
35
39
  )
@@ -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,7 +68,9 @@ 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
 
@@ -69,7 +78,9 @@ describe('MessageBubble', () => {
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
81
+ const bubble = screen
82
+ .getByText('wide')
83
+ .closest('.str-chat__message-bubble') as HTMLElement
73
84
  expect(bubble.classList.contains('w-fit')).toBe(true)
74
85
  expect(bubble.className).toContain('max-w-[520px]')
75
86
  })
@@ -67,7 +67,8 @@ const BubbleShell: React.FC<BubbleShellProps> = ({
67
67
  <div
68
68
  data-dd-privacy="mask"
69
69
  className={classNames(
70
- 'relative px-4 py-3',
70
+ 'str-chat__message-bubble',
71
+ 'relative',
71
72
  'text-[0.875rem] leading-[1.3125rem] tracking-[0.14px]',
72
73
  className
73
74
  )}
@@ -77,13 +78,17 @@ const BubbleShell: React.FC<BubbleShellProps> = ({
77
78
  isolation: 'isolate',
78
79
  }}
79
80
  >
80
- {header != null && (
81
- <div className="mb-1 font-medium" data-testid="message-bubble-header">
82
- {header}
83
- </div>
84
- )}
85
- <p className="m-0 whitespace-pre-wrap break-words hyphens-manual">{text}</p>
86
- {tail && <BubbleTail variant={variant} />}
81
+ <div className="str-chat__message-text">
82
+ {header != null && (
83
+ <div className="mb-1 font-medium" data-testid="message-bubble-header">
84
+ {header}
85
+ </div>
86
+ )}
87
+ <p className="m-0 whitespace-pre-wrap break-words hyphens-manual">
88
+ {text}
89
+ </p>
90
+ {tail && <BubbleTail variant={variant} />}
91
+ </div>
87
92
  </div>
88
93
  )
89
94
 
@@ -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],
@@ -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>