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

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.
@@ -1,6 +1,7 @@
1
1
  import { G as MessagePayload, o as ConversationSummary, m as ConversationEventSource, q as ConversationsApi, L as ListConversationsParams, n as ConversationPage, S as SSEProvider, k as ConversationDocument, N as ResolveMediaUrl } from '../types-C6A_9edv.js';
2
2
  export { A as AudioRecorderButton, a as AudioRecorderButtonLabels, b as AudioRecorderButtonProps, D as DEFAULT_AUDIO_RECORDER_BUTTON_LABELS } from '../types-C6A_9edv.js';
3
3
  import * as react from 'react';
4
+ import { ReactNode } from 'react';
4
5
  import { InteractiveReplyOption, InboundMediaType } from '@adatechnology/meta-whatsapp-contracts/testing';
5
6
  export { PREVIEW_MEDIA_ID_PREFIX } from '@adatechnology/meta-whatsapp-contracts';
6
7
 
@@ -296,6 +297,27 @@ type PreviewUploadedMedia = {
296
297
  declare function mediaTypeOf(mimeType: string): SendPreviewMediaParams['mediaType'];
297
298
  declare function ConversationPreview({ client, sse, conversationId, loadMessages, placeholder, pollIntervalMs, uploadMedia, }: ConversationPreviewProps): react.JSX.Element;
298
299
 
300
+ type ConversationSimulatorPanelLabels = {
301
+ readonly title: string;
302
+ /** Complementa o telefone no subtítulo, explicando para onde a mensagem realmente vai. */
303
+ readonly destinationHint: string;
304
+ readonly close: string;
305
+ readonly placeholder: string;
306
+ };
307
+ declare const DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS: ConversationSimulatorPanelLabels;
308
+ type ConversationSimulatorPanelProps = Omit<ConversationPreviewProps, 'placeholder'> & {
309
+ readonly onClose: () => void;
310
+ /**
311
+ * Telefone já formatado para leitura. É o host que formata: máscara de telefone é convenção
312
+ * regional, e o pacote não tem como saber a do produto.
313
+ */
314
+ readonly displayNumber?: string;
315
+ readonly labels?: Partial<ConversationSimulatorPanelLabels>;
316
+ /** Ações extras no cabeçalho — roteiro automático, limpar conversa, trocar de contato. */
317
+ readonly headerActions?: ReactNode;
318
+ };
319
+ declare function ConversationSimulatorPanel({ onClose, displayNumber, labels, headerActions, ...previewProps }: ConversationSimulatorPanelProps): react.JSX.Element;
320
+
299
321
  /**
300
322
  * Cliente do preview que NÃO carrega segredo: em vez de montar e assinar o payload da Meta no
301
323
  * navegador, manda um comando semântico (`{ kind: 'text', text }`) para uma rota do próprio host,
@@ -467,4 +489,4 @@ type MediaTypesPreviewProps = {
467
489
  };
468
490
  declare function MediaTypesPreview({ conversationId, className, }: MediaTypesPreviewProps): react.JSX.Element;
469
491
 
470
- export { type AppendMessageParams, ConversationPreview, type ConversationPreviewProps, type CreateMockConversationsApiParams, type CreateMockSSEProviderParams, type CreatePreviewBridgeClientParams, type CreatePreviewMediaUploaderParams, type CreatePreviewStoreParams, type CreatePreviewWebhookClientParams, DEFAULT_MEDIA_UPLOAD_PATH, DEFAULT_PREVIEW_SCRIPT, GLOBAL_CHANNEL, type ListConversationsFilters, MEDIA_TYPES_CONVERSATION_ID, MediaTypesPreview, type MediaTypesPreviewProps, type MockEventSource, PREVIEW_CONVERSATIONS, PREVIEW_DOCUMENTS, PREVIEW_FILE_SAMPLES, PREVIEW_MESSAGES, PreviewBridgeRejectedError, type PreviewEmission, PreviewInProductionError, type PreviewInboundCommand, PreviewMediaUploadRejectedError, type PreviewMediaUploadRequest, type PreviewScriptStep, type PreviewStore, type PreviewStoreListener, type PreviewUploadedMedia, type PreviewWebhookClient, PreviewWebhookRejectedError, type SendPreviewInboundCommand, type SendPreviewMediaParams, type SetModeParams, type StartPreviewScriptParams, assertPreviewEnvironment, conversationChannel, createMockConversationsApi, createMockEventSource, createMockSSEProvider, createPreviewBridgeClient, createPreviewMediaPoster, createPreviewMediaResolver, createPreviewMediaUploader, createPreviewStore, createPreviewWebhookClient, mediaTypeOf, previewFileBase64, previewFileUrl, resolvePreviewFileSample, signPreviewPayload, startPreviewScript };
492
+ export { type AppendMessageParams, ConversationPreview, type ConversationPreviewProps, ConversationSimulatorPanel, type ConversationSimulatorPanelLabels, type ConversationSimulatorPanelProps, type CreateMockConversationsApiParams, type CreateMockSSEProviderParams, type CreatePreviewBridgeClientParams, type CreatePreviewMediaUploaderParams, type CreatePreviewStoreParams, type CreatePreviewWebhookClientParams, DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS, DEFAULT_MEDIA_UPLOAD_PATH, DEFAULT_PREVIEW_SCRIPT, GLOBAL_CHANNEL, type ListConversationsFilters, MEDIA_TYPES_CONVERSATION_ID, MediaTypesPreview, type MediaTypesPreviewProps, type MockEventSource, PREVIEW_CONVERSATIONS, PREVIEW_DOCUMENTS, PREVIEW_FILE_SAMPLES, PREVIEW_MESSAGES, PreviewBridgeRejectedError, type PreviewEmission, PreviewInProductionError, type PreviewInboundCommand, PreviewMediaUploadRejectedError, type PreviewMediaUploadRequest, type PreviewScriptStep, type PreviewStore, type PreviewStoreListener, type PreviewUploadedMedia, type PreviewWebhookClient, PreviewWebhookRejectedError, type SendPreviewInboundCommand, type SendPreviewMediaParams, type SetModeParams, type StartPreviewScriptParams, assertPreviewEnvironment, conversationChannel, createMockConversationsApi, createMockEventSource, createMockSSEProvider, createPreviewBridgeClient, createPreviewMediaPoster, createPreviewMediaResolver, createPreviewMediaUploader, createPreviewStore, createPreviewWebhookClient, mediaTypeOf, previewFileBase64, previewFileUrl, resolvePreviewFileSample, signPreviewPayload, startPreviewScript };
@@ -1034,6 +1034,38 @@ function ConversationPreview({
1034
1034
  ] });
1035
1035
  }
1036
1036
 
1037
+ // src/preview/ConversationSimulatorPanel.tsx
1038
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1039
+ var DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS = {
1040
+ title: "Simulador do cliente",
1041
+ destinationHint: "entrega no webhook real",
1042
+ close: "Fechar simulador",
1043
+ placeholder: "Escreva como o cliente\u2026"
1044
+ };
1045
+ function ConversationSimulatorPanel({
1046
+ onClose,
1047
+ displayNumber,
1048
+ labels,
1049
+ headerActions,
1050
+ ...previewProps
1051
+ }) {
1052
+ const text = { ...DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS, ...labels };
1053
+ const subtitle = [displayNumber ?? previewProps.conversationId, text.destinationHint].join(" \xB7 ");
1054
+ return /* @__PURE__ */ jsxs2("aside", { className: "cv-simulator-panel", "aria-label": text.title, children: [
1055
+ /* @__PURE__ */ jsxs2("header", { className: "cv-simulator-panel__header", children: [
1056
+ /* @__PURE__ */ jsxs2("div", { className: "cv-simulator-panel__heading", children: [
1057
+ /* @__PURE__ */ jsx2("h2", { className: "cv-simulator-panel__title", children: text.title }),
1058
+ /* @__PURE__ */ jsx2("p", { className: "cv-simulator-panel__subtitle", children: subtitle })
1059
+ ] }),
1060
+ /* @__PURE__ */ jsxs2("div", { className: "cv-simulator-panel__actions", children: [
1061
+ headerActions,
1062
+ /* @__PURE__ */ jsx2("button", { type: "button", onClick: onClose, title: text.close, "aria-label": text.close, className: "cv-simulator-panel__close", children: /* @__PURE__ */ jsx2("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", "aria-hidden": "true", children: /* @__PURE__ */ jsx2("path", { d: "M18 6 6 18M6 6l12 12" }) }) })
1063
+ ] })
1064
+ ] }),
1065
+ /* @__PURE__ */ jsx2("div", { className: "cv-simulator-panel__body", children: /* @__PURE__ */ jsx2(ConversationPreview, { ...previewProps, placeholder: text.placeholder }) })
1066
+ ] });
1067
+ }
1068
+
1037
1069
  // src/preview/createPreviewWebhookClient.ts
1038
1070
  import {
1039
1071
  buildInboundAudioPayload,
@@ -1244,7 +1276,7 @@ function startPreviewScript(params) {
1244
1276
 
1245
1277
  // src/preview/MediaTypesPreview.tsx
1246
1278
  import { useMemo as useMemo2 } from "react";
1247
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1279
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1248
1280
  var MEDIA_TYPES_CONVERSATION_ID = "5511944443333";
1249
1281
  function MediaTypesPreview({
1250
1282
  conversationId = MEDIA_TYPES_CONVERSATION_ID,
@@ -1259,26 +1291,26 @@ function MediaTypesPreview({
1259
1291
  const messages = PREVIEW_MESSAGES[conversationId] ?? [];
1260
1292
  const documents = PREVIEW_DOCUMENTS[conversationId] ?? [];
1261
1293
  const mimeTypes = [...new Set(documents.map((document) => document.mimeType))];
1262
- return /* @__PURE__ */ jsx2(ConversationsProvider, { api, sse, children: /* @__PURE__ */ jsxs2("div", { className, children: [
1263
- /* @__PURE__ */ jsxs2("header", { className: "border-b px-4 py-3 dark:border-gray-700", children: [
1264
- /* @__PURE__ */ jsx2("h1", { className: "text-lg font-semibold", children: "Teste manual de m\xEDdia" }),
1265
- /* @__PURE__ */ jsxs2("p", { className: "text-sm text-gray-500", children: [
1294
+ return /* @__PURE__ */ jsx3(ConversationsProvider, { api, sse, children: /* @__PURE__ */ jsxs3("div", { className, children: [
1295
+ /* @__PURE__ */ jsxs3("header", { className: "border-b px-4 py-3 dark:border-gray-700", children: [
1296
+ /* @__PURE__ */ jsx3("h1", { className: "text-lg font-semibold", children: "Teste manual de m\xEDdia" }),
1297
+ /* @__PURE__ */ jsxs3("p", { className: "text-sm text-gray-500", children: [
1266
1298
  documents.length,
1267
1299
  " arquivos, ",
1268
1300
  mimeTypes.length,
1269
1301
  " tipos. Clique no olho para abrir em aba nova e no bot\xE3o da bolha para carregar a m\xEDdia na thread \u2014 \xE9 o que teste automatizado n\xE3o v\xEA."
1270
1302
  ] })
1271
1303
  ] }),
1272
- /* @__PURE__ */ jsxs2("div", { className: "grid gap-4 p-4 lg:grid-cols-2", children: [
1273
- /* @__PURE__ */ jsxs2("section", { className: "space-y-3", children: [
1274
- /* @__PURE__ */ jsx2("h2", { className: "text-sm font-semibold uppercase tracking-wide text-gray-500", children: "Biblioteca da empresa" }),
1275
- /* @__PURE__ */ jsx2(DocumentsLibrary, { perPage: documents.length || 20 })
1304
+ /* @__PURE__ */ jsxs3("div", { className: "grid gap-4 p-4 lg:grid-cols-2", children: [
1305
+ /* @__PURE__ */ jsxs3("section", { className: "space-y-3", children: [
1306
+ /* @__PURE__ */ jsx3("h2", { className: "text-sm font-semibold uppercase tracking-wide text-gray-500", children: "Biblioteca da empresa" }),
1307
+ /* @__PURE__ */ jsx3(DocumentsLibrary, { perPage: documents.length || 20 })
1276
1308
  ] }),
1277
- /* @__PURE__ */ jsxs2("section", { className: "space-y-3", children: [
1278
- /* @__PURE__ */ jsx2("h2", { className: "text-sm font-semibold uppercase tracking-wide text-gray-500", children: "Painel da conversa" }),
1279
- /* @__PURE__ */ jsx2(ConversationDocumentsPanel, { conversationId, open: true, perPage: documents.length || 20 }),
1280
- /* @__PURE__ */ jsx2("h2", { className: "text-sm font-semibold uppercase tracking-wide text-gray-500", children: "Bolhas na thread" }),
1281
- /* @__PURE__ */ jsx2(ConversationWallpaper, { className: "max-h-[70vh] overflow-y-auto rounded-lg px-3 py-2", children: messages.map((message, index) => /* @__PURE__ */ jsx2(
1309
+ /* @__PURE__ */ jsxs3("section", { className: "space-y-3", children: [
1310
+ /* @__PURE__ */ jsx3("h2", { className: "text-sm font-semibold uppercase tracking-wide text-gray-500", children: "Painel da conversa" }),
1311
+ /* @__PURE__ */ jsx3(ConversationDocumentsPanel, { conversationId, open: true, perPage: documents.length || 20 }),
1312
+ /* @__PURE__ */ jsx3("h2", { className: "text-sm font-semibold uppercase tracking-wide text-gray-500", children: "Bolhas na thread" }),
1313
+ /* @__PURE__ */ jsx3(ConversationWallpaper, { className: "max-h-[70vh] overflow-y-auto rounded-lg px-3 py-2", children: messages.map((message, index) => /* @__PURE__ */ jsx3(
1282
1314
  MessageBubble,
1283
1315
  {
1284
1316
  message,
@@ -1294,7 +1326,9 @@ function MediaTypesPreview({
1294
1326
  export {
1295
1327
  AudioRecorderButton,
1296
1328
  ConversationPreview,
1329
+ ConversationSimulatorPanel,
1297
1330
  DEFAULT_AUDIO_RECORDER_BUTTON_LABELS,
1331
+ DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS,
1298
1332
  DEFAULT_MEDIA_UPLOAD_PATH,
1299
1333
  DEFAULT_PREVIEW_SCRIPT,
1300
1334
  GLOBAL_CHANNEL,
package/dist/styles.css CHANGED
@@ -237,3 +237,85 @@
237
237
  :where(button:not(:disabled), [role=button]:not(:disabled), summary) {
238
238
  cursor: pointer;
239
239
  }
240
+ .cv-simulator-panel {
241
+ display: flex;
242
+ flex-direction: column;
243
+ width: 24rem;
244
+ flex-shrink: 0;
245
+ border-left: 1px solid rgb(229 231 235);
246
+ background: #fff;
247
+ }
248
+ .cv-simulator-panel__header {
249
+ display: flex;
250
+ align-items: center;
251
+ justify-content: space-between;
252
+ gap: 0.5rem;
253
+ padding: 0.75rem 1rem;
254
+ border-bottom: 1px solid rgb(229 231 235);
255
+ }
256
+ .cv-simulator-panel__heading {
257
+ min-width: 0;
258
+ }
259
+ .cv-simulator-panel__title {
260
+ font-size: 0.875rem;
261
+ font-weight: 600;
262
+ color: rgb(17 24 39);
263
+ overflow: hidden;
264
+ text-overflow: ellipsis;
265
+ white-space: nowrap;
266
+ }
267
+ .cv-simulator-panel__subtitle {
268
+ font-size: 0.75rem;
269
+ color: rgb(156 163 175);
270
+ overflow: hidden;
271
+ text-overflow: ellipsis;
272
+ white-space: nowrap;
273
+ }
274
+ .cv-simulator-panel__actions {
275
+ display: flex;
276
+ align-items: center;
277
+ gap: 0.25rem;
278
+ }
279
+ .cv-simulator-panel__close {
280
+ display: inline-flex;
281
+ padding: 0.375rem;
282
+ border: 0;
283
+ border-radius: 0.5rem;
284
+ background: transparent;
285
+ color: rgb(156 163 175);
286
+ transition: color 150ms, background-color 150ms;
287
+ }
288
+ .cv-simulator-panel__close:hover {
289
+ color: rgb(55 65 81);
290
+ background: rgb(243 244 246);
291
+ }
292
+ .cv-simulator-panel__body {
293
+ flex: 1;
294
+ min-height: 0;
295
+ }
296
+ .dark .cv-simulator-panel {
297
+ border-left-color: rgb(31 41 55);
298
+ background: rgb(17 24 39);
299
+ }
300
+ .dark .cv-simulator-panel__header {
301
+ border-bottom-color: rgb(31 41 55);
302
+ }
303
+ .dark .cv-simulator-panel__title {
304
+ color: rgb(243 244 246);
305
+ }
306
+ .dark .cv-simulator-panel__subtitle {
307
+ color: rgb(107 114 128);
308
+ }
309
+ .dark .cv-simulator-panel__close:hover {
310
+ color: rgb(229 231 235);
311
+ background: rgb(31 41 55);
312
+ }
313
+ @media (max-width: 1023px) {
314
+ .cv-simulator-panel {
315
+ position: fixed;
316
+ inset: 0;
317
+ width: 100%;
318
+ z-index: 40;
319
+ border-left: 0;
320
+ }
321
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adatechnology/conversations-ui",
3
- "version": "0.1.0-rc.21",
3
+ "version": "0.1.0-rc.22",
4
4
  "description": "WhatsApp conversation UI components — parametrizável por endpoint, tema e feature flags",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -0,0 +1,55 @@
1
+ import { describe, expect, it } from 'bun:test'
2
+ import { renderToStaticMarkup } from 'react-dom/server'
3
+
4
+ import { ConversationSimulatorPanel, type ConversationSimulatorPanelProps } from './ConversationSimulatorPanel'
5
+ import { createMockSSEProvider } from './createMockSSEProvider'
6
+ import { createPreviewStore } from './previewStore'
7
+
8
+ function markupOf(overrides: Partial<ConversationSimulatorPanelProps> = {}): string {
9
+ const store = createPreviewStore({ conversations: [], messages: {} })
10
+
11
+ return renderToStaticMarkup(
12
+ <ConversationSimulatorPanel
13
+ conversationId="5511900000042"
14
+ onClose={() => {}}
15
+ client={{ sendText: async () => {}, sendInteractiveReply: async () => {} } as never}
16
+ sse={createMockSSEProvider({ store })}
17
+ loadMessages={async () => []}
18
+ {...overrides}
19
+ />,
20
+ )
21
+ }
22
+
23
+ describe('ConversationSimulatorPanel', () => {
24
+ it('mostra o telefone formatado pelo host, e não o id cru', () => {
25
+ expect(markupOf({ displayNumber: '+55 (11) 90000-0042' })).toContain('+55 (11) 90000-0042')
26
+ })
27
+
28
+ it('cai no id da conversa quando o host não formata', () => {
29
+ expect(markupOf()).toContain('5511900000042')
30
+ })
31
+
32
+ it('avisa para onde a mensagem vai, para ninguém achar que é conversa de mentira', () => {
33
+ expect(markupOf()).toContain('entrega no webhook real')
34
+ })
35
+
36
+ it('dá nome acessível ao botão de fechar, que só tem ícone', () => {
37
+ expect(markupOf()).toContain('aria-label="Fechar simulador"')
38
+ })
39
+
40
+ it('aceita rótulos parciais do host sem exigir o conjunto inteiro', () => {
41
+ const markup = markupOf({ labels: { title: 'Testar fluxo' } })
42
+
43
+ expect(markup).toContain('Testar fluxo')
44
+ // O que não foi sobrescrito continua no default.
45
+ expect(markup).toContain('aria-label="Fechar simulador"')
46
+ })
47
+
48
+ it('renderiza ação extra do host no cabeçalho', () => {
49
+ expect(markupOf({ headerActions: <button type="button">Rodar roteiro</button> })).toContain('Rodar roteiro')
50
+ })
51
+
52
+ it('nomeia o próprio aside, para o leitor de tela distinguir do transcript ao lado', () => {
53
+ expect(markupOf()).toContain('aria-label="Simulador do cliente"')
54
+ })
55
+ })
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Copyright (c) 2026 Ada Technology. All rights reserved.
3
+ *
4
+ * This source code is proprietary and confidential. Unauthorized copying,
5
+ * modification, distribution, or use of this file, via any medium, is
6
+ * strictly prohibited without prior written permission from Ada Technology.
7
+ *
8
+ * Author: Anderson Filho <andersonfrfilho@gmail.com>
9
+ *
10
+ * Painel lateral do simulador de cliente: a moldura em volta do `ConversationPreview`.
11
+ *
12
+ * Existe como componente do pacote porque cada produto tinha reescrito a mesma moldura —
13
+ * `<aside>`, cabeçalho com título e telefone, botão de fechar — e as cópias divergiram. Uma delas
14
+ * migrou para o cliente-ponte (assinatura no servidor) e a outra ficou assinando no navegador, com
15
+ * o app secret publicado no bundle. É o tipo de correção que precisa chegar a todo mundo de uma vez.
16
+ *
17
+ * Mora DENTRO da tela de conversas, e não numa aba própria, por um motivo prático: o token do
18
+ * atendente costuma viver em `sessionStorage`, que é por aba. Numa aba separada o simulador nascia
19
+ * sem sessão, o transcript nunca carregava, e o sintoma era "mandei e não aconteceu nada".
20
+ *
21
+ * O efeito de cada envio aparece na thread ao lado pelo mesmo stream que a inbox já assina — este
22
+ * painel não abre conexão própria.
23
+ */
24
+
25
+ import type { ReactNode } from 'react'
26
+
27
+ import { ConversationPreview, type ConversationPreviewProps } from './ConversationPreview'
28
+
29
+ export type ConversationSimulatorPanelLabels = {
30
+ readonly title: string
31
+ /** Complementa o telefone no subtítulo, explicando para onde a mensagem realmente vai. */
32
+ readonly destinationHint: string
33
+ readonly close: string
34
+ readonly placeholder: string
35
+ }
36
+
37
+ export const DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS: ConversationSimulatorPanelLabels = {
38
+ title: 'Simulador do cliente',
39
+ destinationHint: 'entrega no webhook real',
40
+ close: 'Fechar simulador',
41
+ placeholder: 'Escreva como o cliente…',
42
+ }
43
+
44
+ export type ConversationSimulatorPanelProps = Omit<ConversationPreviewProps, 'placeholder'> & {
45
+ readonly onClose: () => void
46
+ /**
47
+ * Telefone já formatado para leitura. É o host que formata: máscara de telefone é convenção
48
+ * regional, e o pacote não tem como saber a do produto.
49
+ */
50
+ readonly displayNumber?: string
51
+ readonly labels?: Partial<ConversationSimulatorPanelLabels>
52
+ /** Ações extras no cabeçalho — roteiro automático, limpar conversa, trocar de contato. */
53
+ readonly headerActions?: ReactNode
54
+ }
55
+
56
+ export function ConversationSimulatorPanel({
57
+ onClose,
58
+ displayNumber,
59
+ labels,
60
+ headerActions,
61
+ ...previewProps
62
+ }: ConversationSimulatorPanelProps) {
63
+ const text = { ...DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS, ...labels }
64
+ const subtitle = [displayNumber ?? previewProps.conversationId, text.destinationHint].join(' · ')
65
+
66
+ return (
67
+ <aside className="cv-simulator-panel" aria-label={text.title}>
68
+ <header className="cv-simulator-panel__header">
69
+ <div className="cv-simulator-panel__heading">
70
+ <h2 className="cv-simulator-panel__title">{text.title}</h2>
71
+ <p className="cv-simulator-panel__subtitle">{subtitle}</p>
72
+ </div>
73
+ <div className="cv-simulator-panel__actions">
74
+ {headerActions}
75
+ <button type="button" onClick={onClose} title={text.close} aria-label={text.close} className="cv-simulator-panel__close">
76
+ {/* SVG inline em vez de lucide-react: o pacote não impõe biblioteca de ícone ao host. */}
77
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
78
+ <path d="M18 6 6 18M6 6l12 12" />
79
+ </svg>
80
+ </button>
81
+ </div>
82
+ </header>
83
+
84
+ <div className="cv-simulator-panel__body">
85
+ <ConversationPreview {...previewProps} placeholder={text.placeholder} />
86
+ </div>
87
+ </aside>
88
+ )
89
+ }
@@ -25,6 +25,9 @@ export type { CreateMockSSEProviderParams } from './createMockSSEProvider'
25
25
 
26
26
  export { PREVIEW_CONVERSATIONS, PREVIEW_MESSAGES, PREVIEW_DOCUMENTS } from './previewFixtures'
27
27
 
28
+ export { ConversationSimulatorPanel, DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS } from './ConversationSimulatorPanel'
29
+ export type { ConversationSimulatorPanelProps, ConversationSimulatorPanelLabels } from './ConversationSimulatorPanel'
30
+
28
31
  export { ConversationPreview } from './ConversationPreview'
29
32
  export { mediaTypeOf } from './ConversationPreview'
30
33
  export type { ConversationPreviewProps, PreviewUploadedMedia } from './ConversationPreview'
package/src/styles.css CHANGED
@@ -192,3 +192,88 @@
192
192
  :where(button:not(:disabled), [role='button']:not(:disabled), summary) {
193
193
  cursor: pointer;
194
194
  }
195
+
196
+ /**
197
+ * Simulador de cliente. Largura fixa porque é painel auxiliar: a thread ao lado é que deve
198
+ * ganhar o espaço restante quando a janela encolhe.
199
+ */
200
+ .cv-simulator-panel {
201
+ display: flex;
202
+ flex-direction: column;
203
+ width: 24rem;
204
+ flex-shrink: 0;
205
+ border-left: 1px solid rgb(229 231 235);
206
+ background: #fff;
207
+ }
208
+
209
+ .cv-simulator-panel__header {
210
+ display: flex;
211
+ align-items: center;
212
+ justify-content: space-between;
213
+ gap: 0.5rem;
214
+ padding: 0.75rem 1rem;
215
+ border-bottom: 1px solid rgb(229 231 235);
216
+ }
217
+
218
+ .cv-simulator-panel__heading { min-width: 0; }
219
+
220
+ .cv-simulator-panel__title {
221
+ font-size: 0.875rem;
222
+ font-weight: 600;
223
+ color: rgb(17 24 39);
224
+ overflow: hidden;
225
+ text-overflow: ellipsis;
226
+ white-space: nowrap;
227
+ }
228
+
229
+ .cv-simulator-panel__subtitle {
230
+ font-size: 0.75rem;
231
+ color: rgb(156 163 175);
232
+ overflow: hidden;
233
+ text-overflow: ellipsis;
234
+ white-space: nowrap;
235
+ }
236
+
237
+ .cv-simulator-panel__actions {
238
+ display: flex;
239
+ align-items: center;
240
+ gap: 0.25rem;
241
+ }
242
+
243
+ .cv-simulator-panel__close {
244
+ display: inline-flex;
245
+ padding: 0.375rem;
246
+ border: 0;
247
+ border-radius: 0.5rem;
248
+ background: transparent;
249
+ color: rgb(156 163 175);
250
+ transition: color 150ms, background-color 150ms;
251
+ }
252
+
253
+ .cv-simulator-panel__close:hover {
254
+ color: rgb(55 65 81);
255
+ background: rgb(243 244 246);
256
+ }
257
+
258
+ /* `min-height: 0` para o transcript rolar dentro do painel em vez de esticar o `<aside>`. */
259
+ .cv-simulator-panel__body { flex: 1; min-height: 0; }
260
+
261
+ .dark .cv-simulator-panel {
262
+ border-left-color: rgb(31 41 55);
263
+ background: rgb(17 24 39);
264
+ }
265
+ .dark .cv-simulator-panel__header { border-bottom-color: rgb(31 41 55); }
266
+ .dark .cv-simulator-panel__title { color: rgb(243 244 246); }
267
+ .dark .cv-simulator-panel__subtitle { color: rgb(107 114 128); }
268
+ .dark .cv-simulator-panel__close:hover { color: rgb(229 231 235); background: rgb(31 41 55); }
269
+
270
+ /* Painel auxiliar não cabe ao lado da thread em tela estreita: vira sobreposição de tela cheia. */
271
+ @media (max-width: 1023px) {
272
+ .cv-simulator-panel {
273
+ position: fixed;
274
+ inset: 0;
275
+ width: 100%;
276
+ z-index: 40;
277
+ border-left: 0;
278
+ }
279
+ }