@linktr.ee/messaging-react 4.1.8-rc-1787240024 → 4.1.8

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 (34) hide show
  1. package/dist/{Card-BvMoNrpF.js → Card-B5YUxWCs.js} +2 -2
  2. package/dist/{Card-BvMoNrpF.js.map → Card-B5YUxWCs.js.map} +1 -1
  3. package/dist/{Card-DGr_puw8.cjs → Card-DjafcNS_.cjs} +2 -2
  4. package/dist/{Card-DGr_puw8.cjs.map → Card-DjafcNS_.cjs.map} +1 -1
  5. package/dist/assets/index.css +1 -1
  6. package/dist/index-Bwjn-YoV.cjs +3 -0
  7. package/dist/index-Bwjn-YoV.cjs.map +1 -0
  8. package/dist/{index-BSPkcgIW.js → index-l73T0G4A.js} +404 -404
  9. package/dist/index-l73T0G4A.js.map +1 -0
  10. package/dist/index.cjs +1 -1
  11. package/dist/index.js +1 -1
  12. package/package.json +7 -9
  13. package/src/components/AttachmentCard/AttachmentCard.stories.tsx +0 -36
  14. package/src/components/AttachmentCard/Thumbnail.tsx +2 -2
  15. package/src/components/CustomMessageInput/index.tsx +1 -1
  16. package/src/components/MessageAttachment/MessageAttachment.behavior.test.tsx +182 -0
  17. package/src/components/MessageAttachment/_shared/Bubble.test.tsx +111 -0
  18. package/src/components/MessageAttachment/_shared/CarouselNav.test.tsx +36 -0
  19. package/src/components/MessageAttachment/_shared/CompactDocumentRow.test.tsx +114 -0
  20. package/src/components/MessageAttachment/_shared/DismissButton.test.tsx +42 -0
  21. package/src/components/MessageAttachment/_shared/DownloadAction.test.tsx +168 -0
  22. package/src/components/MessageAttachment/_shared/ImageViewer.test.tsx +52 -0
  23. package/src/components/MessageAttachment/_shared/MediaStackGrid.test.tsx +69 -0
  24. package/src/components/MessageAttachment/_shared/PdfViewer.test.tsx +45 -0
  25. package/src/components/MessageAttachment/_shared/VideoViewer.test.tsx +55 -0
  26. package/src/components/MessageAttachment/_shared/ViewerShell.test.tsx +246 -0
  27. package/src/components/MessageAttachment/_shared/fileMeta.test.ts +44 -0
  28. package/src/components/MessageAttachment/_shared/triggerDownload.test.ts +113 -0
  29. package/src/components/MessageAttachment/_shared/useCarousel.test.tsx +68 -0
  30. package/src/components/MessageAttachment/_shared/viewerChromeClasses.test.ts +16 -0
  31. package/src/styles.css +46 -89
  32. package/dist/index-BSPkcgIW.js.map +0 -1
  33. package/dist/index-DKiY5_Ce.cjs +0 -3
  34. package/dist/index-DKiY5_Ce.cjs.map +0 -1
@@ -25,6 +25,14 @@ describe('formatFileSize', () => {
25
25
  expect(formatFileSize(Infinity)).toBe('')
26
26
  expect(formatFileSize(-1)).toBe('')
27
27
  })
28
+
29
+ it('handles unit boundaries without crossing into the next unit early', () => {
30
+ expect(formatFileSize(1023)).toBe('1023 B')
31
+ expect(formatFileSize(1024 * 1024 - 1)).toBe('1024.0 KB')
32
+ expect(formatFileSize(1024 * 1024)).toBe('1.00 MB')
33
+ expect(formatFileSize(1024 ** 3 - 1)).toBe('1024.00 MB')
34
+ expect(formatFileSize(1024 ** 3)).toBe('1.00 GB')
35
+ })
28
36
  })
29
37
 
30
38
  describe('getFileExtensionLabel', () => {
@@ -43,6 +51,18 @@ describe('getFileExtensionLabel', () => {
43
51
  expect(getFileExtensionLabel('application/octet-stream')).toBeUndefined()
44
52
  expect(getFileExtensionLabel()).toBeUndefined()
45
53
  })
54
+
55
+ it('handles dot boundaries, extension length, and MIME fallbacks', () => {
56
+ expect(getFileExtensionLabel('text/plain', '.gitignore')).toBe('TXT')
57
+ expect(getFileExtensionLabel('text/plain', 'notes.')).toBe('TXT')
58
+ expect(getFileExtensionLabel('image/webp', 'photo.webp')).toBe('WEBP')
59
+ expect(getFileExtensionLabel('image/webp', 'photo.abcdef')).toBe('WEBP')
60
+ expect(getFileExtensionLabel('application/json', 'data.12345')).toBe(
61
+ '12345'
62
+ )
63
+ expect(getFileExtensionLabel('image/*')).toBeUndefined()
64
+ expect(getFileExtensionLabel('application/octet-stream')).toBeUndefined()
65
+ })
46
66
  })
47
67
 
48
68
  describe('buildCompactMetaLabel', () => {
@@ -59,6 +79,23 @@ describe('buildCompactMetaLabel', () => {
59
79
  it('returns undefined when nothing is renderable', () => {
60
80
  expect(buildCompactMetaLabel()).toBeUndefined()
61
81
  })
82
+
83
+ it('drops non-positive and non-number sizes while retaining size-only labels', () => {
84
+ expect(buildCompactMetaLabel('application/pdf', 'report.pdf', 0)).toBe(
85
+ 'PDF'
86
+ )
87
+ expect(buildCompactMetaLabel('application/pdf', 'report.pdf', -1)).toBe(
88
+ 'PDF'
89
+ )
90
+ expect(buildCompactMetaLabel(undefined, undefined, 1024)).toBe('1.0 KB')
91
+ expect(
92
+ buildCompactMetaLabel(
93
+ 'application/octet-stream',
94
+ undefined,
95
+ '1024' as never
96
+ )
97
+ ).toBeUndefined()
98
+ })
62
99
  })
63
100
 
64
101
  describe('filenameFromUrl', () => {
@@ -77,4 +114,11 @@ describe('filenameFromUrl', () => {
77
114
  it('falls back to a default when the URL cannot be parsed', () => {
78
115
  expect(filenameFromUrl('not a url at all')).toBe('download')
79
116
  })
117
+
118
+ it('handles trailing slashes, deep paths, and query strings', () => {
119
+ expect(filenameFromUrl('https://cdn.example.com/folder/')).toBe('download')
120
+ expect(
121
+ filenameFromUrl('https://cdn.example.com/a/b/c/report.pdf?download=1')
122
+ ).toBe('report.pdf')
123
+ })
80
124
  })
@@ -0,0 +1,113 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+
3
+ import { triggerDownload } from './triggerDownload'
4
+
5
+ describe('triggerDownload', () => {
6
+ const fetchMock = vi.fn()
7
+ const openMock = vi.fn()
8
+ const createObjectURLMock = vi.fn()
9
+ const revokeObjectURLMock = vi.fn()
10
+ const clickMock = vi
11
+ .spyOn(HTMLAnchorElement.prototype, 'click')
12
+ .mockImplementation(() => undefined)
13
+
14
+ beforeEach(() => {
15
+ vi.stubGlobal('fetch', fetchMock)
16
+ vi.spyOn(window, 'open').mockImplementation(openMock)
17
+ Object.defineProperty(URL, 'createObjectURL', {
18
+ configurable: true,
19
+ value: createObjectURLMock,
20
+ })
21
+ Object.defineProperty(URL, 'revokeObjectURL', {
22
+ configurable: true,
23
+ value: revokeObjectURLMock,
24
+ })
25
+ fetchMock.mockReset()
26
+ openMock.mockReset()
27
+ createObjectURLMock.mockReset()
28
+ revokeObjectURLMock.mockReset()
29
+ clickMock.mockClear()
30
+ })
31
+
32
+ it('downloads a successful response with the supplied filename', async () => {
33
+ const blob = new Blob(['file'])
34
+ fetchMock.mockResolvedValue({
35
+ ok: true,
36
+ blob: () => Promise.resolve(blob),
37
+ })
38
+ createObjectURLMock.mockReturnValue('blob:attachment')
39
+ const appendSpy = vi.spyOn(document.body, 'appendChild')
40
+ const removeSpy = vi.spyOn(document.body, 'removeChild')
41
+
42
+ await expect(
43
+ triggerDownload('https://cdn.example.com/file.pdf', 'report.pdf')
44
+ ).resolves.toBeUndefined()
45
+
46
+ expect(fetchMock).toHaveBeenCalledWith('https://cdn.example.com/file.pdf', {
47
+ mode: 'cors',
48
+ })
49
+ expect(createObjectURLMock).toHaveBeenCalledWith(blob)
50
+ expect(clickMock).toHaveBeenCalledOnce()
51
+ expect(revokeObjectURLMock).toHaveBeenCalledWith('blob:attachment')
52
+ const anchor = appendSpy.mock.calls.at(-1)?.[0] as HTMLAnchorElement
53
+ expect(anchor.href).toBe('blob:attachment')
54
+ expect(anchor.download).toBe('report.pdf')
55
+ expect(anchor.style.display).toBe('none')
56
+ expect(removeSpy).toHaveBeenCalledWith(anchor)
57
+ })
58
+
59
+ it('uses the URL filename when no filename is supplied', async () => {
60
+ fetchMock.mockResolvedValue({
61
+ ok: true,
62
+ blob: () => Promise.resolve(new Blob()),
63
+ })
64
+ createObjectURLMock.mockReturnValue('blob:attachment')
65
+ const appendSpy = vi.spyOn(document.body, 'appendChild')
66
+
67
+ await triggerDownload('https://cdn.example.com/a/My%20file.pdf')
68
+
69
+ expect(
70
+ (appendSpy.mock.calls.at(-1)?.[0] as HTMLAnchorElement).download
71
+ ).toBe('My file.pdf')
72
+ })
73
+
74
+ it.each(['a non-ok response', 'a rejected fetch'])(
75
+ 'falls back to a new window for %s',
76
+ async (label) => {
77
+ if (label === 'a non-ok response') {
78
+ fetchMock.mockResolvedValue({ ok: false, status: 404 })
79
+ } else {
80
+ fetchMock.mockRejectedValue(new Error('offline'))
81
+ }
82
+ openMock.mockReturnValue({})
83
+
84
+ await expect(
85
+ triggerDownload('https://cdn.example.com/file.pdf')
86
+ ).resolves.toBeUndefined()
87
+
88
+ expect(openMock).toHaveBeenCalledWith(
89
+ 'https://cdn.example.com/file.pdf',
90
+ '_blank',
91
+ 'noopener,noreferrer'
92
+ )
93
+ }
94
+ )
95
+
96
+ it('uses a raw-url anchor when the popup is blocked', async () => {
97
+ fetchMock.mockRejectedValue(new Error('offline'))
98
+ openMock.mockReturnValue(null)
99
+ const appendSpy = vi.spyOn(document.body, 'appendChild')
100
+ const removeSpy = vi.spyOn(document.body, 'removeChild')
101
+
102
+ await expect(
103
+ triggerDownload('https://cdn.example.com/file.pdf')
104
+ ).resolves.toBeUndefined()
105
+
106
+ const anchor = appendSpy.mock.calls.at(-1)?.[0] as HTMLAnchorElement
107
+ expect(anchor.href).toBe('https://cdn.example.com/file.pdf')
108
+ expect(anchor.download).toBe('file.pdf')
109
+ expect(anchor.target).toBe('_blank')
110
+ expect(anchor.rel).toBe('noopener noreferrer')
111
+ expect(removeSpy).toHaveBeenCalledWith(anchor)
112
+ })
113
+ })
@@ -0,0 +1,68 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { fireEvent, renderWithProviders, screen } from '../../../test/utils'
4
+
5
+ import { useCarousel } from './useCarousel'
6
+
7
+ const Harness = ({
8
+ open,
9
+ length,
10
+ initialIndex = 0,
11
+ }: {
12
+ open: boolean
13
+ length: number
14
+ initialIndex?: number
15
+ }) => {
16
+ const { index, prev, next } = useCarousel({ open, length, initialIndex })
17
+ return (
18
+ <div>
19
+ <span data-testid="index">{index}</span>
20
+ <button type="button" onClick={prev}>
21
+ Prev
22
+ </button>
23
+ <button type="button" onClick={next}>
24
+ Next
25
+ </button>
26
+ </div>
27
+ )
28
+ }
29
+
30
+ describe('useCarousel', () => {
31
+ it('clamps initial index, wraps, and resets on reopen', () => {
32
+ const { rerender } = renderWithProviders(
33
+ <Harness open length={3} initialIndex={99} />
34
+ )
35
+ expect(screen.getByTestId('index')).toHaveTextContent('2')
36
+ fireEvent.click(screen.getByRole('button', { name: 'Next' }))
37
+ expect(screen.getByTestId('index')).toHaveTextContent('0')
38
+ fireEvent.click(screen.getByRole('button', { name: 'Prev' }))
39
+ expect(screen.getByTestId('index')).toHaveTextContent('2')
40
+ rerender(<Harness open={false} length={3} initialIndex={1} />)
41
+ rerender(<Harness open length={3} initialIndex={1} />)
42
+ expect(screen.getByTestId('index')).toHaveTextContent('1')
43
+ })
44
+
45
+ it('handles arrows only when open with multiple items', () => {
46
+ const { rerender } = renderWithProviders(
47
+ <Harness open length={2} initialIndex={0} />
48
+ )
49
+ const right = fireEvent.keyDown(window, { key: 'ArrowRight' })
50
+ expect(screen.getByTestId('index')).toHaveTextContent('1')
51
+ expect(right).toBe(false)
52
+ for (const tagName of ['video', 'audio'] as const) {
53
+ const media = document.createElement(tagName)
54
+ media.tabIndex = 0
55
+ document.body.appendChild(media)
56
+ media.focus()
57
+ fireEvent.keyDown(window, { key: 'ArrowLeft' })
58
+ expect(screen.getByTestId('index')).toHaveTextContent('1')
59
+ media.remove()
60
+ }
61
+ rerender(<Harness open length={1} />)
62
+ fireEvent.keyDown(window, { key: 'ArrowRight' })
63
+ expect(screen.getByTestId('index')).toHaveTextContent('0')
64
+ rerender(<Harness open={false} length={2} />)
65
+ fireEvent.keyDown(window, { key: 'ArrowRight' })
66
+ expect(screen.getByTestId('index')).toHaveTextContent('0')
67
+ })
68
+ })
@@ -0,0 +1,16 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { VIEWER_ACTION_CLASS, VIEWER_NAV_CLASS } from './viewerChromeClasses'
4
+
5
+ describe('viewer chrome classes', () => {
6
+ it('keeps the load-bearing action button tokens', () => {
7
+ expect(VIEWER_ACTION_CLASS).toContain('size-10')
8
+ expect(VIEWER_ACTION_CLASS).toContain('rounded-full')
9
+ })
10
+
11
+ it('keeps the load-bearing navigation button tokens', () => {
12
+ expect(VIEWER_NAV_CLASS).toContain('absolute')
13
+ expect(VIEWER_NAV_CLASS).toContain('size-12')
14
+ expect(VIEWER_NAV_CLASS).toContain('-translate-y-1/2')
15
+ })
16
+ })
package/src/styles.css CHANGED
@@ -120,10 +120,6 @@
120
120
  }
121
121
  }
122
122
 
123
- .messaging-channel-layout {
124
- --messaging-composer-fade-height: 2rem;
125
- }
126
-
127
123
  .messaging-composer-chrome--floating {
128
124
  position: relative;
129
125
  right: 0;
@@ -138,21 +134,10 @@
138
134
 
139
135
  .messaging-composer-chrome--floating .messaging-composer-backdrop {
140
136
  position: absolute;
141
- top: calc(-1 * var(--messaging-composer-fade-height));
142
- right: 0;
143
- bottom: 0;
144
- left: 0;
137
+ inset: 0;
145
138
  overflow: hidden;
146
139
  isolation: isolate;
147
140
  pointer-events: none;
148
- /* Frosted band height, from the backdrop's top edge down to the composer
149
- pill: the fade-height above the chrome plus the chrome's own top padding
150
- (p-4 = 1rem). Blur/gradient stops are anchored to this fixed band so the
151
- frosting stays in the gap above the composer and does not scale with the
152
- (opaque) composer height. */
153
- --messaging-composer-fade-band: calc(
154
- var(--messaging-composer-fade-height) + 1rem
155
- );
156
141
  }
157
142
 
158
143
  .messaging-composer-backdrop-blur {
@@ -161,99 +146,71 @@
161
146
  pointer-events: none;
162
147
  }
163
148
 
149
+ /* Figma progressive background blur: (50%, 0%) → (50%, 100%), 0 → 16.
150
+ CSS has no spatially-varying blur, so five evenly spaced masked layers
151
+ approximate the linear ramp across the full parent height. */
164
152
  .messaging-composer-backdrop-blur:nth-child(1) {
165
- backdrop-filter: blur(2px);
166
- -webkit-backdrop-filter: blur(2px);
167
- mask: linear-gradient(
168
- to bottom,
169
- transparent 0,
170
- black calc(var(--messaging-composer-fade-band) * 0.12),
171
- black calc(var(--messaging-composer-fade-band) * 0.32),
172
- transparent calc(var(--messaging-composer-fade-band) * 0.5)
173
- );
174
- -webkit-mask: linear-gradient(
153
+ --messaging-composer-blur-mask: linear-gradient(
175
154
  to bottom,
176
- transparent 0,
177
- black calc(var(--messaging-composer-fade-band) * 0.12),
178
- black calc(var(--messaging-composer-fade-band) * 0.32),
179
- transparent calc(var(--messaging-composer-fade-band) * 0.5)
155
+ transparent 0%,
156
+ black 20%,
157
+ transparent 40%
180
158
  );
159
+ backdrop-filter: blur(3.2px);
160
+ -webkit-backdrop-filter: blur(3.2px);
161
+ mask: var(--messaging-composer-blur-mask);
162
+ -webkit-mask: var(--messaging-composer-blur-mask);
181
163
  }
182
164
 
183
165
  .messaging-composer-backdrop-blur:nth-child(2) {
184
- backdrop-filter: blur(4px);
185
- -webkit-backdrop-filter: blur(4px);
186
- mask: linear-gradient(
187
- to bottom,
188
- transparent calc(var(--messaging-composer-fade-band) * 0.2),
189
- black calc(var(--messaging-composer-fade-band) * 0.38),
190
- black calc(var(--messaging-composer-fade-band) * 0.55),
191
- transparent calc(var(--messaging-composer-fade-band) * 0.7)
192
- );
193
- -webkit-mask: linear-gradient(
166
+ --messaging-composer-blur-mask: linear-gradient(
194
167
  to bottom,
195
- transparent calc(var(--messaging-composer-fade-band) * 0.2),
196
- black calc(var(--messaging-composer-fade-band) * 0.38),
197
- black calc(var(--messaging-composer-fade-band) * 0.55),
198
- transparent calc(var(--messaging-composer-fade-band) * 0.7)
168
+ transparent 20%,
169
+ black 40%,
170
+ transparent 60%
199
171
  );
172
+ backdrop-filter: blur(6.4px);
173
+ -webkit-backdrop-filter: blur(6.4px);
174
+ mask: var(--messaging-composer-blur-mask);
175
+ -webkit-mask: var(--messaging-composer-blur-mask);
200
176
  }
201
177
 
202
178
  .messaging-composer-backdrop-blur:nth-child(3) {
203
- backdrop-filter: blur(8px);
204
- -webkit-backdrop-filter: blur(8px);
205
- mask: linear-gradient(
179
+ --messaging-composer-blur-mask: linear-gradient(
206
180
  to bottom,
207
- transparent calc(var(--messaging-composer-fade-band) * 0.4),
208
- black calc(var(--messaging-composer-fade-band) * 0.58),
209
- black calc(var(--messaging-composer-fade-band) * 0.72),
210
- transparent calc(var(--messaging-composer-fade-band) * 0.85)
211
- );
212
- -webkit-mask: linear-gradient(
213
- to bottom,
214
- transparent calc(var(--messaging-composer-fade-band) * 0.4),
215
- black calc(var(--messaging-composer-fade-band) * 0.58),
216
- black calc(var(--messaging-composer-fade-band) * 0.72),
217
- transparent calc(var(--messaging-composer-fade-band) * 0.85)
181
+ transparent 40%,
182
+ black 60%,
183
+ transparent 80%
218
184
  );
185
+ backdrop-filter: blur(9.6px);
186
+ -webkit-backdrop-filter: blur(9.6px);
187
+ mask: var(--messaging-composer-blur-mask);
188
+ -webkit-mask: var(--messaging-composer-blur-mask);
219
189
  }
220
190
 
221
191
  .messaging-composer-backdrop-blur:nth-child(4) {
222
- backdrop-filter: blur(12px);
223
- -webkit-backdrop-filter: blur(12px);
224
- mask: linear-gradient(
225
- to bottom,
226
- transparent calc(var(--messaging-composer-fade-band) * 0.55),
227
- black calc(var(--messaging-composer-fade-band) * 0.72),
228
- black calc(var(--messaging-composer-fade-band) * 0.88),
229
- transparent var(--messaging-composer-fade-band)
230
- );
231
- -webkit-mask: linear-gradient(
192
+ --messaging-composer-blur-mask: linear-gradient(
232
193
  to bottom,
233
- transparent calc(var(--messaging-composer-fade-band) * 0.55),
234
- black calc(var(--messaging-composer-fade-band) * 0.72),
235
- black calc(var(--messaging-composer-fade-band) * 0.88),
236
- transparent var(--messaging-composer-fade-band)
194
+ transparent 60%,
195
+ black 80%,
196
+ transparent 100%
237
197
  );
198
+ backdrop-filter: blur(12.8px);
199
+ -webkit-backdrop-filter: blur(12.8px);
200
+ mask: var(--messaging-composer-blur-mask);
201
+ -webkit-mask: var(--messaging-composer-blur-mask);
238
202
  }
239
203
 
240
204
  .messaging-composer-backdrop-blur:nth-child(5) {
241
- backdrop-filter: blur(16px);
242
- -webkit-backdrop-filter: blur(16px);
243
- mask: linear-gradient(
205
+ --messaging-composer-blur-mask: linear-gradient(
244
206
  to bottom,
245
- transparent calc(var(--messaging-composer-fade-band) * 0.7),
246
- black calc(var(--messaging-composer-fade-band) * 0.9),
247
- black var(--messaging-composer-fade-band),
248
- transparent var(--messaging-composer-fade-band)
249
- );
250
- -webkit-mask: linear-gradient(
251
- to bottom,
252
- transparent calc(var(--messaging-composer-fade-band) * 0.7),
253
- black calc(var(--messaging-composer-fade-band) * 0.9),
254
- black var(--messaging-composer-fade-band),
255
- transparent var(--messaging-composer-fade-band)
207
+ transparent 80%,
208
+ black 100%
256
209
  );
210
+ backdrop-filter: blur(16px);
211
+ -webkit-backdrop-filter: blur(16px);
212
+ mask: var(--messaging-composer-blur-mask);
213
+ -webkit-mask: var(--messaging-composer-blur-mask);
257
214
  }
258
215
 
259
216
  .messaging-composer-backdrop-gradient {
@@ -261,9 +218,9 @@
261
218
  inset: 0;
262
219
  pointer-events: none;
263
220
  background: linear-gradient(
264
- 180deg,
265
- rgba(251, 250, 249, 0) 0,
266
- #fbfaf9 var(--messaging-composer-fade-band)
221
+ to bottom,
222
+ rgba(251, 250, 249, 0) 0%,
223
+ #fbfaf9 100%
267
224
  );
268
225
  }
269
226