@adatechnology/conversations-ui 0.1.0-rc.7 → 0.1.0-rc.9

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.
@@ -151,9 +151,63 @@ function waToHTMLInline(text) {
151
151
  return escapeHtml(text).replace(/\*([^*\n]+)\*/g, "<strong>$1</strong>").replace(/_([^_\n]+)_/g, "<em>$1</em>").replace(/~([^~\n]+)~/g, "<del>$1</del>").replace(/`([^`\n]+)`/g, "<code>$1</code>");
152
152
  }
153
153
 
154
+ // src/useDarkMode.ts
155
+ import { useState, useEffect, useCallback } from "react";
156
+ var STORAGE_KEY = "conversations-ui-dark-mode";
157
+ function getInitialDark() {
158
+ if (typeof window === "undefined") return false;
159
+ const stored = localStorage.getItem(STORAGE_KEY);
160
+ if (stored !== null) {
161
+ return stored === "true";
162
+ }
163
+ return window.matchMedia("(prefers-color-scheme: dark)").matches;
164
+ }
165
+ function useIsDarkTheme() {
166
+ const [isDark, setIsDark] = useState(
167
+ () => typeof document !== "undefined" && document.documentElement.classList.contains("dark")
168
+ );
169
+ useEffect(() => {
170
+ const root = document.documentElement;
171
+ const observer = new MutationObserver(() => setIsDark(root.classList.contains("dark")));
172
+ observer.observe(root, { attributes: true, attributeFilter: ["class"] });
173
+ setIsDark(root.classList.contains("dark"));
174
+ return () => observer.disconnect();
175
+ }, []);
176
+ return isDark;
177
+ }
178
+ function useDarkMode() {
179
+ const [isDark, setIsDark] = useState(getInitialDark);
180
+ useEffect(() => {
181
+ const root = document.documentElement;
182
+ if (isDark) {
183
+ root.classList.add("dark");
184
+ } else {
185
+ root.classList.remove("dark");
186
+ }
187
+ localStorage.setItem(STORAGE_KEY, String(isDark));
188
+ }, [isDark]);
189
+ useEffect(() => {
190
+ const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
191
+ const handleChange = (event) => {
192
+ const stored = localStorage.getItem(STORAGE_KEY);
193
+ if (stored === null) {
194
+ setIsDark(event.matches);
195
+ }
196
+ };
197
+ mediaQuery.addEventListener("change", handleChange);
198
+ return () => mediaQuery.removeEventListener("change", handleChange);
199
+ }, []);
200
+ const toggle = useCallback(() => {
201
+ setIsDark((prev) => !prev);
202
+ }, []);
203
+ return { isDark, toggle };
204
+ }
205
+
154
206
  export {
155
207
  parseWhatsAppFormatting,
156
208
  waToHTML,
157
209
  htmlToWA,
158
- waToHTMLInline
210
+ waToHTMLInline,
211
+ useIsDarkTheme,
212
+ useDarkMode
159
213
  };
@@ -1,6 +1,7 @@
1
1
  import {
2
- parseWhatsAppFormatting
3
- } from "./chunk-OGRRHQQW.js";
2
+ parseWhatsAppFormatting,
3
+ useIsDarkTheme
4
+ } from "./chunk-2AYDBWNE.js";
4
5
 
5
6
  // src/ConversationLocalesProvider.tsx
6
7
  import { createContext, useContext } from "react";
@@ -394,6 +395,7 @@ function Lightbox({ imageUrl, caption, onClose, labels }) {
394
395
  // src/InteractiveMessage.tsx
395
396
  import { useState as useState3 } from "react";
396
397
  import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
398
+ var FORMATTING_CLASSES = "[&_strong]:font-bold [&_em]:italic [&_del]:line-through";
397
399
  var DEFAULT_INTERACTIVE_MESSAGE_LABELS = {
398
400
  openList: "Ver op\xE7\xF5es"
399
401
  };
@@ -411,9 +413,18 @@ function InteractiveMessage({ payload, onSelect, labels, className }) {
411
413
  const hasRows = collectRows(payload).length > 0;
412
414
  const isInteractable = onSelect !== void 0;
413
415
  return /* @__PURE__ */ jsxs5("div", { className: cn("flex flex-col gap-1", className), children: [
414
- payload.header?.text ? /* @__PURE__ */ jsx7("p", { className: "text-sm font-semibold text-gray-900 dark:text-gray-100", children: payload.header.text }) : null,
415
- payload.body?.text ? /* @__PURE__ */ jsx7("p", { className: "whitespace-pre-wrap break-words text-sm text-gray-900 dark:text-gray-100", children: payload.body.text }) : null,
416
- payload.footer?.text ? /* @__PURE__ */ jsx7("p", { className: "text-xs text-gray-500 dark:text-gray-400", children: payload.footer.text }) : null,
416
+ payload.header?.text ? /* @__PURE__ */ jsx7("p", { className: cn("text-sm font-semibold text-gray-900 dark:text-gray-100", FORMATTING_CLASSES), children: parseWhatsAppFormatting(payload.header.text) }) : null,
417
+ payload.body?.text ? /* @__PURE__ */ jsx7(
418
+ "p",
419
+ {
420
+ className: cn(
421
+ "whitespace-pre-wrap break-words text-sm text-gray-900 dark:text-gray-100",
422
+ FORMATTING_CLASSES
423
+ ),
424
+ children: parseWhatsAppFormatting(payload.body.text)
425
+ }
426
+ ) : null,
427
+ payload.footer?.text ? /* @__PURE__ */ jsx7("p", { className: cn("text-xs text-gray-500 dark:text-gray-400", FORMATTING_CLASSES), children: parseWhatsAppFormatting(payload.footer.text) }) : null,
417
428
  buttons.length > 0 ? /* @__PURE__ */ jsx7("div", { className: "mt-1 flex flex-col gap-1 border-t border-gray-200 pt-1 dark:border-gray-700", children: buttons.map((button) => /* @__PURE__ */ jsx7(
418
429
  "button",
419
430
  {
@@ -606,8 +617,32 @@ function MessageBubble({
606
617
 
607
618
  // src/Wallpaper.tsx
608
619
  import { jsx as jsx10 } from "react/jsx-runtime";
609
- function ConversationWallpaper({ children, className }) {
610
- return /* @__PURE__ */ jsx10("div", { className: cn("cv-wallpaper", className), children });
620
+ function doodlePattern(strokeColor, opacity) {
621
+ const svg = `<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'><g fill='none' stroke='${strokeColor}' stroke-width='1.2' opacity='${opacity}'><circle cx='15' cy='15' r='3'/><path d='M40 10 q5 8 0 16 q-5 -8 0 -16z'/><circle cx='70' cy='28' r='2'/><path d='M18 55 l6 6 m-6 0 l6 -6'/><circle cx='55' cy='68' r='2.5'/><path d='M85 58 q6 6 0 12 q-6 -6 0 -12z'/><circle cx='8' cy='85' r='2'/><path d='M65 90 l5 5 m-5 0 l5 -5'/></g></svg>`;
622
+ return `url("data:image/svg+xml,${encodeURIComponent(svg)}")`;
623
+ }
624
+ var LIGHT_WALLPAPER = {
625
+ backgroundColor: "#efeae2",
626
+ backgroundImage: doodlePattern("#d7cfc0", 0.7),
627
+ backgroundRepeat: "repeat",
628
+ backgroundSize: "100px 100px"
629
+ };
630
+ var DARK_WALLPAPER = {
631
+ backgroundColor: "#0b141a",
632
+ backgroundImage: doodlePattern("#19232a", 0.9),
633
+ backgroundRepeat: "repeat",
634
+ backgroundSize: "100px 100px"
635
+ };
636
+ function ConversationWallpaper({ children, className, style }) {
637
+ const isDark = useIsDarkTheme();
638
+ return /* @__PURE__ */ jsx10(
639
+ "div",
640
+ {
641
+ className: cn("cv-wallpaper", className),
642
+ style: { ...isDark ? DARK_WALLPAPER : LIGHT_WALLPAPER, ...style },
643
+ children
644
+ }
645
+ );
611
646
  }
612
647
 
613
648
  // src/emojiCatalog.ts
@@ -1,9 +1,7 @@
1
1
  import {
2
+ parseWhatsAppFormatting,
2
3
  useIsDarkTheme
3
- } from "../chunk-NV2RZ5KT.js";
4
- import {
5
- parseWhatsAppFormatting
6
- } from "../chunk-OGRRHQQW.js";
4
+ } from "../chunk-2AYDBWNE.js";
7
5
 
8
6
  // src/flows/FlowNodeCard.tsx
9
7
  import { Handle, Position } from "@xyflow/react";
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import react__default, { ReactNode, FormEvent } from 'react';
2
+ import react__default, { ReactNode, CSSProperties, FormEvent } from 'react';
3
3
  import { y as MessagePayload, A as ResolveMediaUrl, v as InteractiveSelection, t as InteractivePayload, p as ConversationsFeatures, m as ConversationSummary, h as ConversationChannel, L as ListConversationsParams, o as ConversationsApi, S as SSEProvider, w as ListDocumentsParams, i as ConversationDocument, n as ConversationTemplate } from './types-B5C1DLu1.js';
4
4
  export { C as CHANNEL_CAPABILITIES, a as CHANNEL_FILTER_ALL, b as CONVERSATION_CHANNEL, c as ChannelCapabilities, d as ChannelFilter, e as ChannelFilterOption, f as CompanyDocument, g as CompanyDocumentPage, j as ConversationDocumentPage, k as ConversationEventSource, l as ConversationPage, q as ConversationsTheme, r as ConversationsUIConfig, D as DEFAULT_CONVERSATION_CHANNEL, F as FormatContactHandleParams, H as HANDLE_KIND, s as HandleKind, I as InteractiveOption, u as InteractiveSection, M as MediaRenderer, x as MediaRendererProps, R as REOPEN_MECHANISM, z as ReopenMechanism, B as capabilitiesOf, E as channelFiltersFor, G as contactFlag, J as formatContactHandle } from './types-B5C1DLu1.js';
5
5
 
@@ -41,8 +41,10 @@ declare function InteractiveMessage({ payload, onSelect, labels, className }: In
41
41
  interface ConversationWallpaperProps {
42
42
  children?: ReactNode;
43
43
  className?: string;
44
+ /** Ajusta ou substitui o fundo padrão — para produto com identidade visual própria. */
45
+ style?: CSSProperties;
44
46
  }
45
- declare function ConversationWallpaper({ children, className }: ConversationWallpaperProps): react.JSX.Element;
47
+ declare function ConversationWallpaper({ children, className, style }: ConversationWallpaperProps): react.JSX.Element;
46
48
 
47
49
  interface ConversationLocales {
48
50
  bubble: {
package/dist/index.js CHANGED
@@ -1,7 +1,3 @@
1
- import {
2
- useDarkMode,
3
- useIsDarkTheme
4
- } from "./chunk-NV2RZ5KT.js";
5
1
  import {
6
2
  AudioPlayer,
7
3
  ConversationDocumentsPanel,
@@ -43,13 +39,15 @@ import {
43
39
  useConversationDocuments,
44
40
  useConversationLocales,
45
41
  useConversations
46
- } from "./chunk-TGTBMMFC.js";
42
+ } from "./chunk-G5BM3VBP.js";
47
43
  import {
48
44
  htmlToWA,
49
45
  parseWhatsAppFormatting,
46
+ useDarkMode,
47
+ useIsDarkTheme,
50
48
  waToHTML,
51
49
  waToHTMLInline
52
- } from "./chunk-OGRRHQQW.js";
50
+ } from "./chunk-2AYDBWNE.js";
53
51
 
54
52
  // src/WhatsAppMessageEditor.tsx
55
53
  import { useRef } from "react";
@@ -6,8 +6,8 @@ import {
6
6
  DocumentsLibrary,
7
7
  MessageBubble,
8
8
  MessageComposer
9
- } from "../chunk-TGTBMMFC.js";
10
- import "../chunk-OGRRHQQW.js";
9
+ } from "../chunk-G5BM3VBP.js";
10
+ import "../chunk-2AYDBWNE.js";
11
11
 
12
12
  // src/preview/previewStore.ts
13
13
  var GLOBAL_CHANNEL = "global";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adatechnology/conversations-ui",
3
- "version": "0.1.0-rc.7",
3
+ "version": "0.1.0-rc.9",
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,41 @@
1
+ import { describe, expect, it } from 'bun:test'
2
+ import { renderToStaticMarkup } from 'react-dom/server'
3
+
4
+ import { InteractiveMessage } from './InteractiveMessage'
5
+ import type { InteractivePayload } from './types'
6
+
7
+ function render(payload: InteractivePayload): string {
8
+ return renderToStaticMarkup(<InteractiveMessage payload={payload} />)
9
+ }
10
+
11
+ describe('InteractiveMessage', () => {
12
+ it('formata negrito no corpo, como o aparelho faz', () => {
13
+ const markup = render({ type: 'button', body: { text: 'Qual o *prazo desejado*?' } })
14
+
15
+ expect(markup).toContain('<strong>prazo desejado</strong>')
16
+ expect(markup).not.toContain('*prazo desejado*')
17
+ })
18
+
19
+ it('formata cabeçalho e rodapé também', () => {
20
+ const markup = render({
21
+ type: 'list',
22
+ header: { text: '*Menu*' },
23
+ body: { text: 'corpo' },
24
+ footer: { text: '_Selecione_' },
25
+ })
26
+
27
+ expect(markup).toContain('<strong>Menu</strong>')
28
+ expect(markup).toContain('<em>Selecione</em>')
29
+ })
30
+
31
+ it('deixa título de opção literal, porque o WhatsApp não formata ali', () => {
32
+ const markup = render({
33
+ type: 'button',
34
+ body: { text: 'corpo' },
35
+ action: { buttons: [{ reply: { id: 'sim', title: '*Sim*' } }] },
36
+ })
37
+
38
+ expect(markup).toContain('*Sim*')
39
+ expect(markup).not.toContain('<strong>Sim</strong>')
40
+ })
41
+ })
@@ -13,6 +13,10 @@
13
13
  import { useState } from 'react'
14
14
  import type { InteractiveOption, InteractivePayload, InteractiveSelection } from './types'
15
15
  import { cn } from './lib/cn'
16
+ import { parseWhatsAppFormatting } from './lib/whatsapp-formatting'
17
+
18
+ /** `parseWhatsAppFormatting` emite `<strong>`/`<em>`/`<del>`, que o reset do Tailwind zera. */
19
+ const FORMATTING_CLASSES = '[&_strong]:font-bold [&_em]:italic [&_del]:line-through'
16
20
 
17
21
  export interface InteractiveMessageLabels {
18
22
  /** Fallback do rótulo do botão que abre a lista, quando o payload não traz um. */
@@ -51,18 +55,31 @@ export function InteractiveMessage({ payload, onSelect, labels, className }: Int
51
55
 
52
56
  return (
53
57
  <div className={cn('flex flex-col gap-1', className)}>
58
+ {/* Cabeçalho, corpo e rodapé passam pela formatação do WhatsApp — o aparelho renderiza
59
+ `*negrito*` neles como em qualquer texto. Título e descrição de opção NÃO: ali o WhatsApp
60
+ mostra os asteriscos literais, e formatá-los aqui deixaria o simulador mais bonito que o
61
+ aparelho, que é o tipo de divergência que só aparece com o cliente na frente. */}
54
62
  {payload.header?.text ? (
55
- <p className="text-sm font-semibold text-gray-900 dark:text-gray-100">{payload.header.text}</p>
63
+ <p className={cn('text-sm font-semibold text-gray-900 dark:text-gray-100', FORMATTING_CLASSES)}>
64
+ {parseWhatsAppFormatting(payload.header.text)}
65
+ </p>
56
66
  ) : null}
57
67
 
58
68
  {payload.body?.text ? (
59
- <p className="whitespace-pre-wrap break-words text-sm text-gray-900 dark:text-gray-100">
60
- {payload.body.text}
69
+ <p
70
+ className={cn(
71
+ 'whitespace-pre-wrap break-words text-sm text-gray-900 dark:text-gray-100',
72
+ FORMATTING_CLASSES,
73
+ )}
74
+ >
75
+ {parseWhatsAppFormatting(payload.body.text)}
61
76
  </p>
62
77
  ) : null}
63
78
 
64
79
  {payload.footer?.text ? (
65
- <p className="text-xs text-gray-500 dark:text-gray-400">{payload.footer.text}</p>
80
+ <p className={cn('text-xs text-gray-500 dark:text-gray-400', FORMATTING_CLASSES)}>
81
+ {parseWhatsAppFormatting(payload.footer.text)}
82
+ </p>
66
83
  ) : null}
67
84
 
68
85
  {buttons.length > 0 ? (
@@ -0,0 +1,21 @@
1
+ import { describe, expect, it } from 'bun:test'
2
+ import { renderToStaticMarkup } from 'react-dom/server'
3
+
4
+ import { ConversationWallpaper } from './Wallpaper'
5
+
6
+ describe('ConversationWallpaper', () => {
7
+ it('desenha o fundo sozinho, sem depender do styles.css do host', () => {
8
+ const markup = renderToStaticMarkup(<ConversationWallpaper />)
9
+
10
+ expect(markup).toContain('background-color:#efeae2')
11
+ expect(markup).toContain('background-image:url(&quot;data:image/svg+xml,')
12
+ expect(markup).toContain('cv-wallpaper')
13
+ })
14
+
15
+ it('deixa o produto sobrescrever o fundo por style', () => {
16
+ const markup = renderToStaticMarkup(<ConversationWallpaper style={{ backgroundColor: '#123456' }} />)
17
+
18
+ expect(markup).toContain('background-color:#123456')
19
+ expect(markup).not.toContain('background-color:#efeae2')
20
+ })
21
+ })
package/src/Wallpaper.tsx CHANGED
@@ -1,15 +1,61 @@
1
- import type { ReactNode } from 'react'
1
+ /**
2
+ * Fundo da conversa — a textura clara/escura do WhatsApp.
3
+ *
4
+ * O fundo vem embutido, e não da folha de estilo do pacote: `styles.css` é um import opcional que o
5
+ * host precisa lembrar de fazer, e esquecê-lo deixava a conversa sobre branco liso, sem erro nenhum
6
+ * para denunciar o problema. Fundo é identidade do componente, não tema do host.
7
+ *
8
+ * A classe `cv-wallpaper` continua no elemento para quem já sobrescreve por CSS.
9
+ */
10
+
11
+ import type { CSSProperties, ReactNode } from 'react'
2
12
 
3
13
  import { cn } from './lib/cn'
14
+ import { useIsDarkTheme } from './useDarkMode'
15
+
16
+ /** Textura do WhatsApp: rabiscos esparsos que se repetem a cada 100px. */
17
+ function doodlePattern(strokeColor: string, opacity: number): string {
18
+ const svg =
19
+ `<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'>` +
20
+ `<g fill='none' stroke='${strokeColor}' stroke-width='1.2' opacity='${opacity}'>` +
21
+ `<circle cx='15' cy='15' r='3'/><path d='M40 10 q5 8 0 16 q-5 -8 0 -16z'/>` +
22
+ `<circle cx='70' cy='28' r='2'/><path d='M18 55 l6 6 m-6 0 l6 -6'/>` +
23
+ `<circle cx='55' cy='68' r='2.5'/><path d='M85 58 q6 6 0 12 q-6 -6 0 -12z'/>` +
24
+ `<circle cx='8' cy='85' r='2'/><path d='M65 90 l5 5 m-5 0 l5 -5'/>` +
25
+ `</g></svg>`
26
+ return `url("data:image/svg+xml,${encodeURIComponent(svg)}")`
27
+ }
28
+
29
+ const LIGHT_WALLPAPER: CSSProperties = {
30
+ backgroundColor: '#efeae2',
31
+ backgroundImage: doodlePattern('#d7cfc0', 0.7),
32
+ backgroundRepeat: 'repeat',
33
+ backgroundSize: '100px 100px',
34
+ }
35
+
36
+ const DARK_WALLPAPER: CSSProperties = {
37
+ backgroundColor: '#0b141a',
38
+ backgroundImage: doodlePattern('#19232a', 0.9),
39
+ backgroundRepeat: 'repeat',
40
+ backgroundSize: '100px 100px',
41
+ }
4
42
 
5
43
  export interface ConversationWallpaperProps {
6
44
  children?: ReactNode
7
45
  className?: string
46
+ /** Ajusta ou substitui o fundo padrão — para produto com identidade visual própria. */
47
+ style?: CSSProperties
8
48
  }
9
49
 
10
- // Requer o import de '@adatechnology/conversations-ui/styles.css' uma vez no host —
11
- // a classe `.cv-wallpaper` (e sua variante `.dark`) vive nesse stylesheet, não em CSS-in-JS,
12
- // para não depender da configuração de Tailwind do consumidor.
13
- export function ConversationWallpaper({ children, className }: ConversationWallpaperProps) {
14
- return <div className={cn('cv-wallpaper', className)}>{children}</div>
50
+ export function ConversationWallpaper({ children, className, style }: ConversationWallpaperProps) {
51
+ const isDark = useIsDarkTheme()
52
+
53
+ return (
54
+ <div
55
+ className={cn('cv-wallpaper', className)}
56
+ style={{ ...(isDark ? DARK_WALLPAPER : LIGHT_WALLPAPER), ...style }}
57
+ >
58
+ {children}
59
+ </div>
60
+ )
15
61
  }
@@ -1,56 +0,0 @@
1
- // src/useDarkMode.ts
2
- import { useState, useEffect, useCallback } from "react";
3
- var STORAGE_KEY = "conversations-ui-dark-mode";
4
- function getInitialDark() {
5
- if (typeof window === "undefined") return false;
6
- const stored = localStorage.getItem(STORAGE_KEY);
7
- if (stored !== null) {
8
- return stored === "true";
9
- }
10
- return window.matchMedia("(prefers-color-scheme: dark)").matches;
11
- }
12
- function useIsDarkTheme() {
13
- const [isDark, setIsDark] = useState(
14
- () => typeof document !== "undefined" && document.documentElement.classList.contains("dark")
15
- );
16
- useEffect(() => {
17
- const root = document.documentElement;
18
- const observer = new MutationObserver(() => setIsDark(root.classList.contains("dark")));
19
- observer.observe(root, { attributes: true, attributeFilter: ["class"] });
20
- setIsDark(root.classList.contains("dark"));
21
- return () => observer.disconnect();
22
- }, []);
23
- return isDark;
24
- }
25
- function useDarkMode() {
26
- const [isDark, setIsDark] = useState(getInitialDark);
27
- useEffect(() => {
28
- const root = document.documentElement;
29
- if (isDark) {
30
- root.classList.add("dark");
31
- } else {
32
- root.classList.remove("dark");
33
- }
34
- localStorage.setItem(STORAGE_KEY, String(isDark));
35
- }, [isDark]);
36
- useEffect(() => {
37
- const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
38
- const handleChange = (event) => {
39
- const stored = localStorage.getItem(STORAGE_KEY);
40
- if (stored === null) {
41
- setIsDark(event.matches);
42
- }
43
- };
44
- mediaQuery.addEventListener("change", handleChange);
45
- return () => mediaQuery.removeEventListener("change", handleChange);
46
- }, []);
47
- const toggle = useCallback(() => {
48
- setIsDark((prev) => !prev);
49
- }, []);
50
- return { isDark, toggle };
51
- }
52
-
53
- export {
54
- useIsDarkTheme,
55
- useDarkMode
56
- };