@linktr.ee/messaging-react 3.36.0 → 3.37.0-rc-1786161722

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/dist/index.cjs CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./index-AQNn2p5e.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.RichCard=e.RichCard;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.optimizeMessagingAttachmentUrl=e.optimizeMessagingAttachmentUrl;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-D7A8U8GQ.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.RichCard=e.RichCard;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.optimizeMessagingAttachmentUrl=e.optimizeMessagingAttachmentUrl;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
@@ -1698,8 +1698,9 @@ export declare interface RichCardActionButton {
1698
1698
  onClick?: () => void;
1699
1699
  /**
1700
1700
  * Button emphasis. The **first** action's variant drives the footer layout:
1701
- * `primary` → a stacked full-width button; `secondary` → an inline compact
1702
- * row. Defaults to `primary`.
1701
+ * `primary` → a stacked full-width button below the copy; `secondary` → a row
1702
+ * in the chin with the copy on the left and the action pinned right. Defaults
1703
+ * to `primary`.
1703
1704
  */
1704
1705
  variant?: 'primary' | 'secondary';
1705
1706
  }
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 h, k as L, l as d, m as p, R as C, n as v, p as A, q as k, s as F, t as b, u as P, v as f, w as x, x as y, o as z, y as B, z as S, B as q, D, E, G as R, H as V, I as w } from "./index-CrdAtOBl.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 c, i as u, j as h, k as L, l as d, m as p, R as C, n as v, p as A, q as k, s as F, t as b, u as P, v as f, w as x, x as y, o as z, y as B, z as S, B as q, D, E, G as R, H as V, I as w } from "./index-DGmVyxHy.js";
2
2
  export {
3
3
  e as ActionButton,
4
4
  t as Avatar,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linktr.ee/messaging-react",
3
- "version": "3.36.0",
3
+ "version": "3.37.0-rc-1786161722",
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
  },
51
51
  "dependencies": {
52
52
  "@linktr.ee/component-library": "11.8.6",
53
- "@linktr.ee/messaging-core": "^2.4.0",
54
- "@linktr.ee/messaging-taxonomy": "^0.8.0",
53
+ "@linktr.ee/messaging-core": "2.4.0-rc-1786161722",
54
+ "@linktr.ee/messaging-taxonomy": "^0.9.0",
55
55
  "@phosphor-icons/react": "^2.1.10"
56
56
  },
57
57
  "devDependencies": {
@@ -58,11 +58,22 @@ const CardCta: React.FC<CardCtaProps> = ({
58
58
  const emphasis: CtaEmphasis = cta.variant ?? 'primary'
59
59
  const className = classNames(
60
60
  'inline-flex h-10 items-center justify-center rounded-full px-4 text-sm font-medium leading-none transition-colors',
61
- fullWidth && 'w-full',
61
+ fullWidth ? 'w-full' : 'min-w-0',
62
62
  !standalone && 'mt-2',
63
63
  BUTTON_CLASS_BY_VARIANT[variant][emphasis]
64
64
  )
65
65
 
66
+ // A content-width button (RichCard's `secondary` row, which sits beside the
67
+ // copy in a fixed-width card) can be squeezed by a long or localized label —
68
+ // truncate to an ellipsis instead of overflowing and getting clipped by the
69
+ // card. The full-width button (LinkAttachment's default) spans its own row
70
+ // and never needs this, so it is left exactly as-is.
71
+ const labelNode = fullWidth ? (
72
+ cta.label
73
+ ) : (
74
+ <span className="min-w-0 truncate">{cta.label}</span>
75
+ )
76
+
66
77
  // Mirror the URL normalization used by the shell anchor so bare
67
78
  // hostnames (e.g. `tr.ee/foo`) open as external links rather than
68
79
  // resolving against the current host.
@@ -82,7 +93,7 @@ const CardCta: React.FC<CardCtaProps> = ({
82
93
  }}
83
94
  className={`${className} no-underline`}
84
95
  >
85
- {cta.label}
96
+ {labelNode}
86
97
  </a>
87
98
  )
88
99
  }
@@ -96,7 +107,7 @@ const CardCta: React.FC<CardCtaProps> = ({
96
107
  }}
97
108
  className={className}
98
109
  >
99
- {cta.label}
110
+ {labelNode}
100
111
  </button>
101
112
  )
102
113
  }
@@ -161,14 +161,16 @@ describe('RichCard', () => {
161
161
  ]}
162
162
  />
163
163
  )
164
- const link = await screen.findByText('Shop now')
165
- expect(link.tagName).toBe('A')
164
+ // Secondary labels render inside a truncating span, so resolve the anchor/
165
+ // button from the label node.
166
+ const link = (await screen.findByText('Shop now')).closest('a')
167
+ expect(link).not.toBeNull()
166
168
  expect(link).toHaveAttribute('href', 'https://tr.ee/x')
167
169
  expect(link).toHaveAttribute('target', '_blank')
168
170
 
169
- const button = screen.getByText('Set up')
170
- expect(button.tagName).toBe('BUTTON')
171
- fireEvent.click(button)
171
+ const button = screen.getByText('Set up').closest('button')
172
+ expect(button).not.toBeNull()
173
+ fireEvent.click(button as HTMLButtonElement)
172
174
  expect(onClick).toHaveBeenCalledTimes(1)
173
175
  })
174
176
 
@@ -191,6 +193,32 @@ describe('RichCard', () => {
191
193
  />
192
194
  )
193
195
  // Secondary → inline, content-width button (no w-full).
194
- expect(screen.getByText('Shop now')).not.toHaveClass('w-full')
196
+ expect(screen.getByText('Shop now').closest('a')).not.toHaveClass('w-full')
197
+ })
198
+
199
+ it('lays a secondary chin out as a row: copy left, action right', async () => {
200
+ const { rerender } = renderWithProviders(
201
+ <RichCard.Received
202
+ title="Test collection"
203
+ subtitle="Collection of 3 links"
204
+ images={[HERO, IMG_2, IMG_3]}
205
+ actions={[{ label: 'Open', href: 'tr.ee/c', variant: 'secondary' }]}
206
+ />
207
+ )
208
+ // The action shares one justify-between row with the title/subtitle copy.
209
+ const link = await screen.findByText('Open')
210
+ const row = link.closest('.justify-between')
211
+ expect(row).not.toBeNull()
212
+ expect(row?.textContent).toContain('Test collection')
213
+
214
+ // Primary stays stacked below the copy — no chin row.
215
+ rerender(
216
+ <RichCard.Received
217
+ title="Welcome"
218
+ images={[HERO]}
219
+ actions={[{ label: 'Set up', variant: 'primary' }]}
220
+ />
221
+ )
222
+ expect(screen.getByText('Set up').closest('.justify-between')).toBeNull()
195
223
  })
196
224
  })
@@ -25,24 +25,33 @@ const TITLE_CLASS = 'line-clamp-2 text-[14px] font-medium leading-5 tracking-[0.
25
25
  const SUBTITLE_CLASS = 'text-[12px] leading-4 tracking-[0.24px]'
26
26
 
27
27
  /**
28
- * Footer of action buttons. The **first** action's `variant` picks the layout:
29
- * `primary` stacked full-width buttons; `secondary` → an inline compact row.
30
- * Each button still paints in its own emphasis. Returns `null` when there are
31
- * no actions.
28
+ * The action footer. `layout` (chosen by the first action's `variant` in
29
+ * {@link RichCardBody}) picks the presentation: `stacked` (`primary`)
30
+ * full-width buttons stacked below the copy; `row` (`secondary`) content-width
31
+ * buttons pinned to the right of the chin row (`shrink-0`, so they never
32
+ * compress the title/subtitle). Each button still paints in its own emphasis.
33
+ * Returns `null` when there are no actions.
32
34
  */
33
35
  const RichCardFooter: React.FC<{
34
36
  surface: LinkAttachmentVariant
35
37
  actions?: RichCardActionButton[]
36
- }> = ({ surface, actions }) => {
38
+ layout: 'stacked' | 'row'
39
+ }> = ({ surface, actions, layout }) => {
37
40
  if (actions == null || actions.length === 0) return null
38
41
 
39
- const inline = (actions[0].variant ?? 'primary') === 'secondary'
42
+ const stacked = layout === 'stacked'
40
43
 
41
44
  return (
42
45
  <div
43
46
  className={classNames(
44
- 'mt-3 flex gap-2',
45
- inline ? 'flex-row flex-wrap' : 'flex-col'
47
+ 'flex gap-2',
48
+ // `row` is capped (`max-w-[60%]`) and shrinkable (`min-w-0`) so a long
49
+ // or localized secondary label can never grow the footer past the
50
+ // card's width; the button truncates within it and the copy yields
51
+ // first. `stacked` keeps the full-width buttons-below layout.
52
+ stacked
53
+ ? 'mt-3 flex-col'
54
+ : 'min-w-0 max-w-[60%] flex-row flex-wrap justify-end'
46
55
  )}
47
56
  >
48
57
  {actions.map((action, index) => (
@@ -50,7 +59,7 @@ const RichCardFooter: React.FC<{
50
59
  key={`${action.label}-${index}`}
51
60
  variant={surface}
52
61
  cta={action}
53
- fullWidth={!inline}
62
+ fullWidth={stacked}
54
63
  standalone
55
64
  />
56
65
  ))}
@@ -90,32 +99,49 @@ const RichCardBody: React.FC<RichCardBodyProps> = ({
90
99
  }
91
100
  : chinTextStyle
92
101
 
93
- return (
94
- <div className="shrink-0 px-4 py-3">
95
- <div className="flex min-w-0 flex-col gap-[2px]">
96
- {hasTitle && (
97
- <div className="flex min-w-0 items-center gap-2">
98
- {appIcon ? <span className="shrink-0">{appIcon}</span> : null}
99
- <p className={classNames('min-w-0', TITLE_CLASS)} style={chinTextStyle}>
100
- {title}
101
- </p>
102
- </div>
103
- )}
102
+ // A secondary primary-action lays out as a row (copy left, action right);
103
+ // primary stacks the button full-width below the copy.
104
+ const inline =
105
+ hasActions && (actions?.[0]?.variant ?? 'primary') === 'secondary'
104
106
 
105
- {hasSubtitle && (
106
- <p
107
- className={classNames(
108
- SUBTITLE_CLASS,
109
- subtitleLines <= 1 && 'truncate'
110
- )}
111
- style={subtitleStyle}
112
- >
113
- {subtitle}
107
+ const textBlock = (
108
+ <div className="flex min-w-0 flex-1 flex-col gap-[2px]">
109
+ {hasTitle && (
110
+ <div className="flex min-w-0 items-center gap-2">
111
+ {appIcon ? <span className="shrink-0">{appIcon}</span> : null}
112
+ <p className={classNames('min-w-0', TITLE_CLASS)} style={chinTextStyle}>
113
+ {title}
114
114
  </p>
115
- )}
116
- </div>
115
+ </div>
116
+ )}
117
+
118
+ {hasSubtitle && (
119
+ <p
120
+ className={classNames(
121
+ SUBTITLE_CLASS,
122
+ subtitleLines <= 1 && 'truncate'
123
+ )}
124
+ style={subtitleStyle}
125
+ >
126
+ {subtitle}
127
+ </p>
128
+ )}
129
+ </div>
130
+ )
117
131
 
118
- <RichCardFooter surface={ctaSurface} actions={actions} />
132
+ return (
133
+ <div className="shrink-0 px-4 py-3">
134
+ {inline ? (
135
+ <div className="flex items-center justify-between gap-3">
136
+ {textBlock}
137
+ <RichCardFooter surface={ctaSurface} actions={actions} layout="row" />
138
+ </div>
139
+ ) : (
140
+ <>
141
+ {textBlock}
142
+ <RichCardFooter surface={ctaSurface} actions={actions} layout="stacked" />
143
+ </>
144
+ )}
119
145
  </div>
120
146
  )
121
147
  }
@@ -32,12 +32,27 @@ const FAN_BG: Record<LinkAttachmentVariant, string> = {
32
32
  const FAN_ROTATION: Record<1 | 2 | 3, number[]> = {
33
33
  1: [0],
34
34
  2: [-10, 10],
35
- 3: [-15, 0, 15],
35
+ 3: [-13, 0, 13],
36
36
  }
37
37
  const FAN_OFFSET_PX: Record<1 | 2 | 3, number[]> = {
38
38
  1: [0],
39
- 2: [-34, 34],
40
- 3: [-58, 0, 58],
39
+ 2: [-30, 30],
40
+ 3: [-44, 0, 44],
41
+ }
42
+ // Paint z-order per slot so the **centre** card sits on top. Survivors render
43
+ // left→right, so with the naive `zIndex: position` the right card overlaps the
44
+ // middle; here the centre (slot 1 of a 3-fan) gets the highest layer.
45
+ const FAN_ZORDER: Record<1 | 2 | 3, number[]> = {
46
+ 1: [0],
47
+ 2: [0, 1],
48
+ 3: [1, 3, 2],
49
+ }
50
+ // The centre card of a 3-fan is nudged larger so it reads as the top of the
51
+ // stack rather than one of three equals (mirrors the collection design).
52
+ const FAN_SCALE: Record<1 | 2 | 3, number[]> = {
53
+ 1: [1],
54
+ 2: [1, 1],
55
+ 3: [1, 1.08, 1],
41
56
  }
42
57
 
43
58
  /**
@@ -75,6 +90,8 @@ const RichCardFan: React.FC<{
75
90
  ) as 1 | 2 | 3
76
91
  const rotations = FAN_ROTATION[count]
77
92
  const offsets = FAN_OFFSET_PX[count]
93
+ const zorder = FAN_ZORDER[count]
94
+ const scales = FAN_SCALE[count]
78
95
 
79
96
  return (
80
97
  <div
@@ -95,13 +112,17 @@ const RichCardFan: React.FC<{
95
112
  loading="lazy"
96
113
  decoding="async"
97
114
  onError={() => setFailed((prev) => ({ ...prev, [index]: true }))}
98
- // `rounded-[12px]` explicit, NOT `rounded-xl` this preset's `xl`
99
- // radius is 4rem, which rounds a small tile into an oval. A soft drop
100
- // shadow (no border) separates the overlapping tiles.
101
- className="absolute left-1/2 top-1/2 h-[82%] w-[46%] rounded-[12px] object-cover shadow-lg"
115
+ // A uniform, slightly-landscape tile (`h-74% w-66%`) with `object-cover`
116
+ // so the fan stays tidy whatever the source thumbnails are (OG scrape,
117
+ // upload, video frame) the tiles are decorative, so an even cover
118
+ // crop reads better than per-image aspect. `rounded-[12px]` explicit,
119
+ // NOT `rounded-xl` — this preset's `xl` radius is 4rem, which rounds a
120
+ // tile into an oval. A soft drop shadow (no border) separates the
121
+ // overlapping tiles.
122
+ className="absolute left-1/2 top-1/2 h-[74%] w-[66%] rounded-[12px] object-cover shadow-lg"
102
123
  style={{
103
- transform: `translate(-50%, -50%) translateX(${offsets[position]}px) rotate(${rotations[position]}deg)`,
104
- zIndex: position,
124
+ transform: `translate(-50%, -50%) translateX(${offsets[position]}px) rotate(${rotations[position]}deg) scale(${scales[position]})`,
125
+ zIndex: zorder[position],
105
126
  }}
106
127
  />
107
128
  ))}
@@ -22,8 +22,9 @@ export interface RichCardActionButton {
22
22
  onClick?: () => void
23
23
  /**
24
24
  * Button emphasis. The **first** action's variant drives the footer layout:
25
- * `primary` → a stacked full-width button; `secondary` → an inline compact
26
- * row. Defaults to `primary`.
25
+ * `primary` → a stacked full-width button below the copy; `secondary` → a row
26
+ * in the chin with the copy on the left and the action pinned right. Defaults
27
+ * to `primary`.
27
28
  */
28
29
  variant?: 'primary' | 'secondary'
29
30
  }