@adatechnology/conversations-ui 0.1.0-rc.2 → 0.1.0-rc.21

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 (106) hide show
  1. package/dist/{chunk-ZDURDZTM.js → chunk-2AYDBWNE.js} +14 -0
  2. package/dist/chunk-TV4OQRGH.js +2187 -0
  3. package/dist/flows/index.d.ts +14 -2
  4. package/dist/flows/index.js +117 -39
  5. package/dist/index.d.ts +862 -136
  6. package/dist/index.js +1746 -1124
  7. package/dist/preview/index.d.ts +470 -0
  8. package/dist/preview/index.js +1329 -0
  9. package/dist/styles.css +228 -0
  10. package/dist/types-C6A_9edv.d.ts +456 -0
  11. package/package.json +10 -3
  12. package/src/AudioRecorderButton.test.tsx +30 -0
  13. package/src/AudioRecorderButton.tsx +248 -0
  14. package/src/AudioTranscription.test.tsx +115 -0
  15. package/src/AudioTranscription.tsx +249 -0
  16. package/src/Avatar.tsx +30 -4
  17. package/src/ChannelIcon.tsx +87 -0
  18. package/src/ConversationContextPanel.tsx +280 -0
  19. package/src/ConversationDocumentsPanel.tsx +425 -0
  20. package/src/ConversationHeader.tsx +257 -0
  21. package/src/ConversationListItem.tsx +54 -7
  22. package/src/ConversationLocalesProvider.tsx +44 -0
  23. package/src/ConversationRow.tsx +137 -0
  24. package/src/DateDivider.tsx +16 -3
  25. package/src/DocumentsLibrary.tsx +322 -0
  26. package/src/EmojiPicker.tsx +69 -55
  27. package/src/FileIcon.test.ts +83 -0
  28. package/src/FileIcon.tsx +88 -11
  29. package/src/InteractiveMessage.test.tsx +41 -0
  30. package/src/InteractiveMessage.tsx +143 -0
  31. package/src/Lightbox.tsx +18 -3
  32. package/src/MediaRenderer.tsx +96 -22
  33. package/src/MessageBubble.tsx +77 -5
  34. package/src/MessageComposer.test.tsx +35 -0
  35. package/src/MessageComposer.tsx +165 -19
  36. package/src/RichMessageComposer.test.tsx +83 -0
  37. package/src/RichMessageComposer.tsx +380 -0
  38. package/src/Wallpaper.test.tsx +21 -0
  39. package/src/Wallpaper.tsx +69 -7
  40. package/src/WhatsAppMessageEditor.tsx +28 -4
  41. package/src/WindowExpiredNotice.tsx +57 -0
  42. package/src/audioRecorderFormat.test.ts +67 -0
  43. package/src/conversationChannel.test.ts +53 -0
  44. package/src/conversationChannel.ts +146 -0
  45. package/src/conversationTranscript.test.ts +122 -0
  46. package/src/conversationTranscript.ts +89 -0
  47. package/src/conversationWindow.test.ts +90 -0
  48. package/src/conversationWindow.ts +78 -0
  49. package/src/documentTypeLabel.test.ts +57 -0
  50. package/src/emojiCatalog.test.ts +35 -0
  51. package/src/emojiCatalog.ts +189 -0
  52. package/src/flows/FlowGroupHeader.tsx +12 -2
  53. package/src/flows/FlowMapCanvas.tsx +17 -14
  54. package/src/flows/FlowMapNode.tsx +3 -1
  55. package/src/flows/FlowNodeCard.tsx +22 -4
  56. package/src/flows/FlowNodePanel.tsx +132 -35
  57. package/src/flows/FlowPalette.tsx +6 -2
  58. package/src/flows/FlowWhatsAppPreview.tsx +14 -3
  59. package/src/flows/flowGraph.ts +5 -5
  60. package/src/flows/labels.ts +5 -0
  61. package/src/hooks/useConversationActions.ts +56 -0
  62. package/src/hooks/useConversationDocuments.ts +15 -9
  63. package/src/hooks/useConversationList.ts +15 -9
  64. package/src/hooks/useConversationMessages.ts +2 -2
  65. package/src/hooks/useScrollToLatestMessage.ts +127 -0
  66. package/src/index.ts +140 -16
  67. package/src/lib/cn.test.ts +29 -0
  68. package/src/lib/cn.ts +15 -0
  69. package/src/lib/createMediaUrlResolver.ts +33 -0
  70. package/src/lib/paginated.test.ts +33 -0
  71. package/src/lib/paginated.ts +26 -0
  72. package/src/lib/phone.ts +34 -0
  73. package/src/preview/ConversationPreview.tsx +334 -0
  74. package/src/preview/MediaTypesPreview.tsx +87 -0
  75. package/src/preview/conversationPreviewFailures.test.ts +64 -0
  76. package/src/preview/createMockConversationsApi.ts +271 -0
  77. package/src/preview/createMockSSEProvider.ts +40 -0
  78. package/src/preview/createPreviewBridgeClient.test.ts +92 -0
  79. package/src/preview/createPreviewBridgeClient.ts +124 -0
  80. package/src/preview/createPreviewMediaUploader.ts +82 -0
  81. package/src/preview/createPreviewWebhookClient.test.ts +194 -0
  82. package/src/preview/createPreviewWebhookClient.ts +222 -0
  83. package/src/preview/index.ts +67 -0
  84. package/src/preview/mediaTypeOf.test.ts +15 -0
  85. package/src/preview/mockDocumentsSearch.test.ts +57 -0
  86. package/src/preview/mockEventSource.ts +53 -0
  87. package/src/preview/preview.test.ts +177 -0
  88. package/src/preview/previewFileSamples.test.ts +151 -0
  89. package/src/preview/previewFileSamples.ts +74 -0
  90. package/src/preview/previewFixtures.ts +440 -0
  91. package/src/preview/previewMediaSource.test.ts +62 -0
  92. package/src/preview/previewMediaSource.ts +91 -0
  93. package/src/preview/previewMediaUploader.test.ts +61 -0
  94. package/src/preview/previewStore.ts +193 -0
  95. package/src/preview/startPreviewScript.ts +60 -0
  96. package/src/providers/types.ts +175 -12
  97. package/src/quickReply.test.ts +58 -0
  98. package/src/settings/TranscriptionSettingsForm.test.tsx +81 -0
  99. package/src/settings/TranscriptionSettingsForm.tsx +189 -0
  100. package/src/settings/WhatsAppCreateTemplateForm.tsx +3 -1
  101. package/src/settings/WhatsAppTemplatesSettings.tsx +106 -0
  102. package/src/styles.css +173 -0
  103. package/src/types.ts +72 -1
  104. package/src/useDarkMode.ts +26 -0
  105. package/src/useIsNarrow.ts +29 -0
  106. package/src/useWaitingNotifications.ts +74 -29
@@ -0,0 +1,177 @@
1
+ import { describe, expect, it } from 'bun:test'
2
+ import { createMockConversationsApi } from './createMockConversationsApi'
3
+ import { createMockSSEProvider } from './createMockSSEProvider'
4
+ import { PREVIEW_CONVERSATIONS, PREVIEW_MESSAGES } from './previewFixtures'
5
+ import { createPreviewStore, type PreviewStore } from './previewStore'
6
+ import { DEFAULT_PREVIEW_SCRIPT } from './startPreviewScript'
7
+
8
+ function createStore(): PreviewStore {
9
+ return createPreviewStore({ conversations: PREVIEW_CONVERSATIONS, messages: PREVIEW_MESSAGES })
10
+ }
11
+
12
+ const WAITING_CONVERSATION_ID = '5511977776666'
13
+ const BOT_CONVERSATION_ID = '5511988887777'
14
+
15
+ describe('previewStore', () => {
16
+ it('filtra a fila de espera humana', () => {
17
+ const store = createStore()
18
+
19
+ const waiting = store.listConversations({ waitingHuman: true })
20
+
21
+ expect(waiting.map((conversation) => conversation.id)).toEqual([WAITING_CONVERSATION_ID])
22
+ })
23
+
24
+ it('busca por nome e por número', () => {
25
+ const store = createStore()
26
+
27
+ expect(store.listConversations({ search: 'marina' })).toHaveLength(1)
28
+ expect(store.listConversations({ search: '9777' })).toHaveLength(1)
29
+ })
30
+
31
+ it('assumir a conversa apaga a espera por humano', () => {
32
+ const store = createStore()
33
+
34
+ store.setMode({ conversationId: WAITING_CONVERSATION_ID, mode: 'human', assignedUserId: 'agent-2' })
35
+ const conversation = store.listConversations().find((item) => item.id === WAITING_CONVERSATION_ID)
36
+
37
+ expect(conversation?.mode).toBe('human')
38
+ expect(conversation?.assignedUserId).toBe('agent-2')
39
+ expect(conversation?.waitingHuman).toBe(false)
40
+ })
41
+
42
+ it('mensagem recebida incrementa não-lidas e markRead zera', () => {
43
+ const store = createStore()
44
+ const before = store.listConversations().find((item) => item.id === BOT_CONVERSATION_ID)?.unread ?? 0
45
+
46
+ store.appendMessage({
47
+ conversationId: BOT_CONVERSATION_ID,
48
+ content: 'e o troco?',
49
+ direction: 'inbound',
50
+ sender: 'customer',
51
+ })
52
+ const after = store.listConversations().find((item) => item.id === BOT_CONVERSATION_ID)?.unread
53
+
54
+ expect(after).toBe(before + 1)
55
+
56
+ store.markRead(BOT_CONVERSATION_ID)
57
+ expect(store.listConversations().find((item) => item.id === BOT_CONVERSATION_ID)?.unread).toBe(0)
58
+ })
59
+ })
60
+
61
+ describe('createMockSSEProvider', () => {
62
+ // Paridade com o servidor: o evento `message` é ping, não entrega de dados. Se este teste
63
+ // começar a exigir `content`, o mock passou a mentir sobre o que a produção envia.
64
+ it('entrega evento message como ping, sem conteúdo, com data serializada', () => {
65
+ const store = createStore()
66
+ const sse = createMockSSEProvider({ store })
67
+ const source = sse.connectConversationStream(BOT_CONVERSATION_ID)
68
+ const received: string[] = []
69
+
70
+ source.addEventListener('message', (event) => received.push(event.data as string))
71
+ store.appendMessage({
72
+ conversationId: BOT_CONVERSATION_ID,
73
+ content: 'oi',
74
+ direction: 'inbound',
75
+ sender: 'customer',
76
+ })
77
+
78
+ expect(received).toHaveLength(1)
79
+ expect(JSON.parse(received[0] ?? '{}')).toEqual({ direction: 'inbound', sender: 'customer' })
80
+ })
81
+
82
+ it('emite mode-changed e data-changed nos canais certos', () => {
83
+ const store = createStore()
84
+ const sse = createMockSSEProvider({ store })
85
+ const conversationEvents: string[] = []
86
+ const globalEvents: string[] = []
87
+
88
+ sse
89
+ .connectConversationStream(WAITING_CONVERSATION_ID)
90
+ .addEventListener('mode-changed', () => conversationEvents.push('mode-changed'))
91
+ sse.connectGlobalStream().addEventListener('data-changed', () => globalEvents.push('data-changed'))
92
+
93
+ store.setMode({ conversationId: WAITING_CONVERSATION_ID, mode: 'human', assignedUserId: 'agent-2' })
94
+
95
+ expect(conversationEvents).toEqual(['mode-changed'])
96
+ expect(globalEvents).toEqual(['data-changed'])
97
+ })
98
+
99
+ // A regressão que isto tranca: sem desassinar do store no close, cada remontagem de componente
100
+ // deixaria um listener preso e a mesma mensagem chegaria duplicada na UI.
101
+ it('desassina do store ao fechar o stream', () => {
102
+ const store = createStore()
103
+ const sse = createMockSSEProvider({ store })
104
+ const source = sse.connectConversationStream(BOT_CONVERSATION_ID)
105
+ const received: string[] = []
106
+
107
+ source.addEventListener('message', (event) => received.push(event.data as string))
108
+ source.close()
109
+ store.appendMessage({
110
+ conversationId: BOT_CONVERSATION_ID,
111
+ content: 'oi',
112
+ direction: 'inbound',
113
+ sender: 'customer',
114
+ })
115
+
116
+ expect(received).toHaveLength(0)
117
+ })
118
+ })
119
+
120
+ describe('createMockConversationsApi', () => {
121
+ // Modela o fluxo real: o ping avisa, o refetch traz o dado. É o compartilhamento de store que
122
+ // faz os dois concordarem — com estados separados, o ping anunciaria algo que a query não vê.
123
+ it('ping do SSE e refetch da API contam a mesma história', async () => {
124
+ const store = createStore()
125
+ const api = createMockConversationsApi({ store, latencyMs: 0 })
126
+ const sse = createMockSSEProvider({ store })
127
+ const pings: unknown[] = []
128
+
129
+ sse.connectConversationStream(BOT_CONVERSATION_ID).addEventListener('message', (event) => {
130
+ pings.push(JSON.parse(event.data as string))
131
+ })
132
+
133
+ await api.sendMessage(BOT_CONVERSATION_ID, 'já separei aqui')
134
+ const messages = await api.fetchMessages(BOT_CONVERSATION_ID)
135
+
136
+ expect(pings).toEqual([{ direction: 'outbound', sender: 'agent' }])
137
+ expect(messages.at(-1)?.content).toBe('já separei aqui')
138
+ expect(messages.at(-1)?.sender).toBe('agent')
139
+ })
140
+
141
+ it('pagina e filtra pela mesma regra do store', async () => {
142
+ const store = createStore()
143
+ const api = createMockConversationsApi({ store, latencyMs: 0 })
144
+
145
+ const firstPage = await api.fetchConversations({ page: 1, limit: 2 })
146
+ const waiting = await api.fetchConversations({ waitingHuman: true })
147
+
148
+ expect(firstPage.conversations).toHaveLength(2)
149
+ // O total conta o conjunto filtrado inteiro, não a fatia — é o que a paginação da UI lê.
150
+ expect(firstPage.total).toBeGreaterThan(2)
151
+ expect(waiting.conversations.every((conversation) => conversation.waitingHuman)).toBe(true)
152
+ })
153
+
154
+ it('markRead zera as não-lidas vistas pela lista', async () => {
155
+ const store = createStore()
156
+ const api = createMockConversationsApi({ store, latencyMs: 0 })
157
+
158
+ await api.markRead(BOT_CONVERSATION_ID)
159
+ const conversations = await api.fetchConversations()
160
+
161
+ expect(conversations.conversations.find((item) => item.id === BOT_CONVERSATION_ID)?.unread).toBe(0)
162
+ })
163
+ })
164
+
165
+ describe('roteiro padrão', () => {
166
+ // O roteiro é o que faz a inbox deixar de ser tela estática; se algum passo apontar para uma
167
+ // conversa que não existe nas fixtures, ele passa a ser no-op silencioso.
168
+ it('todos os passos alteram o estado das conversas das fixtures', () => {
169
+ const store = createStore()
170
+ const before = JSON.stringify(store.listConversations())
171
+
172
+ for (const step of DEFAULT_PREVIEW_SCRIPT) step(store)
173
+
174
+ expect(JSON.stringify(store.listConversations())).not.toBe(before)
175
+ expect(store.listMessages(WAITING_CONVERSATION_ID).at(-1)?.sender).toBe('agent')
176
+ })
177
+ })
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Guarda que o preview entrega arquivo do tipo que promete.
3
+ *
4
+ * O defeito que motivou o arquivo: `getDocumentUrl` devolvia sempre a mesma imagem PNG, então
5
+ * "visualizar" num PDF abria um PNG rotulado `application/pdf` e o leitor dizia que o arquivo era
6
+ * inválido. Aqui os bytes são decodificados e conferidos pela assinatura de cada formato — teste que
7
+ * só olhasse o prefixo da data URL passaria com o bug de volta.
8
+ */
9
+
10
+ import { describe, expect, it } from 'bun:test'
11
+
12
+ import { PREVIEW_FILE_SAMPLES, resolvePreviewFileSample } from './previewFileSamples'
13
+ import { PREVIEW_DOCUMENTS } from './previewFixtures'
14
+
15
+ function decode(dataUrl: string): { mimeType: string; bytes: Uint8Array } {
16
+ const [head, payload] = dataUrl.split(',')
17
+ const mimeType = head!.replace(/^data:/, '').replace(/;base64$/, '')
18
+ if (!head!.endsWith(';base64')) {
19
+ return { mimeType, bytes: new TextEncoder().encode(decodeURIComponent(payload!)) }
20
+ }
21
+ return { mimeType, bytes: Uint8Array.from(atob(payload!), (char) => char.charCodeAt(0)) }
22
+ }
23
+
24
+ function startsWith(bytes: Uint8Array, signature: readonly number[], offset = 0): boolean {
25
+ return signature.every((byte, index) => bytes[offset + index] === byte)
26
+ }
27
+
28
+ const ascii = (text: string): number[] => [...text].map((char) => char.charCodeAt(0))
29
+
30
+ // Assinatura real de cada formato, não o rótulo do mimeType.
31
+ const SIGNATURES: Record<string, (bytes: Uint8Array) => boolean> = {
32
+ 'application/pdf': (bytes) => startsWith(bytes, ascii('%PDF-')),
33
+ 'image/png': (bytes) => startsWith(bytes, [0x89, 0x50, 0x4e, 0x47]),
34
+ 'image/jpeg': (bytes) => startsWith(bytes, [0xff, 0xd8, 0xff]),
35
+ 'image/webp': (bytes) => startsWith(bytes, ascii('RIFF')) && startsWith(bytes, ascii('WEBP'), 8),
36
+ 'video/mp4': (bytes) => startsWith(bytes, ascii('ftyp'), 4),
37
+ 'audio/mp4': (bytes) => startsWith(bytes, ascii('ftyp'), 4),
38
+ // ID3 quando há tag, ou o sync do primeiro frame MPEG.
39
+ 'audio/mpeg': (bytes) => startsWith(bytes, ascii('ID3')) || (bytes[0] === 0xff && (bytes[1]! & 0xe0) === 0xe0),
40
+ 'audio/ogg': (bytes) => startsWith(bytes, ascii('OggS')),
41
+ 'audio/aac': (bytes) => bytes[0] === 0xff && (bytes[1]! & 0xf0) === 0xf0,
42
+ 'text/plain': (bytes) => bytes.length > 0,
43
+ 'text/csv': (bytes) => bytes.length > 0,
44
+ // Todo pacote Office é zip: assinatura PK\x03\x04.
45
+ 'application/zip': (bytes) => startsWith(bytes, [0x50, 0x4b, 0x03, 0x04]),
46
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': (bytes) =>
47
+ startsWith(bytes, [0x50, 0x4b, 0x03, 0x04]),
48
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': (bytes) =>
49
+ startsWith(bytes, [0x50, 0x4b, 0x03, 0x04]),
50
+ }
51
+
52
+ describe('amostras de arquivo do preview', () => {
53
+ it('toda amostra declara o mimeType que anuncia', () => {
54
+ for (const [mimeType, dataUrl] of Object.entries(PREVIEW_FILE_SAMPLES)) {
55
+ expect(decode(dataUrl).mimeType).toBe(mimeType)
56
+ }
57
+ })
58
+
59
+ for (const [mimeType, isValid] of Object.entries(SIGNATURES)) {
60
+ it(`os bytes de ${mimeType} são realmente desse formato`, () => {
61
+ const sample = PREVIEW_FILE_SAMPLES[mimeType]
62
+ expect(sample).toBeDefined()
63
+ expect(isValid(decode(sample!).bytes)).toBe(true)
64
+ })
65
+ }
66
+
67
+ // O PDF sem xref/startxref é o caso que alguns leitores recusam de cara.
68
+ it('o PDF tem tabela xref e termina em %%EOF', () => {
69
+ const text = new TextDecoder().decode(decode(PREVIEW_FILE_SAMPLES['application/pdf']!).bytes)
70
+
71
+ expect(text).toContain('\nxref\n')
72
+ expect(text).toContain('startxref')
73
+ expect(text.trimEnd().endsWith('%%EOF')).toBe(true)
74
+ })
75
+ })
76
+
77
+ describe('resolvePreviewFileSample', () => {
78
+ it('descarta parâmetro do mimeType — áudio de WhatsApp chega com codecs', () => {
79
+ expect(resolvePreviewFileSample('audio/ogg; codecs=opus')).toBe(PREVIEW_FILE_SAMPLES['audio/ogg'])
80
+ })
81
+
82
+ // Tipos aceitos pela Meta sem amostra local (sem encoder amr/3gp nesta máquina): tocam pela
83
+ // família, em vez de devolver bytes de outro formato.
84
+ it('cai na família quando não há amostra do tipo exato', () => {
85
+ expect(resolvePreviewFileSample('audio/amr')).toBe(PREVIEW_FILE_SAMPLES['audio/mpeg'])
86
+ expect(resolvePreviewFileSample('video/3gp')).toBe(PREVIEW_FILE_SAMPLES['video/mp4'])
87
+ expect(resolvePreviewFileSample('image/heic')).toBe(PREVIEW_FILE_SAMPLES['image/png'])
88
+ })
89
+
90
+ // Office legado é OLE2, não zip: servir o docx aqui daria arquivo corrompido no Word. Melhor um
91
+ // texto que abre e se explica.
92
+ it('explica em texto quando não há como forjar o binário', () => {
93
+ const resolved = resolvePreviewFileSample('application/msword', 'procuracao.doc')
94
+
95
+ expect(resolved.startsWith('data:text/plain')).toBe(true)
96
+ expect(decodeURIComponent(resolved)).toContain('procuracao.doc')
97
+ })
98
+
99
+ it('não devolve vazio para tipo desconhecido', () => {
100
+ expect(resolvePreviewFileSample(undefined).length).toBeGreaterThan(0)
101
+ expect(resolvePreviewFileSample('application/octet-stream').length).toBeGreaterThan(0)
102
+ })
103
+ })
104
+
105
+ describe('biblioteca do preview', () => {
106
+ // A infidelidade que existia: a biblioteca listava só `document`, enquanto o backend linka as
107
+ // cinco espécies de mídia. Foto, vídeo, áudio e sticker apareciam na tela real e não no preview.
108
+ it('lista foto, vídeo, áudio e sticker junto dos documentos', () => {
109
+ const mimeTypes = (PREVIEW_DOCUMENTS['5511944443333'] ?? []).map((document) => document.mimeType)
110
+
111
+ expect(mimeTypes.some((mimeType) => mimeType.startsWith('image/'))).toBe(true)
112
+ expect(mimeTypes.some((mimeType) => mimeType.startsWith('video/'))).toBe(true)
113
+ expect(mimeTypes.some((mimeType) => mimeType.startsWith('audio/'))).toBe(true)
114
+ expect(mimeTypes).toContain('image/webp')
115
+ })
116
+
117
+ // Cada tipo que a Cloud API da Meta aceita para envio de mídia.
118
+ const ACCEPTED_BY_META = [
119
+ 'image/jpeg',
120
+ 'image/png',
121
+ 'image/webp',
122
+ 'video/mp4',
123
+ 'video/3gp',
124
+ 'audio/aac',
125
+ 'audio/amr',
126
+ 'audio/mpeg',
127
+ 'audio/mp4',
128
+ 'audio/ogg',
129
+ 'application/pdf',
130
+ 'application/msword',
131
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
132
+ 'application/vnd.ms-excel',
133
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
134
+ 'application/vnd.ms-powerpoint',
135
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
136
+ 'text/plain',
137
+ ]
138
+
139
+ it('cobre todo tipo aceito pela Meta', () => {
140
+ const present = new Set((PREVIEW_DOCUMENTS['5511944443333'] ?? []).map((document) => document.mimeType))
141
+ const faltando = ACCEPTED_BY_META.filter((mimeType) => !present.has(mimeType))
142
+
143
+ expect(faltando).toEqual([])
144
+ })
145
+
146
+ it('todo item da biblioteca abre com bytes não vazios', () => {
147
+ for (const document of PREVIEW_DOCUMENTS['5511944443333'] ?? []) {
148
+ expect(decode(resolvePreviewFileSample(document.mimeType, document.filename)).bytes.length).toBeGreaterThan(0)
149
+ }
150
+ })
151
+ })
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Bytes de verdade para o preview abrir.
3
+ *
4
+ * O mock antes devolvia a MESMA imagem PNG para qualquer documento, então clicar em "visualizar" num
5
+ * PDF entregava um PNG rotulado `application/pdf` e o leitor dizia que o arquivo era inválido. Cada
6
+ * amostra aqui é um arquivo mínimo porém legítimo do seu tipo: as de mídia foram geradas com ffmpeg
7
+ * e conferidas com ffprobe, o PDF tem xref/startxref/%%EOF, e os pacotes Office são zips com as
8
+ * partes obrigatórias (`[Content_Types].xml`, `_rels/.rels`, o documento).
9
+ *
10
+ * Não há amostra de `audio/amr`, `video/3gp` nem dos formatos binários legados do Office
11
+ * (`application/msword`, `application/vnd.ms-excel`, `application/vnd.ms-powerpoint`): não há
12
+ * encoder para eles nesta máquina e forjar OLE2 à mão daria arquivo corrompido — exatamente o que
13
+ * este módulo existe para evitar. Esses caem no texto explicativo de `resolvePreviewFileSample`,
14
+ * que abre sem erro e diz o que é.
15
+ *
16
+ * GERADO — não editar à mão.
17
+ */
18
+
19
+ export const PREVIEW_FILE_SAMPLES: Readonly<Record<string, string>> = {
20
+ 'application/pdf':
21
+ 'data:application/pdf;base64,JVBERi0xLjQKMSAwIG9iajw8L1R5cGUvQ2F0YWxvZy9QYWdlcyAyIDAgUj4+ZW5kb2JqCjIgMCBvYmo8PC9UeXBlL1BhZ2VzL0tpZHNbMyAwIFJdL0NvdW50IDE+PmVuZG9iagozIDAgb2JqPDwvVHlwZS9QYWdlL1BhcmVudCAyIDAgUi9NZWRpYUJveFswIDAgMjYwIDExMF0vQ29udGVudHMgNCAwIFIvUmVzb3VyY2VzPDwvRm9udDw8L0YxIDUgMCBSPj4+Pj4+ZW5kb2JqCjQgMCBvYmo8PC9MZW5ndGggNjI+PnN0cmVhbQpCVCAvRjEgMTMgVGYgMTggNTUgVGQgKERvY3VtZW50byBkZSBleGVtcGxvIGRvIHByZXZpZXcpIFRqIEVUCmVuZHN0cmVhbWVuZG9iago1IDAgb2JqPDwvVHlwZS9Gb250L1N1YnR5cGUvVHlwZTEvQmFzZUZvbnQvSGVsdmV0aWNhPj5lbmRvYmoKeHJlZgowIDYKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAwNTIgMDAwMDAgbiAKMDAwMDAwMDEwMSAwMDAwMCBuIAowMDAwMDAwMjExIDAwMDAwIG4gCjAwMDAwMDAzMTcgMDAwMDAgbiAKdHJhaWxlcjw8L1NpemUgNi9Sb290IDEgMCBSPj4Kc3RhcnR4cmVmCjM3OAolJUVPRgo=',
22
+ 'image/png':
23
+ 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAIAAADYYG7QAAAACXBIWXMAAAABAAAAAQBPJcTWAAAATElEQVR4nO3OMRHAIAAAMbhDWo2hCYPdu/xIh0RB5rPP+JN1O/AlVISKUBEqQkWoCBWhIlSEilARKkJFqAgVoSJUhIpQESpCRagIlRcgDwI25HmPQAAAAABJRU5ErkJggg==',
24
+ 'image/jpeg':
25
+ 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAgAAAQABAAD//gAQTGF2YzYyLjExLjEwMAD/2wBDAAgEBAQEBAUFBQUFBQYGBgYGBgYGBgYGBgYHBwcICAgHBwcGBgcHCAgICAkJCQgICAgJCQoKCgwMCwsODg4RERT/xABMAAEBAAAAAAAAAAAAAAAAAAAABAEBAQAAAAAAAAAAAAAAAAAAAAYQAQAAAAAAAAAAAAAAAAAAAAARAQAAAAAAAAAAAAAAAAAAAAD/wAARCAAwADADASIAAhEAAxEA/9oADAMBAAIRAxEAPwCUBRpwAAAAAAAAAAAAAAAB/9k=',
26
+ 'image/webp': 'data:image/webp;base64,UklGRhoAAABXRUJQVlA4TA0AAAAvAAAAEAcQERGIiP4HAA==',
27
+ 'video/mp4':
28
+ 'data:video/mp4;base64,AAAAIGZ0eXBpc29tAAACAGlzb21pc28yYXZjMW1wNDEAAANdbW9vdgAAAGxtdmhkAAAAAAAAAAAAAAAAAAAD6AAAASwAAQAAAQAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAoh0cmFrAAAAXHRraGQAAAADAAAAAAAAAAAAAAABAAAAAAAAASwAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAEAAAABAAAAAAAAkZWR0cwAAABxlbHN0AAAAAAAAAAEAAAEsAAAIAAABAAAAAAIAbWRpYQAAACBtZGhkAAAAAAAAAAAAAAAAAAAoAAAAEABVxAAAAAAALWhkbHIAAAAAAAAAAHZpZGUAAAAAAAAAAAAAAABWaWRlb0hhbmRsZXIAAAABq21pbmYAAAAUdm1oZAAAAAEAAAAAAAAAAAAAACRkaW5mAAAAHGRyZWYAAAAAAAAAAQAAAAx1cmwgAAAAAQAAAWtzdGJsAAAAv3N0c2QAAAAAAAAAAQAAAK9hdmMxAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAEAAQABIAAAASAAAAAAAAAABFUxhdmM2Mi4xMS4xMDAgbGlieDI2NAAAAAAAAAAAAAAAGP//AAAANWF2Y0MBZAAK/+EAGGdkAAqs2UQmwEQAAAMABAAAAwBQPEiWWAEABmjr48siwP34+AAAAAAQcGFzcAAAAAEAAAABAAAAFGJ0cnQAAAAAAABO2gAAAAAAAAAYc3R0cwAAAAAAAAABAAAAAwAABAAAAAAUc3RzcwAAAAAAAAABAAAAAQAAAChjdHRzAAAAAAAAAAMAAAABAAAIAAAAAAEAAAwAAAAAAQAABAAAAAAcc3RzYwAAAAAAAAABAAAAAQAAAAMAAAABAAAAIHN0c3oAAAAAAAAAAAAAAAMAAALbAAAADgAAAAwAAAAUc3RjbwAAAAAAAAABAAADjQAAAGF1ZHRhAAAAWW1ldGEAAAAAAAAAIWhkbHIAAAAAAAAAAG1kaXJhcHBsAAAAAAAAAAAAAAAALGlsc3QAAAAkqXRvbwAAABxkYXRhAAAAAQAAAABMYXZmNjIuMy4xMDAAAAAIZnJlZQAAAv1tZGF0AAACrgYF//+q3EXpvebZSLeWLNgg2SPu73gyNjQgLSBjb3JlIDE2NSByMzIyMiBiMzU2MDVhIC0gSC4yNjQvTVBFRy00IEFWQyBjb2RlYyAtIENvcHlsZWZ0IDIwMDMtMjAyNSAtIGh0dHA6Ly93d3cudmlkZW9sYW4ub3JnL3gyNjQuaHRtbCAtIG9wdGlvbnM6IGNhYmFjPTEgcmVmPTMgZGVibG9jaz0xOjA6MCBhbmFseXNlPTB4MzoweDExMyBtZT1oZXggc3VibWU9NyBwc3k9MSBwc3lfcmQ9MS4wMDowLjAwIG1peGVkX3JlZj0xIG1lX3JhbmdlPTE2IGNocm9tYV9tZT0xIHRyZWxsaXM9MSA4eDhkY3Q9MSBjcW09MCBkZWFkem9uZT0yMSwxMSBmYXN0X3Bza2lwPTEgY2hyb21hX3FwX29mZnNldD0tMiB0aHJlYWRzPTIgbG9va2FoZWFkX3RocmVhZHM9MSBzbGljZWRfdGhyZWFkcz0wIG5yPTAgZGVjaW1hdGU9MSBpbnRlcmxhY2VkPTAgYmx1cmF5X2NvbXBhdD0wIGNvbnN0cmFpbmVkX2ludHJhPTAgYmZyYW1lcz0zIGJfcHlyYW1pZD0yIGJfYWRhcHQ9MSBiX2JpYXM9MCBkaXJlY3Q9MSB3ZWlnaHRiPTEgb3Blbl9nb3A9MCB3ZWlnaHRwPTIga2V5aW50PTI1MCBrZXlpbnRfbWluPTEwIHNjZW5lY3V0PTQwIGludHJhX3JlZnJlc2g9MCByY19sb29rYWhlYWQ9NDAgcmM9Y3JmIG1idHJlZT0xIGNyZj0yMy4wIHFjb21wPTAuNjAgcXBtaW49MCBxcG1heD02OSBxcHN0ZXA9NCBpcF9yYXRpbz0xLjQwIGFxPTE6MS4wMACAAAAAJWWIhAA///7mdfgUoKykhh+DdK6xVynnAWznTQ4fVSgMZ1FwY28AAAAKQZoibEN//qePiAAAAAgBnkF5Df8HfQ==',
29
+ 'audio/mpeg':
30
+ 'data:audio/mpeg;base64,SUQzBAAAAAAAIlRTU0UAAAAOAAADTGF2ZjYyLjMuMTAwAAAAAAAAAAAAAAD/40jAAAAAAAAAAAAASW5mbwAAAA8AAAAFAAAGwABVVVVVVVVVVVVVVVVVVVVVVVVVgICAgICAgICAgICAgICAgICAgICqqqqqqqqqqqqqqqqqqqqqqqqqqtXV1dXV1dXV1dXV1dXV1dXV1dXV//////////////////////////8AAAAATGF2YzYyLjExAAAAAAAAAAAAAAAAJALAAAAAAAAABsDR4nNFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/40jEAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/40jEAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/40jEAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/40jEAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/40jEAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU=',
31
+ 'audio/ogg':
32
+ 'data:audio/ogg;base64,T2dnUwACAAAAAAAAAABXDTmmAAAAAKMnDKEBE09wdXNIZWFkAQE4AYC7AAAAAABPZ2dTAAAAAAAAAAAAAFcNOaYBAAAAZlPuKwE9T3B1c1RhZ3MMAAAATGF2ZjYyLjMuMTAwAQAAAB0AAABlbmNvZGVyPUxhdmM2Mi4xMS4xMDAgbGlib3B1c09nZ1MABLgmAAAAAAAAVw05pgIAAAA3uL6lCw8REhISEhISEhISeAvkwTbsxY2MSUaZpCaweAfJcifhROpV8fDA1d3pUmB4B8l5yMlXwKISI/rvZ/NkwOB4B8l5yMlXwKISI/rvZ/NkwOB4B8l5yMlXwKISI/rvZ/NkwOB4B8l5yMlXwKISI/rvZ/NkwOB4B8l5yMlXwKISI/rvZ/NkwOB4B8l5yMlXwKISI/rvZ/NkwOB4B8l5yMlXwKISI/rvZ/NkwOB4B8l5yMlXwKISI/rvZ/NkwOB4B8l5yMlXwKISI/rvZ/NkwOA=',
33
+ 'audio/mp4':
34
+ 'data:audio/mp4;base64,AAAAHGZ0eXBNNEEgAAACAE00QSBpc29taXNvMgAAAAhmcmVlAAAAQW1kYXTeAgBMYXZjNjIuMTEuMTAwAAIwQA4BGCAHARggBwEYIAcBGCAHARggBwEYIAcBGCAHARggBwEYIAcAAAMibW9vdgAAAGxtdmhkAAAAAAAAAAAAAAAAAAAD6AAAAMgAAQAAAQAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAk10cmFrAAAAXHRraGQAAAADAAAAAAAAAAAAAAABAAAAAAAAAMgAAAAAAAAAAAAAAAEBAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAkZWR0cwAAABxlbHN0AAAAAAAAAAEAAADIAAAEAAABAAAAAAHFbWRpYQAAACBtZGhkAAAAAAAAAAAAAAAAAACsRAAAJnRVxAAAAAAALWhkbHIAAAAAAAAAAHNvdW4AAAAAAAAAAAAAAABTb3VuZEhhbmRsZXIAAAABcG1pbmYAAAAQc21oZAAAAAAAAAAAAAAAJGRpbmYAAAAcZHJlZgAAAAAAAAABAAAADHVybCAAAAABAAABNHN0YmwAAABqc3RzZAAAAAAAAAABAAAAWm1wNGEAAAAAAAAAAQAAAAAAAAAAAAEAEAAAAACsRAAAAAAANmVzZHMAAAAAA4CAgCUAAQAEgICAF0AVAAAAAAB9AAAAB/oFgICABRIIVuUABoCAgAECAAAAIHN0dHMAAAAAAAAAAgAAAAkAAAQAAAAAAQAAAnQAAAAcc3RzYwAAAAAAAAABAAAAAQAAAAoAAAABAAAAPHN0c3oAAAAAAAAAAAAAAAoAAAAVAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAAFHN0Y28AAAAAAAAAAQAAACwAAAAac2dwZAEAAAByb2xsAAAAAgAAAAH//wAAABxzYmdwAAAAAHJvbGwAAAABAAAACgAAAAEAAABhdWR0YQAAAFltZXRhAAAAAAAAACFoZGxyAAAAAAAAAABtZGlyYXBwbAAAAAAAAAAAAAAAACxpbHN0AAAAJKl0b28AAAAcZGF0YQAAAAEAAAAATGF2ZjYyLjMuMTAw',
35
+ 'audio/aac':
36
+ 'data:audio/aac;base64,//FQQAOf/N4CAExhdmM2Mi4xMS4xMDAAAjBADv/xUEABf/wBGCAH//FQQAF//AEYIAf/8VBAAX/8ARggB//xUEABf/wBGCAH//FQQAF//AEYIAf/8VBAAX/8ARggB//xUEABf/wBGCAH//FQQAF//AEYIAf/8VBAAX/8ARggBw==',
37
+ 'text/plain': 'data:text/plain;base64,TGlzdGEgZGUgY29tcHJhcwotIGFycm96IDJrZwotIG9sZW8gMXVuCg==',
38
+ 'text/csv': 'data:text/csv;base64,cHJvZHV0byxxdWFudGlkYWRlLHByZWNvCmFycm96LDIsMjQuOTAKb2xlbywxLDcuNTAK',
39
+ 'application/zip':
40
+ 'data:application/zip;base64,UEsDBBQAAAAIANFO/FwtmGOxGwAAABkAAAARAAAAY29tcHJvdmFudGUtMS50eHRLzs8tKMovS8wrSVVISVUoSExPzE3NK8nnAgBQSwMEFAAAAAgA0U78XCftV8YWAAAAFAAAABEAAABjb21wcm92YW50ZS0yLnR4dCtOTS/NS8lXSM7PLSjKL0vMK0nlAgBQSwECFAMUAAAACADRTvxcLZhjsRsAAAAZAAAAEQAAAAAAAAAAAAAAgAEAAAAAY29tcHJvdmFudGUtMS50eHRQSwECFAMUAAAACADRTvxcJ+1XxhYAAAAUAAAAEQAAAAAAAAAAAAAAgAFKAAAAY29tcHJvdmFudGUtMi50eHRQSwUGAAAAAAIAAgB+AAAAjwAAAAAA',
41
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
42
+ 'data:application/vnd.openxmlformats-officedocument.wordprocessingml.document;base64,UEsDBBQAAAAIANFO/Fx5bjPX6AAAAK0BAAATAAAAW0NvbnRlbnRfVHlwZXNdLnhtbH1QyU7DMBD9FWuuKHHggBCK0wPLETiUDxjZk8SqN3nc0v49Tlt6QIXjzFv1+tXeO7GjzDYGBbdtB4KCjsaGScHn+rV5AMEFg0EXAyk4EMNq6NeHRCyqNrCCuZT0KCXrmTxyGxOFiowxeyz1zJNMqDc4kbzrunupYygUSlMWDxj6Zxpx64p42df3qUcmxyCeTsQlSwGm5KzGUnG5C+ZXSnNOaKvyyOHZJr6pBJBXExbk74Cz7r0Ok60h8YG5vKGvLPkVs5Em6q2vyvZ/mys94zhaTRf94pZy1MRcF/euvSAebfjpL49zD99QSwMEFAAAAAgA0U78XJv9N+qtAAAAKQEAAAsAAABfcmVscy8ucmVsc43POw7CMAwG4KtE3mlaBoRQ0y4IqSsqB7ASN61oHkrCo7cnAwNFDIy2f3+W6/ZpZnanECdnBVRFCYysdGqyWsClP232wGJCq3B2lgQsFKFt6jPNmPJKHCcfWTZsFDCm5A+cRzmSwVg4TzZPBhcMplwGzT3KK2ri27Lc8fBpwNpknRIQOlUB6xdP/9huGCZJRydvhmz6ceIrkWUMmpKAhwuKq3e7yCzwpuarF5sXUEsDBBQAAAAIANFO/FyraIuoqgAAAOcAAAARAAAAd29yZC9kb2N1bWVudC54bWxFjksOwjAMBa8SZU9TWCBUNWWBxAngAKExbaTGjpzQz+1JyoLNPFt+Grm9rn4SM3B0hFoeq1oKwJ6sw0HL5+N+uEgRk0FrJkLQcoMor127NJb6jwdMIgswNouWY0qhUSr2I3gTKwqA+fYm9ibllQe1ENvA1EOM2e8ndarrs/LGoSzKF9mtZCjggtTdCBObRMKCgBV8mPJIIjDMDpZWlU4h7ww7fx71/7H7AlBLAQIUAxQAAAAIANFO/Fx5bjPX6AAAAK0BAAATAAAAAAAAAAAAAACAAQAAAABbQ29udGVudF9UeXBlc10ueG1sUEsBAhQDFAAAAAgA0U78XJv9N+qtAAAAKQEAAAsAAAAAAAAAAAAAAIABGQEAAF9yZWxzLy5yZWxzUEsBAhQDFAAAAAgA0U78XKtoi6iqAAAA5wAAABEAAAAAAAAAAAAAAIAB7wEAAHdvcmQvZG9jdW1lbnQueG1sUEsFBgAAAAADAAMAuQAAAMgCAAAAAA==',
43
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
44
+ 'data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64,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',
45
+ }
46
+
47
+ const FAMILY_FALLBACK: Readonly<Record<string, string>> = {
48
+ image: 'image/png',
49
+ video: 'video/mp4',
50
+ audio: 'audio/mpeg',
51
+ text: 'text/plain',
52
+ }
53
+
54
+ /**
55
+ * A data URL que o preview abre para um documento.
56
+ *
57
+ * Ordem: tipo exato → família (`audio/amr` toca como mp3, `video/3gp` como mp4) → nota em texto.
58
+ * A nota fecha o caso dos tipos sem amostra: abre num leitor de texto e explica, em vez de baixar
59
+ * um binário quebrado com extensão de Word.
60
+ */
61
+ export function resolvePreviewFileSample(mimeType: string | undefined, filename?: string): string {
62
+ const normalized = (mimeType ?? '').split(';')[0]!.trim().toLowerCase()
63
+ const exact = PREVIEW_FILE_SAMPLES[normalized]
64
+ if (exact) return exact
65
+
66
+ const family = FAMILY_FALLBACK[normalized.split('/')[0] ?? '']
67
+ const byFamily = family ? PREVIEW_FILE_SAMPLES[family] : undefined
68
+ if (byFamily) return byFamily
69
+
70
+ const note = `Preview do SDK: nao existe amostra binaria valida para ${normalized || 'tipo desconhecido'}${
71
+ filename ? ` (${filename})` : ''
72
+ }. Na tela real este arquivo vem do storage.`
73
+ return `data:text/plain;charset=utf-8,${encodeURIComponent(note)}`
74
+ }