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

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-1787614546",
3
+ "version": "4.4.1",
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-1787614546",
54
- "@linktr.ee/messaging-taxonomy": "0.11.0-rc-1787614546",
53
+ "@linktr.ee/messaging-core": "^2.6.0",
54
+ "@linktr.ee/messaging-taxonomy": "^0.11.0",
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="str-chat min-h-screen w-full space-y-6 bg-[#F9F7F4] p-12">
13
+ <div className="min-h-screen w-full space-y-6 bg-[#F9F7F4] p-12">
14
14
  {children}
15
15
  </div>
16
16
  )
@@ -26,14 +26,10 @@ const Row = ({
26
26
  }) => (
27
27
  <div>
28
28
  <p className="mb-2 text-xs font-medium text-black/40">{label}</p>
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>
29
+ <div
30
+ className={`flex ${align === 'end' ? 'justify-end' : 'justify-start'}`}
31
+ >
32
+ {children}
37
33
  </div>
38
34
  </div>
39
35
  )
@@ -22,15 +22,12 @@ describe('MessageBubble', () => {
22
22
  const { rerender } = renderWithProviders(
23
23
  <MessageBubble.Sent text="tailed" />
24
24
  )
25
- const bubble = screen
26
- .getByText('tailed')
27
- .closest('.str-chat__message-bubble') as HTMLElement
25
+ // The bubble box wraps the text; its last child is the tail <span>.
26
+ const bubble = screen.getByText('tailed').parentElement as HTMLElement
28
27
  expect(bubble.querySelector('span[aria-hidden="true"]')).not.toBeNull()
29
28
 
30
29
  rerender(<MessageBubble.Sent text="tailed" tail={false} />)
31
- const bubbleNoTail = screen
32
- .getByText('tailed')
33
- .closest('.str-chat__message-bubble') as HTMLElement
30
+ const bubbleNoTail = screen.getByText('tailed').parentElement as HTMLElement
34
31
  expect(bubbleNoTail.querySelector('span[aria-hidden="true"]')).toBeNull()
35
32
  })
36
33
 
@@ -50,17 +47,13 @@ describe('MessageBubble', () => {
50
47
 
51
48
  it('applies the sent and received surface treatments from theme vars', () => {
52
49
  const { rerender } = renderWithProviders(<MessageBubble.Sent text="mine" />)
53
- const sent = screen
54
- .getByText('mine')
55
- .closest('.str-chat__message-bubble') as HTMLElement
50
+ const sent = screen.getByText('mine').parentElement as HTMLElement
56
51
  expect(sent.style.backgroundColor).toBe(
57
52
  'var(--str-chat__own-message-bubble-background-color, #1e2330)'
58
53
  )
59
54
 
60
55
  rerender(<MessageBubble.Received text="theirs" />)
61
- const received = screen
62
- .getByText('theirs')
63
- .closest('.str-chat__message-bubble') as HTMLElement
56
+ const received = screen.getByText('theirs').parentElement as HTMLElement
64
57
  expect(received.style.backgroundColor).toBe(
65
58
  'var(--str-chat__message-bubble-background-color, #f1f0ee)'
66
59
  )
@@ -68,9 +61,7 @@ describe('MessageBubble', () => {
68
61
 
69
62
  it('masks message content from session replay by default', () => {
70
63
  renderWithProviders(<MessageBubble.Received header="Brie" text="secret" />)
71
- const bubble = screen
72
- .getByText('secret')
73
- .closest('.str-chat__message-bubble') as HTMLElement
64
+ const bubble = screen.getByText('secret').parentElement as HTMLElement
74
65
  expect(bubble.getAttribute('data-dd-privacy')).toBe('mask')
75
66
  })
76
67
 
@@ -78,9 +69,7 @@ describe('MessageBubble', () => {
78
69
  renderWithProviders(
79
70
  <MessageBubble.Sent text="wide" className="w-fit max-w-[520px]" />
80
71
  )
81
- const bubble = screen
82
- .getByText('wide')
83
- .closest('.str-chat__message-bubble') as HTMLElement
72
+ const bubble = screen.getByText('wide').parentElement as HTMLElement
84
73
  expect(bubble.classList.contains('w-fit')).toBe(true)
85
74
  expect(bubble.className).toContain('max-w-[520px]')
86
75
  })
@@ -67,8 +67,7 @@ const BubbleShell: React.FC<BubbleShellProps> = ({
67
67
  <div
68
68
  data-dd-privacy="mask"
69
69
  className={classNames(
70
- 'str-chat__message-bubble',
71
- 'relative',
70
+ 'relative px-4 py-3',
72
71
  'text-[0.875rem] leading-[1.3125rem] tracking-[0.14px]',
73
72
  className
74
73
  )}
@@ -78,17 +77,13 @@ const BubbleShell: React.FC<BubbleShellProps> = ({
78
77
  isolation: 'isolate',
79
78
  }}
80
79
  >
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>
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} />}
92
87
  </div>
93
88
  )
94
89
 
@@ -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.3125rem)' : undefined,
39
+ insetInlineEnd: variant === 'sent' ? 'calc(-1 * 0.25rem)' : undefined,
40
40
  insetInlineStart:
41
- variant === 'received' ? 'calc(-1 * 0.3125rem)' : undefined,
41
+ variant === 'received' ? 'calc(-1 * 0.25rem)' : undefined,
42
42
  transform: variant === 'received' ? 'scaleX(-1)' : undefined,
43
43
  transformOrigin: 'center',
44
44
  backgroundColor: BG_VAR_BY_VARIANT[variant],
@@ -95,13 +95,6 @@ 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
- }
105
98
 
106
99
  const bothSides = (props: RichCardBaseProps) => (
107
100
  <>
@@ -120,8 +113,7 @@ const bothSides = (props: RichCardBaseProps) => (
120
113
  * The three real surfaces that converge on `linktree_rich_card`, each shown
121
114
  * Received (light) and Sent (dark). They differ only by knobs: the official
122
115
  * welcome card is `hero + primary`; a single-link broadcast is `hero +
123
- * secondary`; a collection is `fan + secondary`. A fourth row shows a
124
- * newline-separated subtitle (the onboarding result-card shape).
116
+ * secondary`; a collection is `fan + secondary`.
125
117
  */
126
118
  export const Instances: StoryFn = () => (
127
119
  <Table>
@@ -139,10 +131,6 @@ export const Instances: StoryFn = () => (
139
131
  <RowLabel>Collection โ€” fan ยท secondary</RowLabel>
140
132
  {bothSides(COLLECTION)}
141
133
  </tr>
142
- <tr>
143
- <RowLabel>Subtitle with newlines</RowLabel>
144
- {bothSides(NEWLINE_SUBTITLE)}
145
- </tr>
146
134
  </tbody>
147
135
  </Table>
148
136
  )
@@ -41,26 +41,6 @@ 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
- )
64
44
  })
65
45
 
66
46
  it('renders a single hero image for the hero presentation', async () => {
@@ -18,27 +18,17 @@ export interface RichCardBodyProps {
18
18
  actions?: RichCardActionButton[]
19
19
  }
20
20
 
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.
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.
23
28
  const TITLE_CLASS =
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
- ))
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]'
42
32
 
43
33
  /**
44
34
  * The action footer. `layout` (chosen by the first action's `variant` in
@@ -66,7 +56,7 @@ const RichCardFooter: React.FC<{
66
56
  // card's width; the button truncates within it and the copy yields
67
57
  // first. `stacked` keeps the full-width buttons-below layout.
68
58
  stacked
69
- ? 'mt-4 flex-col'
59
+ ? 'mt-3 flex-col'
70
60
  : 'min-w-0 max-w-[60%] flex-row flex-wrap justify-end'
71
61
  )}
72
62
  >
@@ -121,7 +111,7 @@ const RichCardBody: React.FC<RichCardBodyProps> = ({
121
111
  hasActions && (actions?.[0]?.variant ?? 'primary') === 'secondary'
122
112
 
123
113
  const textBlock = (
124
- <div className="flex min-w-0 flex-1 flex-col gap-4">
114
+ <div className="flex min-w-0 flex-1 flex-col gap-[2px]">
125
115
  {hasTitle && (
126
116
  <div className="flex min-w-0 items-center gap-2">
127
117
  {appIcon ? <span className="shrink-0">{appIcon}</span> : null}
@@ -138,11 +128,12 @@ const RichCardBody: React.FC<RichCardBodyProps> = ({
138
128
  <p
139
129
  className={classNames(
140
130
  SUBTITLE_CLASS,
131
+ subtitleLines > 1 && 'whitespace-pre-line',
141
132
  subtitleLines <= 1 && 'truncate'
142
133
  )}
143
134
  style={subtitleStyle}
144
135
  >
145
- {subtitleLines > 1 ? withLineBreaks(subtitle ?? '') : subtitle}
136
+ {subtitle}
146
137
  </p>
147
138
  )}
148
139
  </div>