@linktr.ee/messaging-react 2.1.0 → 2.2.0-rc-1778753733

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 (58) hide show
  1. package/dist/{Card-CsJvUF_b.js → Card-BdTueeyk.js} +2 -2
  2. package/dist/{Card-CsJvUF_b.js.map → Card-BdTueeyk.js.map} +1 -1
  3. package/dist/{Card-DlMSDSdm.js → Card-ChR37pLZ.js} +2 -2
  4. package/dist/{Card-DlMSDSdm.js.map → Card-ChR37pLZ.js.map} +1 -1
  5. package/dist/{Card-CFFNq49v.js → Card-EKxCn56j.js} +3 -3
  6. package/dist/{Card-CFFNq49v.js.map → Card-EKxCn56j.js.map} +1 -1
  7. package/dist/{LockedThumbnail-DpJx169C.js → LockedThumbnail-B16qP3eH.js} +2 -2
  8. package/dist/{LockedThumbnail-DpJx169C.js.map → LockedThumbnail-B16qP3eH.js.map} +1 -1
  9. package/dist/index-Dn7BC9xK.js +4748 -0
  10. package/dist/index-Dn7BC9xK.js.map +1 -0
  11. package/dist/index.d.ts +591 -25
  12. package/dist/index.js +24 -19
  13. package/package.json +1 -1
  14. package/src/components/CustomMessage/MessageAttachmentConversations.stories.tsx +841 -0
  15. package/src/components/LinkAttachment/LinkAttachment.stories.tsx +7 -92
  16. package/src/components/LinkAttachment/LinkAttachment.test.tsx +69 -0
  17. package/src/components/LinkAttachment/components/Received/Card.tsx +10 -30
  18. package/src/components/LinkAttachment/components/_shared/CardShell.tsx +5 -1
  19. package/src/components/LinkAttachment/index.tsx +24 -50
  20. package/src/components/LinkAttachment/types.ts +12 -5
  21. package/src/components/MessageAttachment/Audio/AudioAttachment.stories.tsx +203 -0
  22. package/src/components/MessageAttachment/Audio/index.tsx +189 -0
  23. package/src/components/MessageAttachment/File/FileAttachment.stories.tsx +352 -0
  24. package/src/components/MessageAttachment/File/index.tsx +240 -0
  25. package/src/components/MessageAttachment/Image/ImageAttachment.stories.tsx +288 -0
  26. package/src/components/MessageAttachment/Image/index.tsx +257 -0
  27. package/src/components/MessageAttachment/MessageAttachment.test.tsx +783 -0
  28. package/src/components/MessageAttachment/Pdf/PdfAttachment.stories.tsx +292 -0
  29. package/src/components/MessageAttachment/Pdf/index.tsx +228 -0
  30. package/src/components/MessageAttachment/Video/VideoAttachment.stories.tsx +272 -0
  31. package/src/components/MessageAttachment/Video/index.tsx +281 -0
  32. package/src/components/MessageAttachment/_shared/Bubble.tsx +173 -0
  33. package/src/components/MessageAttachment/_shared/CompactDocumentRow.tsx +152 -0
  34. package/src/components/MessageAttachment/_shared/DismissButton.tsx +39 -0
  35. package/src/components/MessageAttachment/_shared/DownloadAction.tsx +175 -0
  36. package/src/components/MessageAttachment/_shared/ImageViewer.tsx +314 -0
  37. package/src/components/MessageAttachment/_shared/MediaStackGrid.tsx +139 -0
  38. package/src/components/MessageAttachment/_shared/PdfViewer.tsx +100 -0
  39. package/src/components/MessageAttachment/_shared/VideoViewer.tsx +171 -0
  40. package/src/components/MessageAttachment/_shared/ViewerShell.tsx +159 -0
  41. package/src/components/MessageAttachment/_shared/fileMeta.test.ts +82 -0
  42. package/src/components/MessageAttachment/_shared/fileMeta.ts +95 -0
  43. package/src/components/MessageAttachment/_shared/triggerDownload.ts +54 -0
  44. package/src/components/MessageAttachment/_shared/useViewer.ts +53 -0
  45. package/src/components/MessageAttachment/index.tsx +149 -0
  46. package/src/components/MessageAttachment/stories/StoryTable.tsx +72 -0
  47. package/src/components/MessageAttachment/types.ts +178 -0
  48. package/src/index.ts +32 -0
  49. package/dist/Card-D32U6KfZ.js +0 -85
  50. package/dist/Card-D32U6KfZ.js.map +0 -1
  51. package/dist/Card-DlSSJPip.js +0 -60
  52. package/dist/Card-DlSSJPip.js.map +0 -1
  53. package/dist/Card-zGbhRBwv.js +0 -48
  54. package/dist/Card-zGbhRBwv.js.map +0 -1
  55. package/dist/CardThumbnail-DTBuRQHF.js +0 -239
  56. package/dist/CardThumbnail-DTBuRQHF.js.map +0 -1
  57. package/dist/index-DfcRe-Hj.js +0 -3103
  58. package/dist/index-DfcRe-Hj.js.map +0 -1
@@ -0,0 +1,292 @@
1
+ import type { Meta, StoryFn } from '@storybook/react'
2
+ import React from 'react'
3
+
4
+ import {
5
+ StoryGrid,
6
+ StoryHeading,
7
+ StoryPage,
8
+ StoryRow,
9
+ } from '../stories/StoryTable'
10
+ import type { PdfItem } from '../types'
11
+
12
+ import PdfAttachment from '.'
13
+
14
+ const PDF_SRC = 'https://www.w3.org/WAI/WCAG21/wcag21.pdf'
15
+
16
+ const STACK_PDFS: PdfItem[] = [
17
+ { src: PDF_SRC, filename: 'ESOP-summary.pdf', fileSize: 388_658 },
18
+ { src: PDF_SRC, filename: 'Vesting-schedule.pdf', fileSize: 142_336 },
19
+ { src: PDF_SRC, filename: 'Cap-table-snapshot.pdf', fileSize: 96_021 },
20
+ { src: PDF_SRC, filename: 'Board-resolution.pdf', fileSize: 24_580 },
21
+ ]
22
+
23
+ const meta: Meta = {
24
+ title: 'MessageAttachment/PDF',
25
+ parameters: { layout: 'fullscreen' },
26
+ }
27
+ export default meta
28
+
29
+ const handleDismiss = () => {
30
+ // eslint-disable-next-line no-console
31
+ console.log('Dismissed PDF attachment')
32
+ }
33
+
34
+ /**
35
+ * Compact PDF row inside a chat bubble — icon + filename + `EXT · SIZE`.
36
+ * Composer / Sent / Received are all interactive: clicking opens the
37
+ * built-in `PdfViewer` (full-viewport iframe + filename + download).
38
+ */
39
+ export const Single: StoryFn = () => (
40
+ <StoryPage>
41
+ <StoryHeading
42
+ title="PDF attachment"
43
+ description="Click any bubble to open the full-viewport PDF viewer with built-in download."
44
+ />
45
+ <StoryGrid>
46
+ <StoryRow
47
+ label="Default"
48
+ composer={
49
+ <PdfAttachment.Composer
50
+ src={PDF_SRC}
51
+ filename="ESOP-summary.pdf"
52
+ fileSize={388_658}
53
+ onDismiss={handleDismiss}
54
+ />
55
+ }
56
+ sent={
57
+ <PdfAttachment.Sent
58
+ src={PDF_SRC}
59
+ filename="ESOP-summary.pdf"
60
+ fileSize={388_658}
61
+ />
62
+ }
63
+ received={
64
+ <PdfAttachment.Received
65
+ src={PDF_SRC}
66
+ filename="ESOP-summary.pdf"
67
+ fileSize={388_658}
68
+ />
69
+ }
70
+ />
71
+ <StoryRow
72
+ label="Long filename"
73
+ composer={
74
+ <PdfAttachment.Composer
75
+ src={PDF_SRC}
76
+ filename="ESOP-summary-and-vesting-schedule-2026-Q4-FINAL.pdf"
77
+ fileSize={1_240_000}
78
+ onDismiss={handleDismiss}
79
+ />
80
+ }
81
+ sent={
82
+ <PdfAttachment.Sent
83
+ src={PDF_SRC}
84
+ filename="ESOP-summary-and-vesting-schedule-2026-Q4-FINAL.pdf"
85
+ fileSize={1_240_000}
86
+ />
87
+ }
88
+ received={
89
+ <PdfAttachment.Received
90
+ src={PDF_SRC}
91
+ filename="ESOP-summary-and-vesting-schedule-2026-Q4-FINAL.pdf"
92
+ fileSize={1_240_000}
93
+ />
94
+ }
95
+ />
96
+ <StoryRow
97
+ label="No file size"
98
+ composer={
99
+ <PdfAttachment.Composer
100
+ src={PDF_SRC}
101
+ filename="contract.pdf"
102
+ onDismiss={handleDismiss}
103
+ />
104
+ }
105
+ sent={<PdfAttachment.Sent src={PDF_SRC} filename="contract.pdf" />}
106
+ received={<PdfAttachment.Received src={PDF_SRC} filename="contract.pdf" />}
107
+ />
108
+ </StoryGrid>
109
+ </StoryPage>
110
+ )
111
+
112
+ export const SingleWithText: StoryFn = () => (
113
+ <StoryPage>
114
+ <StoryHeading
115
+ title="PDF with caption"
116
+ description="Caption sits inside the same bubble, below the file row — matches the mobile chat 'Here is the file' screenshot."
117
+ />
118
+ <StoryGrid>
119
+ <StoryRow
120
+ label="Caption"
121
+ composer={
122
+ <PdfAttachment.Composer
123
+ src={PDF_SRC}
124
+ filename="ESOP-summary.pdf"
125
+ fileSize={388_658}
126
+ onDismiss={handleDismiss}
127
+ />
128
+ }
129
+ sent={
130
+ <PdfAttachment.Sent
131
+ src={PDF_SRC}
132
+ filename="ESOP-summary.pdf"
133
+ fileSize={388_658}
134
+ text="Here is the file"
135
+ />
136
+ }
137
+ received={
138
+ <PdfAttachment.Received
139
+ src={PDF_SRC}
140
+ filename="ESOP-summary.pdf"
141
+ fileSize={388_658}
142
+ text="Here is the file"
143
+ />
144
+ }
145
+ />
146
+ <StoryRow
147
+ label="Long caption"
148
+ composer={
149
+ <PdfAttachment.Composer
150
+ src={PDF_SRC}
151
+ filename="ESOP-summary.pdf"
152
+ fileSize={388_658}
153
+ onDismiss={handleDismiss}
154
+ />
155
+ }
156
+ sent={
157
+ <PdfAttachment.Sent
158
+ src={PDF_SRC}
159
+ filename="ESOP-summary.pdf"
160
+ fileSize={388_658}
161
+ text="Sharing the latest ESOP summary — let me know if you need any sections clarified before we sign."
162
+ />
163
+ }
164
+ received={
165
+ <PdfAttachment.Received
166
+ src={PDF_SRC}
167
+ filename="ESOP-summary.pdf"
168
+ fileSize={388_658}
169
+ text="Sharing the latest ESOP summary — let me know if you need any sections clarified before we sign."
170
+ />
171
+ }
172
+ />
173
+ </StoryGrid>
174
+ </StoryPage>
175
+ )
176
+
177
+ /**
178
+ * Stacked PDF attachments — multiple PDFs in one bubble. Each row is
179
+ * its own tappable target that opens the `PdfViewer` for that file.
180
+ *
181
+ * Sent + Received only — the composer surface accepts a single
182
+ * attachment at a time (the user picks files one at a time inside the
183
+ * file picker, so the composer preview lives outside this scenario).
184
+ */
185
+ export const Stacked: StoryFn = () => (
186
+ <StoryPage>
187
+ <StoryHeading
188
+ title="Stacked PDFs"
189
+ description="Each row is its own tappable target; clicking opens that PDF in the viewer. Composer column is intentionally blank — the composer accepts a single attachment at a time."
190
+ />
191
+ <StoryGrid>
192
+ <StoryRow
193
+ label="2 PDFs"
194
+ composer={
195
+ <PdfAttachment.Composer
196
+ src={PDF_SRC}
197
+ filename="ESOP-summary.pdf"
198
+ fileSize={388_658}
199
+ onDismiss={handleDismiss}
200
+ />
201
+ }
202
+ sent={<PdfAttachment.Sent items={STACK_PDFS.slice(0, 2)} />}
203
+ received={<PdfAttachment.Received items={STACK_PDFS.slice(0, 2)} />}
204
+ />
205
+ <StoryRow
206
+ label="3 PDFs"
207
+ composer={
208
+ <PdfAttachment.Composer
209
+ src={PDF_SRC}
210
+ filename="ESOP-summary.pdf"
211
+ fileSize={388_658}
212
+ onDismiss={handleDismiss}
213
+ />
214
+ }
215
+ sent={<PdfAttachment.Sent items={STACK_PDFS.slice(0, 3)} />}
216
+ received={<PdfAttachment.Received items={STACK_PDFS.slice(0, 3)} />}
217
+ />
218
+ <StoryRow
219
+ label="4 PDFs"
220
+ composer={
221
+ <PdfAttachment.Composer
222
+ src={PDF_SRC}
223
+ filename="ESOP-summary.pdf"
224
+ fileSize={388_658}
225
+ onDismiss={handleDismiss}
226
+ />
227
+ }
228
+ sent={<PdfAttachment.Sent items={STACK_PDFS} />}
229
+ received={<PdfAttachment.Received items={STACK_PDFS} />}
230
+ />
231
+ </StoryGrid>
232
+ </StoryPage>
233
+ )
234
+
235
+ /** Stacked PDFs with an accompanying caption.
236
+ * Sent + Received only — see `Stacked` for the rationale. */
237
+ export const StackedWithText: StoryFn = () => (
238
+ <StoryPage>
239
+ <StoryHeading
240
+ title="Stacked PDFs with caption"
241
+ description="Caption sits inside the same bubble, below the file list. Composer column is intentionally blank — the composer accepts a single attachment at a time."
242
+ />
243
+ <StoryGrid>
244
+ <StoryRow
245
+ label="3 PDFs + caption"
246
+ composer={
247
+ <PdfAttachment.Composer
248
+ src={PDF_SRC}
249
+ filename="ESOP-summary.pdf"
250
+ fileSize={388_658}
251
+ onDismiss={handleDismiss}
252
+ />
253
+ }
254
+ sent={
255
+ <PdfAttachment.Sent
256
+ items={STACK_PDFS.slice(0, 3)}
257
+ text="Here are the docs from the call — review when you can"
258
+ />
259
+ }
260
+ received={
261
+ <PdfAttachment.Received
262
+ items={STACK_PDFS.slice(0, 3)}
263
+ text="Here are the docs from the call — review when you can"
264
+ />
265
+ }
266
+ />
267
+ <StoryRow
268
+ label="4 PDFs + caption"
269
+ composer={
270
+ <PdfAttachment.Composer
271
+ src={PDF_SRC}
272
+ filename="ESOP-summary.pdf"
273
+ fileSize={388_658}
274
+ onDismiss={handleDismiss}
275
+ />
276
+ }
277
+ sent={
278
+ <PdfAttachment.Sent
279
+ items={STACK_PDFS}
280
+ text="Full ESOP package + the cap table snapshot for tonight's meeting."
281
+ />
282
+ }
283
+ received={
284
+ <PdfAttachment.Received
285
+ items={STACK_PDFS}
286
+ text="Full ESOP package + the cap table snapshot for tonight's meeting."
287
+ />
288
+ }
289
+ />
290
+ </StoryGrid>
291
+ </StoryPage>
292
+ )
@@ -0,0 +1,228 @@
1
+ import React from 'react'
2
+
3
+ import Bubble from '../_shared/Bubble'
4
+ import CompactDocumentRow from '../_shared/CompactDocumentRow'
5
+ import DismissButton from '../_shared/DismissButton'
6
+ import DownloadAction from '../_shared/DownloadAction'
7
+ import { filenameFromUrl } from '../_shared/fileMeta'
8
+ import PdfViewer from '../_shared/PdfViewer'
9
+ import { useViewer } from '../_shared/useViewer'
10
+ import {
11
+ bubbleVariantForState,
12
+ type BubbleVariant,
13
+ type ComposerExtras,
14
+ type MessageAttachmentBaseProps,
15
+ type MessageAttachmentState,
16
+ type PdfItem,
17
+ } from '../types'
18
+
19
+ export interface PdfAttachmentSharedProps extends MessageAttachmentBaseProps {
20
+ /** Single PDF — convenience for the most common case. */
21
+ src?: string
22
+ /** Filename — drives the title + the meta line + the viewer toolbar. */
23
+ filename?: string
24
+ /** File size in bytes — formatted into the `EXT · SIZE` meta line. */
25
+ fileSize?: number
26
+ /**
27
+ * Override displayed title (defaults to `filename`). Useful when a
28
+ * sender re-titles a generated PDF before sending.
29
+ */
30
+ title?: string
31
+ /**
32
+ * Stacked PDFs. Takes precedence over `src` when set. Each item
33
+ * renders as its own row inside the bubble; clicking a row opens
34
+ * that PDF in the viewer. Sent + Received only — the composer
35
+ * surface accepts a single attachment at a time.
36
+ */
37
+ items?: PdfItem[]
38
+ /**
39
+ * Forwarded to the viewer trigger. When omitted the click still
40
+ * opens the built-in `PdfViewer`. Supply this to fire analytics, or
41
+ * return `false` to intercept the open (e.g. swap to a route-based
42
+ * viewer / confirmation modal). Any other return value (including
43
+ * `void`/`undefined`) lets the built-in viewer open. For stacked
44
+ * attachments the `index` argument identifies the row.
45
+ */
46
+ onClick?: (index: number) => boolean | void
47
+ }
48
+
49
+ const resolveItems = ({
50
+ src,
51
+ filename,
52
+ fileSize,
53
+ title,
54
+ items,
55
+ }: {
56
+ src?: string
57
+ filename?: string
58
+ fileSize?: number
59
+ title?: string
60
+ items?: PdfItem[]
61
+ }): PdfItem[] => {
62
+ if (items && items.length > 0) return items
63
+ if (src) return [{ src, filename, fileSize, title }]
64
+ return []
65
+ }
66
+
67
+ interface PdfRowButtonProps {
68
+ variant: BubbleVariant
69
+ item: PdfItem
70
+ index: number
71
+ onActivate: (index: number) => void
72
+ trailingAction?: React.ReactNode
73
+ }
74
+
75
+ /**
76
+ * Single tappable PDF row — used both for the lone PDF case and for
77
+ * each child of a stacked attachment. The icon + filename area opens
78
+ * the viewer; the trailing slot hosts a separate, sibling button
79
+ * (download or dismiss) so each affordance owns its own click target.
80
+ */
81
+ const PdfRowButton: React.FC<PdfRowButtonProps> = ({
82
+ variant,
83
+ item,
84
+ index,
85
+ onActivate,
86
+ trailingAction,
87
+ }) => {
88
+ const resolvedFilename = item.filename ?? filenameFromUrl(item.src)
89
+ return (
90
+ <CompactDocumentRow
91
+ variant={variant}
92
+ filename={resolvedFilename}
93
+ title={item.title}
94
+ mimeType="application/pdf"
95
+ fileSize={item.fileSize}
96
+ onActivate={() => onActivate(index)}
97
+ activateLabel={`Open ${resolvedFilename}`}
98
+ trailingAction={trailingAction}
99
+ />
100
+ )
101
+ }
102
+
103
+ interface InternalPdfRowProps extends PdfAttachmentSharedProps {
104
+ state: MessageAttachmentState
105
+ onDismiss?: () => void
106
+ }
107
+
108
+ const PdfAttachmentRow: React.FC<InternalPdfRowProps> = ({
109
+ state,
110
+ src,
111
+ filename,
112
+ fileSize,
113
+ title,
114
+ items,
115
+ text,
116
+ groupPosition,
117
+ onClick,
118
+ onDismiss,
119
+ }) => {
120
+ const variant = bubbleVariantForState(state)
121
+ const resolvedItems = resolveItems({ src, filename, fileSize, title, items })
122
+ const { viewerOpen, viewerIndex, handleActivate, closeViewer } = useViewer(
123
+ onClick
124
+ )
125
+ const showDismiss = state === 'composer' && !!onDismiss
126
+
127
+ if (resolvedItems.length === 0) {
128
+ return null
129
+ }
130
+
131
+ // Clamp the viewer index defensively: if `items` shrinks between
132
+ // the open click and the next render (server push, optimistic
133
+ // update, etc.) `viewerIndex` may point past the new array.
134
+ // Dereferencing `undefined` would crash the whole bubble, so fall
135
+ // back to the last available row instead.
136
+ const safeIndex = Math.min(viewerIndex, resolvedItems.length - 1)
137
+ const activeItem = resolvedItems[safeIndex]
138
+ const activeFilename =
139
+ activeItem.filename ?? filenameFromUrl(activeItem.src)
140
+
141
+ return (
142
+ <Bubble
143
+ variant={variant}
144
+ text={text}
145
+ groupPosition={groupPosition}
146
+ data-testid="pdf-attachment"
147
+ >
148
+ {/* Stacked rows get a small vertical gap so each PDF reads as
149
+ a discrete attachment rather than one merged blob. Single
150
+ row falls through to a no-op `gap-0`. */}
151
+ <div className="flex flex-col gap-2">
152
+ {resolvedItems.map((item, index) => {
153
+ const itemFilename = item.filename ?? filenameFromUrl(item.src)
154
+ // Composer only supports a single attachment so the dismiss
155
+ // control sits on the only row. Sent / Received rows expose
156
+ // a download icon button so the user can grab the PDF
157
+ // without opening the viewer first (the viewer also has a
158
+ // download in its toolbar — this is the quick-grab shortcut).
159
+ const trailingAction =
160
+ showDismiss && index === 0 ? (
161
+ <DismissButton onClick={onDismiss!} variant="inline" />
162
+ ) : state === 'composer' ? undefined : (
163
+ <DownloadAction
164
+ url={item.src}
165
+ filename={itemFilename}
166
+ variant="inline"
167
+ label={`Download ${itemFilename}`}
168
+ tone={variant}
169
+ />
170
+ )
171
+ return (
172
+ <PdfRowButton
173
+ key={`${item.src}-${index}`}
174
+ variant={variant}
175
+ item={item}
176
+ index={index}
177
+ onActivate={handleActivate}
178
+ trailingAction={trailingAction}
179
+ />
180
+ )
181
+ })}
182
+ </div>
183
+
184
+ <PdfViewer
185
+ open={viewerOpen}
186
+ src={activeItem.src}
187
+ filename={activeFilename}
188
+ onClose={closeViewer}
189
+ />
190
+ </Bubble>
191
+ )
192
+ }
193
+
194
+ /**
195
+ * Composer-only props. Single PDF (`src`) only — the composer surface
196
+ * accepts a single attachment at a time, so `items` is not supported.
197
+ * Captions (`text`) and `groupPosition` are also dropped: the composer
198
+ * renders a standalone draft, not part of a same-author run, and
199
+ * captions live in the message-input textarea, not inside the draft
200
+ * attachment preview.
201
+ */
202
+ export interface PdfComposerProps extends ComposerExtras {
203
+ src?: string
204
+ filename?: string
205
+ fileSize?: number
206
+ title?: string
207
+ onClick?: (index: number) => boolean | void
208
+ }
209
+ export type PdfSentProps = PdfAttachmentSharedProps
210
+ export type PdfReceivedProps = PdfAttachmentSharedProps
211
+
212
+ const PdfComposer: React.FC<PdfComposerProps> = (props) => (
213
+ <PdfAttachmentRow {...props} state="composer" />
214
+ )
215
+ const PdfSent: React.FC<PdfSentProps> = (props) => (
216
+ <PdfAttachmentRow {...props} state="sent" />
217
+ )
218
+ const PdfReceived: React.FC<PdfReceivedProps> = (props) => (
219
+ <PdfAttachmentRow {...props} state="received" />
220
+ )
221
+
222
+ const PdfAttachment = {
223
+ Composer: PdfComposer,
224
+ Sent: PdfSent,
225
+ Received: PdfReceived,
226
+ }
227
+
228
+ export default PdfAttachment