@adatechnology/conversations-ui 0.11.0 → 0.12.0

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.
@@ -190,6 +190,8 @@ type ParticipantConversationsLabels = {
190
190
  readonly edit: string;
191
191
  /** Name of the menu button on a failed message. Optional; translated hosts should fill it, otherwise the English default shows. */
192
192
  readonly messageOptions?: string;
193
+ /** Toggle that opens the message as received (e.g. the whole email). Optional; the English default shows otherwise. */
194
+ readonly showOriginalText?: string;
193
195
  readonly notFound: string;
194
196
  readonly loading: string;
195
197
  readonly loadError: string;
@@ -64,6 +64,7 @@ function formatRecordingElapsedLabel(template, elapsed, limit) {
64
64
 
65
65
  // src/participant/participantLabels.ts
66
66
  var DEFAULT_MESSAGE_OPTIONS_LABEL = "Message options";
67
+ var DEFAULT_SHOW_ORIGINAL_TEXT_LABEL = "Show original";
67
68
  var DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS = {
68
69
  inboxTitle: "Conversations",
69
70
  sectionAwaiting: "Waiting for your reply",
@@ -101,6 +102,7 @@ var DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS = {
101
102
  discard: "Discard",
102
103
  edit: "Edit",
103
104
  messageOptions: DEFAULT_MESSAGE_OPTIONS_LABEL,
105
+ showOriginalText: DEFAULT_SHOW_ORIGINAL_TEXT_LABEL,
104
106
  notFound: "Conversation not found",
105
107
  loading: "Loading\u2026",
106
108
  loadError: "Could not load. Check your connection and try again.",
@@ -2577,6 +2579,14 @@ function BubbleHeading({ content, labels }) {
2577
2579
  if (!content.authorName) return null;
2578
2580
  return /* @__PURE__ */ jsx27("span", { className: "cv-p-bubble__author", children: content.authorName });
2579
2581
  }
2582
+ function OriginalText({ content, labels }) {
2583
+ const original = content.originalText?.trim();
2584
+ if (!original || original === content.text?.trim()) return null;
2585
+ return /* @__PURE__ */ jsxs21("details", { className: "cv-p-bubble__original", children: [
2586
+ /* @__PURE__ */ jsx27("summary", { className: "cv-p-bubble__original-toggle", children: labels.showOriginalText ?? DEFAULT_SHOW_ORIGINAL_TEXT_LABEL }),
2587
+ /* @__PURE__ */ jsx27("div", { className: "cv-p-bubble__original-text", children: original })
2588
+ ] });
2589
+ }
2580
2590
  function BubbleBody({ content, labels, ownStatus, textId, resolveAttachmentUrl, onRetry, onDiscard, onEdit }) {
2581
2591
  return /* @__PURE__ */ jsxs21(Fragment6, { children: [
2582
2592
  /* @__PURE__ */ jsx27(BubbleHeading, { content, labels }),
@@ -2595,7 +2605,8 @@ function BubbleBody({ content, labels, ownStatus, textId, resolveAttachmentUrl,
2595
2605
  /* @__PURE__ */ jsx27("time", { className: "cv-p-bubble__time", dateTime: content.createdAt, children: formatTimestamp(content.createdAt) }),
2596
2606
  ownStatus ? /* @__PURE__ */ jsx27(OwnStatus, { status: ownStatus, labels, onRetry }) : null,
2597
2607
  ownStatus === "failed" ? /* @__PURE__ */ jsx27(ParticipantFailedMenu, { labels, onEdit, onDiscard }) : null
2598
- ] })
2608
+ ] }),
2609
+ /* @__PURE__ */ jsx27(OriginalText, { content, labels })
2599
2610
  ] });
2600
2611
  }
2601
2612
 
@@ -2669,6 +2680,7 @@ function describeItem(item, perspective) {
2669
2680
  isMine: isOwnMessage(message, perspective),
2670
2681
  authorName: message.authorName ?? void 0,
2671
2682
  text: message.text ?? void 0,
2683
+ originalText: message.originalText ?? void 0,
2672
2684
  createdAt: message.createdAt,
2673
2685
  attachments: message.attachments,
2674
2686
  pendingFilenames: [],
package/dist/styles.css CHANGED
@@ -1978,7 +1978,41 @@
1978
1978
  .cv-p-bubble > .cv-p-attachment {
1979
1979
  flex: 1 0 100%;
1980
1980
  }
1981
+ .cv-p-bubble__original {
1982
+ flex: 1 0 100%;
1983
+ margin-top: 0.2rem;
1984
+ }
1985
+ .cv-p-bubble__original-toggle {
1986
+ display: inline-block;
1987
+ font-size: 0.7rem;
1988
+ color: var(--cv-p-i-text-muted);
1989
+ cursor: pointer;
1990
+ list-style: none;
1991
+ text-decoration: underline;
1992
+ text-decoration-style: dotted;
1993
+ text-underline-offset: 2px;
1994
+ }
1995
+ .cv-p-bubble__original-toggle::-webkit-details-marker {
1996
+ display: none;
1997
+ }
1998
+ .cv-p-bubble__original-toggle:focus-visible {
1999
+ outline: 2px solid var(--cv-p-i-accent);
2000
+ outline-offset: 2px;
2001
+ border-radius: 2px;
2002
+ }
2003
+ .cv-p-bubble__original-text {
2004
+ margin-top: 0.3rem;
2005
+ padding-inline-start: 0.5rem;
2006
+ border-inline-start: 2px solid var(--cv-p-i-border);
2007
+ max-height: 16rem;
2008
+ overflow: auto;
2009
+ white-space: pre-wrap;
2010
+ overflow-wrap: anywhere;
2011
+ font-size: 0.8rem;
2012
+ color: var(--cv-p-i-text-muted);
2013
+ }
1981
2014
  .cv-p-bubble__meta {
2015
+ order: 1;
1982
2016
  display: flex;
1983
2017
  flex: 0 0 auto;
1984
2018
  justify-content: flex-end;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adatechnology/conversations-ui",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "WhatsApp conversation UI components — parametrizável por endpoint, tema e feature flags",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -35,7 +35,7 @@
35
35
  "clsx": "^2.1.1",
36
36
  "lucide-react": "^1.21.0",
37
37
  "tailwind-merge": "^3.6.0",
38
- "@adatechnology/conversation-contracts": "0.6.0",
38
+ "@adatechnology/conversation-contracts": "0.7.0",
39
39
  "@adatechnology/meta-whatsapp-contracts": "0.6.0"
40
40
  },
41
41
  "peerDependencies": {
@@ -4,7 +4,7 @@ import { OwnStatus } from './ParticipantBubbleStatus'
4
4
  import { ParticipantFailedMenu } from './ParticipantFailedMenu'
5
5
  import type { BubbleContent } from './ParticipantMessageBubble'
6
6
  import { ParticipantMessageText } from './ParticipantMessageText'
7
- import type { ParticipantConversationsLabels } from './participantLabels'
7
+ import { DEFAULT_SHOW_ORIGINAL_TEXT_LABEL, type ParticipantConversationsLabels } from './participantLabels'
8
8
  import type { ParticipantOwnMessageStatus } from './participantMessages'
9
9
 
10
10
  type BubbleBodyProps = {
@@ -24,6 +24,18 @@ function BubbleHeading({ content, labels }: Pick<BubbleBodyProps, 'content' | 'l
24
24
  return <span className="cv-p-bubble__author">{content.authorName}</span>
25
25
  }
26
26
 
27
+ /** Collapsed by default: the trimmed text is what the reader needs, the original is there on demand. */
28
+ function OriginalText({ content, labels }: Pick<BubbleBodyProps, 'content' | 'labels'>) {
29
+ const original = content.originalText?.trim()
30
+ if (!original || original === content.text?.trim()) return null
31
+ return (
32
+ <details className="cv-p-bubble__original">
33
+ <summary className="cv-p-bubble__original-toggle">{labels.showOriginalText ?? DEFAULT_SHOW_ORIGINAL_TEXT_LABEL}</summary>
34
+ <div className="cv-p-bubble__original-text">{original}</div>
35
+ </details>
36
+ )
37
+ }
38
+
27
39
  export function BubbleBody({ content, labels, ownStatus, textId, resolveAttachmentUrl, onRetry, onDiscard, onEdit }: BubbleBodyProps) {
28
40
  return (
29
41
  <>
@@ -49,6 +61,8 @@ export function BubbleBody({ content, labels, ownStatus, textId, resolveAttachme
49
61
  {ownStatus ? <OwnStatus status={ownStatus} labels={labels} onRetry={onRetry} /> : null}
50
62
  {ownStatus === 'failed' ? <ParticipantFailedMenu labels={labels} onEdit={onEdit} onDiscard={onDiscard} /> : null}
51
63
  </span>
64
+ {/* Last child so the ids React derives for the siblings above stay as they were; CSS keeps the meta after it. */}
65
+ <OriginalText content={content} labels={labels} />
52
66
  </>
53
67
  )
54
68
  }
@@ -70,6 +70,41 @@ describe('ParticipantMessageBubble', () => {
70
70
  expect(mine).not.toContain('Marina')
71
71
  })
72
72
 
73
+ it('keeps the original text collapsed behind a discreet toggle inside the bubble', () => {
74
+ const original = 'Pode descarregar.\n\nEm ter., 20 de out. de 2026, Equipe escreveu:\n> Autorizam?'
75
+ const markup = renderServer(buildMessage({ id: 'a', text: 'Pode descarregar.', originalText: original }))
76
+
77
+ expect(markup).toContain('<details class="cv-p-bubble__original">')
78
+ expect(markup).not.toContain('<details class="cv-p-bubble__original" open')
79
+ expect(markup).toContain('<summary class="cv-p-bubble__original-toggle">Show original</summary>')
80
+ expect(markup).toContain('Em ter., 20 de out. de 2026, Equipe escreveu:')
81
+ expect(findUtilityClassTokens(markup)).toEqual([])
82
+ })
83
+
84
+ it('uses the host label for the original-text toggle', () => {
85
+ const item: ParticipantTimelineItem = {
86
+ kind: 'server',
87
+ message: buildMessage({ id: 'a', text: 'Ok', originalText: 'Ok\n> antes' }),
88
+ }
89
+ const markup = renderToStaticMarkup(
90
+ <ParticipantMessageBubble
91
+ item={item}
92
+ labels={{ ...LABELS, showOriginalText: 'ver histórico' }}
93
+ confirmsRead
94
+ resolveAttachmentUrl={resolveAttachmentUrl}
95
+ />,
96
+ )
97
+
98
+ expect(markup).toContain('>ver histórico</summary>')
99
+ })
100
+
101
+ it('shows no toggle when the original is absent, blank or the same as the text', () => {
102
+ expect(renderServer(buildMessage({ id: 'a', text: 'Ok' }))).not.toContain('<details')
103
+ expect(renderServer(buildMessage({ id: 'b', text: 'Ok', originalText: null }))).not.toContain('<details')
104
+ expect(renderServer(buildMessage({ id: 'c', text: 'Ok', originalText: ' ' }))).not.toContain('<details')
105
+ expect(renderServer(buildMessage({ id: 'd', text: 'Ok', originalText: ' Ok\n' }))).not.toContain('<details')
106
+ })
107
+
73
108
  it('renders a time element with the ISO dateTime', () => {
74
109
  expect(renderServer()).toMatch(/<time[^>]*dateTime="2026-10-01T10:00:00.000Z"/)
75
110
  })
@@ -37,6 +37,7 @@ export type BubbleContent = {
37
37
  readonly isMine: boolean
38
38
  readonly authorName?: string
39
39
  readonly text?: string
40
+ readonly originalText?: string
40
41
  readonly createdAt: string
41
42
  readonly attachments: readonly ParticipantAttachment[]
42
43
  readonly pendingFilenames: readonly string[]
@@ -51,6 +52,7 @@ function describeItem(item: ParticipantTimelineItem, perspective?: ParticipantPe
51
52
  isMine: isOwnMessage(message, perspective),
52
53
  authorName: message.authorName ?? undefined,
53
54
  text: message.text ?? undefined,
55
+ originalText: message.originalText ?? undefined,
54
56
  createdAt: message.createdAt,
55
57
  attachments: message.attachments,
56
58
  pendingFilenames: [],
@@ -1,6 +1,7 @@
1
1
  import { DEFAULT_PARTICIPANT_AUDIO_LABELS, type ParticipantAudioLabels } from './participantAudioLabels'
2
2
 
3
3
  export const DEFAULT_MESSAGE_OPTIONS_LABEL = 'Message options'
4
+ export const DEFAULT_SHOW_ORIGINAL_TEXT_LABEL = 'Show original'
4
5
 
5
6
  export type ParticipantConversationsLabels = {
6
7
  readonly inboxTitle: string
@@ -43,6 +44,8 @@ export type ParticipantConversationsLabels = {
43
44
  readonly edit: string
44
45
  /** Name of the menu button on a failed message. Optional; translated hosts should fill it, otherwise the English default shows. */
45
46
  readonly messageOptions?: string
47
+ /** Toggle that opens the message as received (e.g. the whole email). Optional; the English default shows otherwise. */
48
+ readonly showOriginalText?: string
46
49
  readonly notFound: string
47
50
  readonly loading: string
48
51
  readonly loadError: string
@@ -106,6 +109,7 @@ export const DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS: ParticipantConversationsL
106
109
  discard: 'Discard',
107
110
  edit: 'Edit',
108
111
  messageOptions: DEFAULT_MESSAGE_OPTIONS_LABEL,
112
+ showOriginalText: DEFAULT_SHOW_ORIGINAL_TEXT_LABEL,
109
113
  notFound: 'Conversation not found',
110
114
  loading: 'Loading…',
111
115
  loadError: 'Could not load. Check your connection and try again.',
package/src/styles.css CHANGED
@@ -1302,7 +1302,12 @@
1302
1302
  .cv-p-bubble__author { display: block; flex: 1 0 100%; margin-bottom: 0.15rem; font: 0.65rem 'SFMono-Regular', Consolas, monospace; text-transform: uppercase; color: var(--cv-p-i-accent); }
1303
1303
  .cv-p-bubble > .cv-p-text { flex: 0 1 auto; min-width: 0; margin-inline-end: auto; }
1304
1304
  .cv-p-bubble > .cv-p-attachment { flex: 1 0 100%; }
1305
- .cv-p-bubble__meta { display: flex; flex: 0 0 auto; justify-content: flex-end; align-items: center; gap: 0.35rem; margin-inline-start: auto; margin-top: 0.25rem; font: 0.65rem 'SFMono-Regular', Consolas, monospace; color: var(--cv-p-i-text-muted); }
1305
+ .cv-p-bubble__original { flex: 1 0 100%; margin-top: 0.2rem; }
1306
+ .cv-p-bubble__original-toggle { display: inline-block; font-size: 0.7rem; color: var(--cv-p-i-text-muted); cursor: pointer; list-style: none; text-decoration: underline; text-decoration-style: dotted; text-underline-offset: 2px; }
1307
+ .cv-p-bubble__original-toggle::-webkit-details-marker { display: none; }
1308
+ .cv-p-bubble__original-toggle:focus-visible { outline: 2px solid var(--cv-p-i-accent); outline-offset: 2px; border-radius: 2px; }
1309
+ .cv-p-bubble__original-text { margin-top: 0.3rem; padding-inline-start: 0.5rem; border-inline-start: 2px solid var(--cv-p-i-border); max-height: 16rem; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; font-size: 0.8rem; color: var(--cv-p-i-text-muted); }
1310
+ .cv-p-bubble__meta { order: 1; display: flex; flex: 0 0 auto; justify-content: flex-end; align-items: center; gap: 0.35rem; margin-inline-start: auto; margin-top: 0.25rem; font: 0.65rem 'SFMono-Regular', Consolas, monospace; color: var(--cv-p-i-text-muted); }
1306
1311
  .cv-p-bubble__status { display: inline-flex; align-items: center; gap: 0.25rem; }
1307
1312
  .cv-p-bubble .cv-status-ticks--queued, .cv-p-bubble .cv-status-ticks--sent, .cv-p-bubble .cv-status-ticks--delivered { color: var(--cv-p-i-tick); }
1308
1313
  .cv-p-bubble .cv-status-ticks--read { color: var(--cv-p-i-tick-read); }