@adatechnology/conversations-ui 0.1.0-rc.4 → 0.1.0-rc.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-LITPZCWW.js +1465 -0
- package/dist/index.d.ts +223 -29
- package/dist/index.js +316 -366
- package/dist/preview/index.d.ts +92 -3
- package/dist/preview/index.js +627 -103
- package/dist/types-CeixG2Z9.d.ts +324 -0
- package/package.json +2 -2
- package/src/Avatar.tsx +13 -2
- package/src/ConversationDocumentsPanel.tsx +342 -24
- package/src/ConversationListItem.tsx +18 -2
- package/src/DocumentsLibrary.tsx +322 -0
- package/src/FileIcon.test.ts +83 -0
- package/src/FileIcon.tsx +88 -11
- package/src/Lightbox.tsx +18 -3
- package/src/MediaRenderer.tsx +5 -2
- package/src/MessageBubble.tsx +18 -2
- package/src/MessageComposer.tsx +20 -3
- package/src/WhatsAppMessageEditor.tsx +28 -4
- package/src/hooks/useConversationActions.ts +56 -0
- package/src/hooks/useConversationDocuments.ts +11 -7
- package/src/hooks/useConversationList.ts +15 -9
- package/src/hooks/useConversationMessages.ts +2 -2
- package/src/index.ts +32 -11
- package/src/lib/cn.test.ts +29 -0
- package/src/lib/createMediaUrlResolver.ts +33 -0
- package/src/lib/paginated.test.ts +33 -0
- package/src/lib/paginated.ts +26 -0
- package/src/preview/MediaTypesPreview.tsx +87 -0
- package/src/preview/createMockConversationsApi.ts +175 -15
- package/src/preview/index.ts +6 -1
- package/src/preview/mockDocumentsSearch.test.ts +57 -0
- package/src/preview/preview.test.ts +5 -3
- package/src/preview/previewFileSamples.test.ts +151 -0
- package/src/preview/previewFileSamples.ts +74 -0
- package/src/preview/previewFixtures.ts +288 -1
- package/src/preview/previewMediaSource.test.ts +62 -0
- package/src/preview/previewMediaSource.ts +91 -0
- package/src/providers/types.ts +127 -9
- package/src/settings/WhatsAppCreateTemplateForm.tsx +3 -1
- package/src/useWaitingNotifications.ts +74 -29
- package/dist/chunk-4R6Y43DQ.js +0 -726
- package/dist/types-C0PtaO7S.d.ts +0 -207
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
|
|
3
|
+
interface ConversationsUIConfig {
|
|
4
|
+
apiBaseUrl: string;
|
|
5
|
+
theme?: ConversationsTheme;
|
|
6
|
+
features?: ConversationsFeatures;
|
|
7
|
+
}
|
|
8
|
+
interface ConversationsTheme {
|
|
9
|
+
primaryColor?: string;
|
|
10
|
+
backgroundColor?: string;
|
|
11
|
+
bubbleSent?: string;
|
|
12
|
+
bubbleReceived?: string;
|
|
13
|
+
textPrimary?: string;
|
|
14
|
+
textSecondary?: string;
|
|
15
|
+
}
|
|
16
|
+
interface ConversationsFeatures {
|
|
17
|
+
audio?: boolean;
|
|
18
|
+
documents?: boolean;
|
|
19
|
+
emoji?: boolean;
|
|
20
|
+
darkMode?: boolean;
|
|
21
|
+
}
|
|
22
|
+
interface MessagePayload {
|
|
23
|
+
id: string;
|
|
24
|
+
type: 'text' | 'image' | 'video' | 'audio' | 'document' | 'sticker' | 'template';
|
|
25
|
+
content?: string;
|
|
26
|
+
caption?: string;
|
|
27
|
+
mediaUrl?: string;
|
|
28
|
+
base64?: string;
|
|
29
|
+
uploadId?: string;
|
|
30
|
+
mediaId?: string;
|
|
31
|
+
mimeType?: string;
|
|
32
|
+
filename?: string;
|
|
33
|
+
sizeBytes?: number;
|
|
34
|
+
direction: 'inbound' | 'outbound';
|
|
35
|
+
sender: 'bot' | 'customer' | 'agent';
|
|
36
|
+
timestamp: string;
|
|
37
|
+
status?: 'sent' | 'delivered' | 'read' | 'failed';
|
|
38
|
+
readAt?: string;
|
|
39
|
+
agentName?: string | null;
|
|
40
|
+
templateName?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Veredito de moderação vindo do backend — a UI só exibe, nunca calcula. Dicionário no browser
|
|
43
|
+
* seria peso morto e daria veredito diferente por versão de cliente.
|
|
44
|
+
*
|
|
45
|
+
* `null`/ausente = não avaliado (moderação desligada, ou mensagem anterior ao recurso), que é
|
|
46
|
+
* diferente de avaliado e limpo.
|
|
47
|
+
*/
|
|
48
|
+
moderation?: {
|
|
49
|
+
isOffensive: boolean;
|
|
50
|
+
terms: string[];
|
|
51
|
+
} | null;
|
|
52
|
+
isFirstInGroup?: boolean;
|
|
53
|
+
isLastInGroup?: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
type ResolveMediaUrl = (message: MessagePayload) => Promise<string | null>;
|
|
57
|
+
interface MediaRendererProps {
|
|
58
|
+
message: MessagePayload;
|
|
59
|
+
onLightbox: (src: string) => void;
|
|
60
|
+
onResolveUrl?: ResolveMediaUrl;
|
|
61
|
+
/** Aplicado no wrapper de cada tipo de mídia — imagem, vídeo, áudio e documento. */
|
|
62
|
+
className?: string;
|
|
63
|
+
}
|
|
64
|
+
declare function MediaRenderer({ message, onLightbox, onResolveUrl, className }: MediaRendererProps): react.JSX.Element | null;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Canal de origem da conversa e o que cada um permite.
|
|
68
|
+
*
|
|
69
|
+
* Existe porque as regras que a inbox precisa respeitar não são do WhatsApp, são **de cada canal**:
|
|
70
|
+
* a janela de sessão, o jeito de reabrir a conversa e o formato do identificador do contato mudam
|
|
71
|
+
* entre WhatsApp, Messenger, Instagram e chat de site. Tratar a regra do WhatsApp como universal
|
|
72
|
+
* faria a UI bloquear o composer num chat de site, onde janela nenhuma existe.
|
|
73
|
+
*
|
|
74
|
+
* `whatsapp` é o padrão em todo lugar: instalações que ainda não informam canal continuam
|
|
75
|
+
* funcionando exatamente como antes.
|
|
76
|
+
*/
|
|
77
|
+
declare const CONVERSATION_CHANNEL: {
|
|
78
|
+
readonly WHATSAPP: "whatsapp";
|
|
79
|
+
readonly MESSENGER: "messenger";
|
|
80
|
+
readonly INSTAGRAM: "instagram";
|
|
81
|
+
readonly WEBCHAT: "webchat";
|
|
82
|
+
};
|
|
83
|
+
type ConversationChannel = (typeof CONVERSATION_CHANNEL)[keyof typeof CONVERSATION_CHANNEL];
|
|
84
|
+
declare const DEFAULT_CONVERSATION_CHANNEL: ConversationChannel;
|
|
85
|
+
/** Como o canal reabre uma conversa fora da janela de sessão. */
|
|
86
|
+
declare const REOPEN_MECHANISM: {
|
|
87
|
+
readonly TEMPLATE: "template";
|
|
88
|
+
readonly TAG: "tag";
|
|
89
|
+
readonly NONE: "none";
|
|
90
|
+
};
|
|
91
|
+
type ReopenMechanism = (typeof REOPEN_MECHANISM)[keyof typeof REOPEN_MECHANISM];
|
|
92
|
+
/** Natureza do identificador do contato — decide como exibi-lo. */
|
|
93
|
+
declare const HANDLE_KIND: {
|
|
94
|
+
readonly PHONE: "phone";
|
|
95
|
+
readonly USERNAME: "username";
|
|
96
|
+
readonly SESSION: "session";
|
|
97
|
+
};
|
|
98
|
+
type HandleKind = (typeof HANDLE_KIND)[keyof typeof HANDLE_KIND];
|
|
99
|
+
type ChannelCapabilities = {
|
|
100
|
+
readonly label: string;
|
|
101
|
+
readonly icon: string;
|
|
102
|
+
readonly hasSessionWindow: boolean;
|
|
103
|
+
readonly windowHours: number;
|
|
104
|
+
readonly reopenMechanism: ReopenMechanism;
|
|
105
|
+
readonly handleKind: HandleKind;
|
|
106
|
+
};
|
|
107
|
+
declare const CHANNEL_CAPABILITIES: Readonly<Record<ConversationChannel, ChannelCapabilities>>;
|
|
108
|
+
declare function capabilitiesOf(channel: ConversationChannel | undefined): ChannelCapabilities;
|
|
109
|
+
declare const CHANNEL_FILTER_ALL = "all";
|
|
110
|
+
type ChannelFilter = ConversationChannel | typeof CHANNEL_FILTER_ALL;
|
|
111
|
+
type ChannelFilterOption = {
|
|
112
|
+
readonly value: ChannelFilter;
|
|
113
|
+
readonly label: string;
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* Opções derivadas do que existe na lista, não do catálogo inteiro: oferecer Instagram numa conta
|
|
117
|
+
* que só tem WhatsApp promete um recorte que nunca traz resultado.
|
|
118
|
+
*
|
|
119
|
+
* Devolve vazio com menos de dois canais — um filtro de opção única não filtra nada, e a barra só
|
|
120
|
+
* ocuparia espaço. O host usa isso para esconder a seção.
|
|
121
|
+
*/
|
|
122
|
+
declare function channelFiltersFor(conversations: readonly {
|
|
123
|
+
readonly channel?: ConversationChannel | undefined;
|
|
124
|
+
}[]): ChannelFilterOption[];
|
|
125
|
+
type FormatContactHandleParams = {
|
|
126
|
+
readonly handle: string;
|
|
127
|
+
readonly channel?: ConversationChannel | undefined;
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* Exibição do identificador conforme a natureza dele. Formatar tudo como telefone — o que a UI
|
|
131
|
+
* fazia — transforma um `@perfil` do Instagram em dígitos sem sentido.
|
|
132
|
+
*/
|
|
133
|
+
declare function formatContactHandle(params: FormatContactHandleParams): string;
|
|
134
|
+
/** Bandeira só faz sentido quando o identificador é telefone. */
|
|
135
|
+
declare function contactFlag(params: FormatContactHandleParams): string;
|
|
136
|
+
|
|
137
|
+
interface ListConversationsParams {
|
|
138
|
+
page?: number;
|
|
139
|
+
limit?: number;
|
|
140
|
+
waitingHuman?: boolean;
|
|
141
|
+
search?: string;
|
|
142
|
+
/**
|
|
143
|
+
* Recortes que só o produto conhece (tipo de financiamento, carteira, campanha) repassados
|
|
144
|
+
* crus ao backend dele. É o que evita o vocabulário de uma vertical virar campo fixo aqui:
|
|
145
|
+
* o pacote transporta o filtro sem saber o que ele significa.
|
|
146
|
+
*/
|
|
147
|
+
filters?: Record<string, string | undefined>;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Página com o total, para a UI conseguir desenhar controles de paginação.
|
|
151
|
+
*
|
|
152
|
+
* `fetchConversations` devolve isto **ou** o array puro de antes: implementações existentes
|
|
153
|
+
* continuam válidas sem mudar uma linha, e quem precisa paginar passa a ter o total. Sem a união
|
|
154
|
+
* seria impossível saber se um retorno curto é a última página ou uma página cheia por acaso.
|
|
155
|
+
*/
|
|
156
|
+
interface ConversationPage {
|
|
157
|
+
conversations: ConversationSummary[];
|
|
158
|
+
total: number;
|
|
159
|
+
}
|
|
160
|
+
interface ListDocumentsParams {
|
|
161
|
+
search?: string;
|
|
162
|
+
page?: number;
|
|
163
|
+
/** Tamanho da página. Sem ele, `page` sozinho não define fatia nenhuma. */
|
|
164
|
+
limit?: number;
|
|
165
|
+
/** Origem do arquivo (`customer`, `agent`, `bot`…). O vocabulário é do host. */
|
|
166
|
+
source?: string;
|
|
167
|
+
sortDirection?: 'asc' | 'desc';
|
|
168
|
+
}
|
|
169
|
+
/** Arquivo na biblioteca da empresa: o mesmo da conversa, mais de qual conversa veio. */
|
|
170
|
+
interface CompanyDocument extends ConversationDocument {
|
|
171
|
+
conversationId: string;
|
|
172
|
+
}
|
|
173
|
+
interface CompanyDocumentPage {
|
|
174
|
+
documents: CompanyDocument[];
|
|
175
|
+
total: number;
|
|
176
|
+
}
|
|
177
|
+
interface ConversationDocumentPage {
|
|
178
|
+
documents: ConversationDocument[];
|
|
179
|
+
total: number;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Template disponível para envio a partir da inbox. Distinto do `WhatsAppTemplateSummary` de
|
|
183
|
+
* `settings/`, e de propósito: aquele serve ao formulário que **edita** template e carrega o que
|
|
184
|
+
* a edição precisa (`shortId`, `variableCount`); este serve a quem só vai **escolher um para
|
|
185
|
+
* enviar**, e pedir os campos de edição obrigaria todo host a produzi-los sem uso.
|
|
186
|
+
*/
|
|
187
|
+
interface ConversationTemplate {
|
|
188
|
+
name: string;
|
|
189
|
+
language: string;
|
|
190
|
+
status: string;
|
|
191
|
+
category?: string;
|
|
192
|
+
bodyText?: string | null;
|
|
193
|
+
}
|
|
194
|
+
interface ConversationsApi {
|
|
195
|
+
fetchMessages(conversationId: string, params?: {
|
|
196
|
+
limit?: number;
|
|
197
|
+
before?: string;
|
|
198
|
+
}): Promise<MessagePayload[]>;
|
|
199
|
+
fetchConversations(params?: ListConversationsParams): Promise<ConversationSummary[] | ConversationPage>;
|
|
200
|
+
sendMessage(conversationId: string, text: string): Promise<MessagePayload>;
|
|
201
|
+
sendMedia(conversationId: string, data: {
|
|
202
|
+
base64: string;
|
|
203
|
+
mimeType: string;
|
|
204
|
+
filename: string;
|
|
205
|
+
caption?: string;
|
|
206
|
+
}): Promise<MessagePayload>;
|
|
207
|
+
/**
|
|
208
|
+
* `templateName` é opcional porque reabrir a janela é a operação, e escolher *qual* template a
|
|
209
|
+
* usa nem sempre é decisão da UI: backends que guardam um template padrão configurado só
|
|
210
|
+
* precisam do "reabra". Exigir o nome obrigaria toda inbox a listar templates antes de poder
|
|
211
|
+
* mandar o primeiro — e a listagem é `listTemplates?`, opcional.
|
|
212
|
+
*/
|
|
213
|
+
sendTemplate(conversationId: string, data: {
|
|
214
|
+
templateName?: string;
|
|
215
|
+
languageCode?: string;
|
|
216
|
+
bodyParams?: string[];
|
|
217
|
+
}): Promise<void>;
|
|
218
|
+
markRead(conversationId: string): Promise<void>;
|
|
219
|
+
getContext(conversationId: string): Promise<Record<string, unknown>>;
|
|
220
|
+
getDocuments(conversationId: string, params?: ListDocumentsParams): Promise<ConversationDocument[] | ConversationDocumentPage>;
|
|
221
|
+
/**
|
|
222
|
+
* `disposition` decide entre abrir no navegador e baixar. É o backend que assina a URL e grava
|
|
223
|
+
* o `Content-Disposition` nela, então a escolha precisa viajar na chamada — depois de assinada
|
|
224
|
+
* não há como o cliente mudá-la. Ausente = o padrão do host.
|
|
225
|
+
*/
|
|
226
|
+
getDocumentUrl(uploadId: string, disposition?: 'inline' | 'attachment'): Promise<string>;
|
|
227
|
+
/**
|
|
228
|
+
* Baixa vários arquivos num zip único.
|
|
229
|
+
*
|
|
230
|
+
* **Opcional por capacidade:** montar zip exige o host LER os bytes do storage, o que nem toda
|
|
231
|
+
* instalação faz — as que só assinam URL não conseguem. Ausente, o painel esconde a seleção em
|
|
232
|
+
* lote em vez de oferecer um botão que falha.
|
|
233
|
+
*/
|
|
234
|
+
downloadDocumentsArchive?(conversationId: string, uploadIds: readonly string[]): Promise<Blob>;
|
|
235
|
+
/**
|
|
236
|
+
* Biblioteca de TODAS as conversas, para uma tela de Documentos fora do atendimento.
|
|
237
|
+
*
|
|
238
|
+
* Opcional por capacidade: host que só expõe anexo dentro da conversa não implementa, e o
|
|
239
|
+
* componente de biblioteca simplesmente não é usável — melhor que uma tela que sempre erra.
|
|
240
|
+
*/
|
|
241
|
+
getAllDocuments?(params?: ListDocumentsParams): Promise<CompanyDocumentPage>;
|
|
242
|
+
getMediaProxyUrl(mediaId: string): Promise<{
|
|
243
|
+
mimeType: string;
|
|
244
|
+
data: string;
|
|
245
|
+
}>;
|
|
246
|
+
/**
|
|
247
|
+
* Operações de atendimento humano. **Opcionais por capacidade, não por descuido:** nem toda
|
|
248
|
+
* inbox tem fila humana — um canal só-bot, ou um chat de site sem operador, não sabe o que é
|
|
249
|
+
* assumir conversa. Quem não implementa não ganha o botão, em vez de ganhar um botão que
|
|
250
|
+
* estoura no clique. Os hooks devolvem `undefined` para a ação ausente, e é isso que a UI
|
|
251
|
+
* consulta para decidir se desenha a afordância.
|
|
252
|
+
*/
|
|
253
|
+
takeover?(conversationId: string): Promise<void>;
|
|
254
|
+
release?(conversationId: string): Promise<void>;
|
|
255
|
+
/** Encerra o atendimento. Despedida, se houver, é decisão do host — o pacote não a inventa. */
|
|
256
|
+
finalize?(conversationId: string): Promise<void>;
|
|
257
|
+
markAllRead?(): Promise<void>;
|
|
258
|
+
listTemplates?(): Promise<ConversationTemplate[]>;
|
|
259
|
+
/**
|
|
260
|
+
* Transcrição completa gerada pelo servidor. Existe ao lado de `buildTranscriptText`, que monta
|
|
261
|
+
* a partir das mensagens já em memória: a tela costuma ter só a última página carregada, e
|
|
262
|
+
* exportar dali entregaria um recorte parcial com cara de histórico inteiro. Opcional porque
|
|
263
|
+
* nem todo backend expõe a rota — quem não tem continua usando o builder local.
|
|
264
|
+
*/
|
|
265
|
+
exportTranscript?(conversationId: string): Promise<{
|
|
266
|
+
transcript: string;
|
|
267
|
+
filename: string;
|
|
268
|
+
}>;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Superfície mínima de stream que o pacote consome — exatamente o que `useConversationRealtime`
|
|
272
|
+
* usa: assinar 'message', desassinar e fechar. Deliberadamente estrutural em vez de
|
|
273
|
+
* `EventSource`: sem servidor HTTP não existe `EventSource`, e é isso que impediria alimentar a
|
|
274
|
+
* inbox com dados mockados em desenvolvimento. Um `EventSource` nativo satisfaz este tipo, então
|
|
275
|
+
* quem já implementa `SSEProvider` continua válido sem mudança.
|
|
276
|
+
*/
|
|
277
|
+
interface ConversationEventSource {
|
|
278
|
+
addEventListener(type: string, listener: (event: MessageEvent) => void): void;
|
|
279
|
+
removeEventListener(type: string, listener: (event: MessageEvent) => void): void;
|
|
280
|
+
close(): void;
|
|
281
|
+
}
|
|
282
|
+
interface SSEProvider {
|
|
283
|
+
connectConversationStream(conversationId: string): ConversationEventSource;
|
|
284
|
+
connectGlobalStream(): ConversationEventSource;
|
|
285
|
+
}
|
|
286
|
+
interface ConversationSummary {
|
|
287
|
+
id: string;
|
|
288
|
+
/**
|
|
289
|
+
* @deprecated Use `contactId` com `channel`. Mantido obrigatório para não quebrar quem já
|
|
290
|
+
* consome; some quando o segundo canal entrar em produção.
|
|
291
|
+
*/
|
|
292
|
+
whatsappNumber: string;
|
|
293
|
+
/** Identificador neutro do contato. Ausente = usa `whatsappNumber`. */
|
|
294
|
+
contactId?: string;
|
|
295
|
+
/** Ausente = `whatsapp`, o comportamento de antes desta mudança. */
|
|
296
|
+
channel?: ConversationChannel;
|
|
297
|
+
clientName?: string;
|
|
298
|
+
lastContent?: string;
|
|
299
|
+
lastDirection?: 'inbound' | 'outbound';
|
|
300
|
+
lastAt: string;
|
|
301
|
+
lastInboundAt: string | null;
|
|
302
|
+
mode: 'bot' | 'human';
|
|
303
|
+
assignedUserId: string | null;
|
|
304
|
+
waitingHuman: boolean;
|
|
305
|
+
unread: number;
|
|
306
|
+
currentState: string;
|
|
307
|
+
/**
|
|
308
|
+
* Atributos que só o produto conhece e desenha (tipo de financiamento, carteira, campanha). É a
|
|
309
|
+
* contraparte de leitura do `filters` de `ListConversationsParams`: o pacote transporta e nunca
|
|
310
|
+
* interpreta. Sem isto, exibir um selo próprio na linha exigiria o host manter uma segunda
|
|
311
|
+
* consulta paralela à mesma listagem — a implementação duplicada que o pacote existe para evitar.
|
|
312
|
+
*/
|
|
313
|
+
attributes?: Record<string, string | undefined>;
|
|
314
|
+
}
|
|
315
|
+
interface ConversationDocument {
|
|
316
|
+
id: string;
|
|
317
|
+
filename: string;
|
|
318
|
+
mimeType: string;
|
|
319
|
+
sizeBytes: number;
|
|
320
|
+
source: string;
|
|
321
|
+
linkedAt: string;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export { contactFlag as A, formatContactHandle as B, CHANNEL_CAPABILITIES as C, DEFAULT_CONVERSATION_CHANNEL as D, type FormatContactHandleParams as F, HANDLE_KIND as H, type ListConversationsParams as L, MediaRenderer as M, REOPEN_MECHANISM as R, type SSEProvider as S, CHANNEL_FILTER_ALL as a, CONVERSATION_CHANNEL as b, type ChannelCapabilities as c, type ChannelFilter as d, type ChannelFilterOption as e, type CompanyDocument as f, type CompanyDocumentPage as g, type ConversationChannel as h, type ConversationDocument as i, type ConversationDocumentPage as j, type ConversationEventSource as k, type ConversationPage as l, type ConversationSummary as m, type ConversationTemplate as n, type ConversationsApi as o, type ConversationsFeatures as p, type ConversationsTheme as q, type ConversationsUIConfig as r, type HandleKind as s, type ListDocumentsParams as t, type MediaRendererProps as u, type MessagePayload as v, type ReopenMechanism as w, type ResolveMediaUrl as x, capabilitiesOf as y, channelFiltersFor as z };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adatechnology/conversations-ui",
|
|
3
|
-
"version": "0.1.0-rc.
|
|
3
|
+
"version": "0.1.0-rc.6",
|
|
4
4
|
"description": "WhatsApp conversation UI components — parametrizável por endpoint, tema e feature flags",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"clsx": "^2.1.1",
|
|
32
32
|
"lucide-react": "^1.21.0",
|
|
33
33
|
"tailwind-merge": "^3.6.0",
|
|
34
|
-
"@adatechnology/meta-whatsapp-contracts": "0.2.0-rc.
|
|
34
|
+
"@adatechnology/meta-whatsapp-contracts": "0.2.0-rc.5"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"react": "^18 || ^19",
|
package/src/Avatar.tsx
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
|
+
export interface AvatarLabels {
|
|
2
|
+
/** Lido por leitor de tela quando não há nome nem imagem — a silhueta genérica. */
|
|
3
|
+
unnamedContact: string
|
|
4
|
+
}
|
|
5
|
+
|
|
1
6
|
export interface AvatarProps {
|
|
2
7
|
name?: string | null
|
|
3
8
|
avatarUrl?: string
|
|
4
9
|
size?: 'sm' | 'md' | 'lg'
|
|
5
10
|
className?: string
|
|
11
|
+
labels?: Partial<AvatarLabels>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_AVATAR_LABELS: AvatarLabels = {
|
|
15
|
+
unnamedContact: 'Contato sem nome',
|
|
6
16
|
}
|
|
7
17
|
|
|
8
18
|
const sizeClasses = {
|
|
@@ -36,8 +46,9 @@ function getBgColor(name: string | undefined | null): string {
|
|
|
36
46
|
|
|
37
47
|
// Paridade com financiamento-imobiliario-bot/apps/web/src/components/Avatar.tsx —
|
|
38
48
|
// mesmas iniciais (primeira+última letra), mesmo hash de cor, mesmas classes de tamanho.
|
|
39
|
-
export function Avatar({ name, avatarUrl, size = 'md', className = '' }: AvatarProps) {
|
|
49
|
+
export function Avatar({ name, avatarUrl, size = 'md', className = '', labels }: AvatarProps) {
|
|
40
50
|
const sizeClass = sizeClasses[size]
|
|
51
|
+
const unnamedContactLabel = labels?.unnamedContact ?? DEFAULT_AVATAR_LABELS.unnamedContact
|
|
41
52
|
|
|
42
53
|
if (avatarUrl) {
|
|
43
54
|
return (
|
|
@@ -58,7 +69,7 @@ export function Avatar({ name, avatarUrl, size = 'md', className = '' }: AvatarP
|
|
|
58
69
|
<div
|
|
59
70
|
className={`${sizeClass} ${bgColor} rounded-full flex items-center justify-center text-white flex-shrink-0 ${className}`}
|
|
60
71
|
role="img"
|
|
61
|
-
aria-label=
|
|
72
|
+
aria-label={unnamedContactLabel}
|
|
62
73
|
>
|
|
63
74
|
<svg viewBox="0 0 24 24" fill="currentColor" className="h-[60%] w-[60%]" aria-hidden>
|
|
64
75
|
<path d="M12 12a5 5 0 1 0 0-10 5 5 0 0 0 0 10Zm0 2c-4.42 0-8 2.24-8 5v1h16v-1c0-2.76-3.58-5-8-5Z" />
|