@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,189 @@
1
+ import React from 'react'
2
+
3
+ import Bubble from '../_shared/Bubble'
4
+ import DismissButton from '../_shared/DismissButton'
5
+ import {
6
+ bubbleVariantForState,
7
+ type AudioItem,
8
+ type ComposerExtras,
9
+ type MediaPreloadMode,
10
+ type MessageAttachmentBaseProps,
11
+ type MessageAttachmentState,
12
+ } from '../types'
13
+
14
+ export interface AudioAttachmentSharedProps extends MessageAttachmentBaseProps {
15
+ /** Audio source URL (`mp3`, `aac`, `wav`, …). */
16
+ src?: string
17
+ /** MIME type hint — typed onto the inline `<source>` element. */
18
+ mimeType?: string
19
+ /**
20
+ * Filename — used as the download default name (consumed by the
21
+ * native player's kebab menu). The HTML `<audio>` element doesn't
22
+ * expose a built-in title slot, so we don't render the filename
23
+ * inside the bubble itself.
24
+ */
25
+ filename?: string
26
+ /**
27
+ * Stacked audio. Takes precedence over `src` when set. Each item
28
+ * renders its own native `<audio controls>` player, vertically
29
+ * stacked inside the same bubble with an 8px gap between players.
30
+ * Sent + Received only — the composer surface accepts a single
31
+ * attachment at a time.
32
+ */
33
+ items?: AudioItem[]
34
+ /**
35
+ * `<audio preload>` hint applied to every player on the bubble.
36
+ * When omitted, the default depends on the rendered shape:
37
+ *
38
+ * - Single audio (no `items`, or `items.length === 1`) →
39
+ * `'metadata'` so the native player surfaces duration
40
+ * immediately.
41
+ * - Stacked audio (`items.length > 1`) → `'none'` so a thread
42
+ * of voice memos doesn't fan out N parallel metadata requests
43
+ * on first paint.
44
+ *
45
+ * Per-track overrides live on `AudioItem.preload`.
46
+ */
47
+ preload?: MediaPreloadMode
48
+ }
49
+
50
+ const resolveItems = ({
51
+ src,
52
+ mimeType,
53
+ filename,
54
+ items,
55
+ }: {
56
+ src?: string
57
+ mimeType?: string
58
+ filename?: string
59
+ items?: AudioItem[]
60
+ }): AudioItem[] => {
61
+ if (items && items.length > 0) return items
62
+ if (src) return [{ src, mimeType, filename }]
63
+ return []
64
+ }
65
+
66
+ const NativeAudioPlayer: React.FC<{
67
+ item: AudioItem
68
+ preload: MediaPreloadMode
69
+ trailingAction?: React.ReactNode
70
+ }> = ({ item, preload, trailingAction }) => (
71
+ <div className="flex items-center gap-2">
72
+ {/* No `<track>` is rendered — we don't author caption sidecars
73
+ for chat attachments, and an empty `<track kind="captions" />`
74
+ emits a runtime warning. Re-add a real track (with `src`,
75
+ `srcLang`, and `default`) once caption support actually ships.
76
+ The `jsx-a11y/media-has-caption` rule is suppressed for the
77
+ same reason. */}
78
+ {/* eslint-disable-next-line jsx-a11y/media-has-caption */}
79
+ <audio
80
+ src={item.src}
81
+ controls
82
+ preload={item.preload ?? preload}
83
+ className="block min-w-0 flex-1"
84
+ >
85
+ {item.mimeType ? <source src={item.src} type={item.mimeType} /> : null}
86
+ </audio>
87
+ {trailingAction ?? null}
88
+ </div>
89
+ )
90
+
91
+ interface InternalAudioRowProps extends AudioAttachmentSharedProps {
92
+ state: MessageAttachmentState
93
+ onDismiss?: () => void
94
+ }
95
+
96
+ const AudioAttachmentRow: React.FC<InternalAudioRowProps> = ({
97
+ state,
98
+ src,
99
+ mimeType,
100
+ filename,
101
+ items,
102
+ text,
103
+ groupPosition,
104
+ preload,
105
+ onDismiss,
106
+ }) => {
107
+ const variant = bubbleVariantForState(state)
108
+ const showDismiss = state === 'composer' && !!onDismiss
109
+ const resolvedItems = resolveItems({ src, mimeType, filename, items })
110
+
111
+ if (resolvedItems.length === 0) {
112
+ return null
113
+ }
114
+
115
+ // Default rule: surface duration immediately for a single player,
116
+ // but avoid fanning out N metadata requests when the bubble is a
117
+ // thread of voice memos. An explicit `preload` prop overrides;
118
+ // per-track overrides live on `AudioItem.preload`.
119
+ const resolvedPreload: MediaPreloadMode =
120
+ preload ?? (resolvedItems.length > 1 ? 'none' : 'metadata')
121
+
122
+ return (
123
+ <Bubble
124
+ variant={variant}
125
+ text={text}
126
+ groupPosition={groupPosition}
127
+ data-testid="audio-attachment"
128
+ >
129
+ {/* Native `<audio controls>` already exposes a download in its
130
+ kebab menu, so we don't render a separate Download button.
131
+ The element also has no built-in title slot — the dismiss
132
+ button (Composer only) sits inline next to the player.
133
+ Stacked players get an 8px vertical gap so each track reads
134
+ as a discrete attachment. */}
135
+ <div className="flex flex-col gap-2">
136
+ {resolvedItems.map((item, index) => (
137
+ <NativeAudioPlayer
138
+ key={`${item.src}-${index}`}
139
+ item={item}
140
+ preload={resolvedPreload}
141
+ trailingAction={
142
+ // Composer only supports a single attachment, so the
143
+ // dismiss control sits on the only player.
144
+ showDismiss && index === 0 ? (
145
+ <DismissButton onClick={onDismiss!} variant="inline" />
146
+ ) : undefined
147
+ }
148
+ />
149
+ ))}
150
+ </div>
151
+ </Bubble>
152
+ )
153
+ }
154
+
155
+ /**
156
+ * Composer-only props. Single audio (`src`) only — the composer
157
+ * surface accepts a single attachment at a time, so `items` is not
158
+ * supported. Captions (`text`) and `groupPosition` are also dropped:
159
+ * the composer renders a standalone draft, not part of a same-author
160
+ * run, and captions live in the message-input textarea, not inside
161
+ * the draft attachment preview.
162
+ */
163
+ export interface AudioComposerProps extends ComposerExtras {
164
+ src?: string
165
+ mimeType?: string
166
+ filename?: string
167
+ /** See `AudioAttachmentSharedProps.preload`. */
168
+ preload?: MediaPreloadMode
169
+ }
170
+ export type AudioSentProps = AudioAttachmentSharedProps
171
+ export type AudioReceivedProps = AudioAttachmentSharedProps
172
+
173
+ const AudioComposer: React.FC<AudioComposerProps> = (props) => (
174
+ <AudioAttachmentRow {...props} state="composer" />
175
+ )
176
+ const AudioSent: React.FC<AudioSentProps> = (props) => (
177
+ <AudioAttachmentRow {...props} state="sent" />
178
+ )
179
+ const AudioReceived: React.FC<AudioReceivedProps> = (props) => (
180
+ <AudioAttachmentRow {...props} state="received" />
181
+ )
182
+
183
+ const AudioAttachment = {
184
+ Composer: AudioComposer,
185
+ Sent: AudioSent,
186
+ Received: AudioReceived,
187
+ }
188
+
189
+ export default AudioAttachment
@@ -0,0 +1,352 @@
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 { FileItem } from '../types'
11
+
12
+ import FileAttachment from '.'
13
+
14
+ const FILE_SRC = 'https://www.w3.org/WAI/WCAG21/wcag21.pdf'
15
+
16
+ const STACK_FILES: FileItem[] = [
17
+ {
18
+ src: FILE_SRC,
19
+ filename: 'workout-plan.zip',
20
+ mimeType: 'application/zip',
21
+ fileSize: 2_457_600,
22
+ },
23
+ {
24
+ src: FILE_SRC,
25
+ filename: 'Q4-revenue.xlsx',
26
+ mimeType:
27
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
28
+ fileSize: 146_432,
29
+ },
30
+ {
31
+ src: FILE_SRC,
32
+ filename: 'brand-guidelines-2026.zip',
33
+ mimeType: 'application/zip',
34
+ fileSize: 11_500_000,
35
+ },
36
+ {
37
+ src: FILE_SRC,
38
+ filename: 'meeting-notes.docx',
39
+ mimeType:
40
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
41
+ fileSize: 28_672,
42
+ },
43
+ ]
44
+
45
+ const meta: Meta = {
46
+ title: 'MessageAttachment/File',
47
+ parameters: { layout: 'fullscreen' },
48
+ }
49
+ export default meta
50
+
51
+ const handleDismiss = () => {
52
+ // eslint-disable-next-line no-console
53
+ console.log('Dismissed file attachment')
54
+ }
55
+
56
+ /**
57
+ * Compact file row inside a chat bubble — typed icon + filename +
58
+ * `EXT · SIZE` — with a download icon in the trailing slot. Clicking
59
+ * any state (Composer / Sent / Received) downloads the asset.
60
+ */
61
+ export const Single: StoryFn = () => (
62
+ <StoryPage>
63
+ <StoryHeading
64
+ title="File attachment"
65
+ description="Click any bubble to download. Trailing icon hints at the download action."
66
+ />
67
+ <StoryGrid>
68
+ <StoryRow
69
+ label="Generic"
70
+ composer={
71
+ <FileAttachment.Composer
72
+ src={FILE_SRC}
73
+ filename="workout-plan.zip"
74
+ mimeType="application/zip"
75
+ fileSize={2_457_600}
76
+ onDismiss={handleDismiss}
77
+ />
78
+ }
79
+ sent={
80
+ <FileAttachment.Sent
81
+ src={FILE_SRC}
82
+ filename="workout-plan.zip"
83
+ mimeType="application/zip"
84
+ fileSize={2_457_600}
85
+ />
86
+ }
87
+ received={
88
+ <FileAttachment.Received
89
+ src={FILE_SRC}
90
+ filename="workout-plan.zip"
91
+ mimeType="application/zip"
92
+ fileSize={2_457_600}
93
+ />
94
+ }
95
+ />
96
+ <StoryRow
97
+ label="Spreadsheet"
98
+ composer={
99
+ <FileAttachment.Composer
100
+ src={FILE_SRC}
101
+ filename="Q4-revenue.xlsx"
102
+ mimeType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
103
+ fileSize={146_432}
104
+ onDismiss={handleDismiss}
105
+ />
106
+ }
107
+ sent={
108
+ <FileAttachment.Sent
109
+ src={FILE_SRC}
110
+ filename="Q4-revenue.xlsx"
111
+ mimeType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
112
+ fileSize={146_432}
113
+ />
114
+ }
115
+ received={
116
+ <FileAttachment.Received
117
+ src={FILE_SRC}
118
+ filename="Q4-revenue.xlsx"
119
+ mimeType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
120
+ fileSize={146_432}
121
+ />
122
+ }
123
+ />
124
+ <StoryRow
125
+ label="Long filename"
126
+ composer={
127
+ <FileAttachment.Composer
128
+ src={FILE_SRC}
129
+ filename="brand-guidelines-2026-final-revision-with-color-tokens.zip"
130
+ mimeType="application/zip"
131
+ fileSize={11_500_000}
132
+ onDismiss={handleDismiss}
133
+ />
134
+ }
135
+ sent={
136
+ <FileAttachment.Sent
137
+ src={FILE_SRC}
138
+ filename="brand-guidelines-2026-final-revision-with-color-tokens.zip"
139
+ mimeType="application/zip"
140
+ fileSize={11_500_000}
141
+ />
142
+ }
143
+ received={
144
+ <FileAttachment.Received
145
+ src={FILE_SRC}
146
+ filename="brand-guidelines-2026-final-revision-with-color-tokens.zip"
147
+ mimeType="application/zip"
148
+ fileSize={11_500_000}
149
+ />
150
+ }
151
+ />
152
+ </StoryGrid>
153
+ </StoryPage>
154
+ )
155
+
156
+ export const SingleWithText: StoryFn = () => (
157
+ <StoryPage>
158
+ <StoryHeading
159
+ title="File with caption"
160
+ description="Caption sits inside the same bubble below the file row."
161
+ />
162
+ <StoryGrid>
163
+ <StoryRow
164
+ label="Caption"
165
+ composer={
166
+ <FileAttachment.Composer
167
+ src={FILE_SRC}
168
+ filename="workout-plan.zip"
169
+ mimeType="application/zip"
170
+ fileSize={2_457_600}
171
+ onDismiss={handleDismiss}
172
+ />
173
+ }
174
+ sent={
175
+ <FileAttachment.Sent
176
+ src={FILE_SRC}
177
+ filename="workout-plan.zip"
178
+ mimeType="application/zip"
179
+ fileSize={2_457_600}
180
+ text="Here is the file"
181
+ />
182
+ }
183
+ received={
184
+ <FileAttachment.Received
185
+ src={FILE_SRC}
186
+ filename="workout-plan.zip"
187
+ mimeType="application/zip"
188
+ fileSize={2_457_600}
189
+ text="Here is the file"
190
+ />
191
+ }
192
+ />
193
+ </StoryGrid>
194
+ </StoryPage>
195
+ )
196
+
197
+ /**
198
+ * Stacked file attachments — multiple files inside one bubble. Each
199
+ * row has its own download trigger, and rows are separated by an 8px
200
+ * gap to match stacked PDF / Audio.
201
+ *
202
+ * Sent + Received only — the composer surface accepts a single
203
+ * attachment at a time.
204
+ */
205
+ export const Stacked: StoryFn = () => (
206
+ <StoryPage>
207
+ <StoryHeading
208
+ title="Stacked files"
209
+ description="Each row downloads independently. Composer column is intentionally blank — the composer accepts a single attachment at a time."
210
+ />
211
+ <StoryGrid>
212
+ <StoryRow
213
+ label="2 files"
214
+ composer={
215
+ <FileAttachment.Composer
216
+ src={FILE_SRC}
217
+ filename="workout-plan.zip"
218
+ mimeType="application/zip"
219
+ fileSize={2_457_600}
220
+ onDismiss={handleDismiss}
221
+ />
222
+ }
223
+ sent={<FileAttachment.Sent items={STACK_FILES.slice(0, 2)} />}
224
+ received={<FileAttachment.Received items={STACK_FILES.slice(0, 2)} />}
225
+ />
226
+ <StoryRow
227
+ label="3 files"
228
+ composer={
229
+ <FileAttachment.Composer
230
+ src={FILE_SRC}
231
+ filename="workout-plan.zip"
232
+ mimeType="application/zip"
233
+ fileSize={2_457_600}
234
+ onDismiss={handleDismiss}
235
+ />
236
+ }
237
+ sent={<FileAttachment.Sent items={STACK_FILES.slice(0, 3)} />}
238
+ received={<FileAttachment.Received items={STACK_FILES.slice(0, 3)} />}
239
+ />
240
+ <StoryRow
241
+ label="4 files"
242
+ composer={
243
+ <FileAttachment.Composer
244
+ src={FILE_SRC}
245
+ filename="workout-plan.zip"
246
+ mimeType="application/zip"
247
+ fileSize={2_457_600}
248
+ onDismiss={handleDismiss}
249
+ />
250
+ }
251
+ sent={<FileAttachment.Sent items={STACK_FILES} />}
252
+ received={<FileAttachment.Received items={STACK_FILES} />}
253
+ />
254
+ </StoryGrid>
255
+ </StoryPage>
256
+ )
257
+
258
+ /**
259
+ * Bubble grouping — mirrors the corner-flattening stream-chat-react
260
+ * applies to text bubbles in a same-author run. The four positions
261
+ * (`single` / `first` / `middle` / `end`) flatten the corners that
262
+ * face the previous / next bubble in the cluster, so a `MessageAttachment`
263
+ * sitting inside a thread visually merges with the surrounding text
264
+ * bubbles instead of floating as its own ungrouped pill.
265
+ *
266
+ * Stack the four positions vertically with `gap-[2px]` to see them
267
+ * snap into a single visual run — the same gap stream uses between
268
+ * grouped messages.
269
+ */
270
+ export const Grouping: StoryFn = () => (
271
+ <StoryPage>
272
+ <StoryHeading
273
+ title="Bubble grouping"
274
+ description="Pass `groupPosition='single' | 'first' | 'middle' | 'end'` (or use `bubbleGroupPositionFromStream({ firstOfGroup, endOfGroup, groupedByUser })`) to make the attachment join a same-author message run. Sender bubbles flatten the corners on their right edge; receiver bubbles mirror onto the left."
275
+ />
276
+ <StoryGrid>
277
+ <StoryRow
278
+ label="single"
279
+ sent={<FileAttachment.Sent {...STACK_FILES[0]} groupPosition="single" />}
280
+ received={
281
+ <FileAttachment.Received {...STACK_FILES[0]} groupPosition="single" />
282
+ }
283
+ />
284
+ <StoryRow
285
+ label="run of 3"
286
+ sent={
287
+ <div className="flex flex-col gap-[2px]">
288
+ <FileAttachment.Sent {...STACK_FILES[0]} groupPosition="first" />
289
+ <FileAttachment.Sent {...STACK_FILES[1]} groupPosition="middle" />
290
+ <FileAttachment.Sent {...STACK_FILES[2]} groupPosition="end" />
291
+ </div>
292
+ }
293
+ received={
294
+ <div className="flex flex-col gap-[2px]">
295
+ <FileAttachment.Received
296
+ {...STACK_FILES[0]}
297
+ groupPosition="first"
298
+ />
299
+ <FileAttachment.Received
300
+ {...STACK_FILES[1]}
301
+ groupPosition="middle"
302
+ />
303
+ <FileAttachment.Received {...STACK_FILES[2]} groupPosition="end" />
304
+ </div>
305
+ }
306
+ />
307
+ </StoryGrid>
308
+ </StoryPage>
309
+ )
310
+
311
+ /** Stacked files with an accompanying caption.
312
+ * Sent + Received only — see `Stacked` for the rationale. */
313
+ export const StackedWithText: StoryFn = () => (
314
+ <StoryPage>
315
+ <StoryHeading
316
+ title="Stacked files with caption"
317
+ description="Caption sits inside the same bubble, below the file stack. Composer column is intentionally blank — the composer accepts a single attachment at a time."
318
+ />
319
+ <StoryGrid>
320
+ <StoryRow
321
+ label="3 files + caption"
322
+ sent={
323
+ <FileAttachment.Sent
324
+ items={STACK_FILES.slice(0, 3)}
325
+ text="Here are the deliverables — let me know if anything's missing."
326
+ />
327
+ }
328
+ received={
329
+ <FileAttachment.Received
330
+ items={STACK_FILES.slice(0, 3)}
331
+ text="Here are the deliverables — let me know if anything's missing."
332
+ />
333
+ }
334
+ />
335
+ <StoryRow
336
+ label="4 files + caption"
337
+ sent={
338
+ <FileAttachment.Sent
339
+ items={STACK_FILES}
340
+ text="Final pass for review."
341
+ />
342
+ }
343
+ received={
344
+ <FileAttachment.Received
345
+ items={STACK_FILES}
346
+ text="Final pass for review."
347
+ />
348
+ }
349
+ />
350
+ </StoryGrid>
351
+ </StoryPage>
352
+ )