@adatechnology/conversations-ui 0.8.0 → 0.10.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.
Files changed (129) hide show
  1. package/README.md +106 -0
  2. package/dist/{ConversationSimulatorPanel-q-wVgBCN.d.ts → ConversationSimulatorPanel-P6eqTSAO.d.ts} +1 -16
  3. package/dist/{chunk-HMCOTZAX.js → chunk-NSOAUYNE.js} +33 -28
  4. package/dist/{chunk-O6457XHK.js → chunk-WCZNIJ3M.js} +16 -1
  5. package/dist/{chunk-L3OFAQPS.js → chunk-Y326LLEP.js} +4 -14
  6. package/dist/chunk-ZSOD5AQT.js +19 -0
  7. package/dist/flows/index.js +4 -2
  8. package/dist/index.d.ts +10 -3
  9. package/dist/index.js +8 -6
  10. package/dist/participant/index.d.ts +54 -3
  11. package/dist/participant/index.js +1722 -418
  12. package/dist/preview/index.d.ts +2 -2
  13. package/dist/preview/index.js +2 -2
  14. package/dist/styles.css +281 -23
  15. package/package.json +3 -2
  16. package/src/AudioRecorderButton.tsx +4 -28
  17. package/src/Tooltip.tsx +5 -12
  18. package/src/TooltipDom.test.tsx +78 -0
  19. package/src/audioRecorderFormat.ts +27 -0
  20. package/src/domHarness.test-helper.ts +53 -0
  21. package/src/participant/ConversationThread.tsx +4 -0
  22. package/src/participant/ParticipantAttachmentItem.tsx +7 -10
  23. package/src/participant/ParticipantAudioAttachment.tsx +34 -0
  24. package/src/participant/ParticipantAudioPlayer.tsx +68 -0
  25. package/src/participant/ParticipantAudioPlayerControls.tsx +68 -0
  26. package/src/participant/ParticipantAudioRecorderView.tsx +55 -0
  27. package/src/participant/ParticipantAudioTranscription.tsx +90 -0
  28. package/src/participant/ParticipantBackButton.tsx +1 -1
  29. package/src/participant/ParticipantBubbleBody.tsx +54 -0
  30. package/src/participant/ParticipantBubbleStatus.tsx +22 -28
  31. package/src/participant/ParticipantChannelBadges.tsx +10 -2
  32. package/src/participant/ParticipantComposer.tsx +51 -16
  33. package/src/participant/ParticipantComposerAudio.tsx +95 -0
  34. package/src/participant/ParticipantComposerControls.tsx +2 -0
  35. package/src/participant/ParticipantConversations.tsx +3 -0
  36. package/src/participant/ParticipantCopyableToken.tsx +1 -0
  37. package/src/participant/ParticipantFailedMenu.tsx +40 -0
  38. package/src/participant/ParticipantFailedMenuList.tsx +41 -0
  39. package/src/participant/ParticipantMessageBubble.test.tsx +6 -6
  40. package/src/participant/ParticipantMessageBubble.tsx +20 -52
  41. package/src/participant/ParticipantMessageText.test.tsx +1 -1
  42. package/src/participant/ParticipantMessageText.tsx +4 -2
  43. package/src/participant/ParticipantProtocolBadge.tsx +1 -0
  44. package/src/participant/ParticipantRecorderParts.tsx +118 -0
  45. package/src/participant/ParticipantScrollToLatest.tsx +1 -1
  46. package/src/participant/ParticipantThread.tsx +30 -12
  47. package/src/participant/ParticipantThreadHeader.test.tsx +1 -1
  48. package/src/participant/ParticipantThreadScreen.tsx +1 -0
  49. package/src/participant/failedMenuKeyboard.ts +29 -0
  50. package/src/participant/index.ts +3 -0
  51. package/src/participant/participantAudio.types.ts +10 -0
  52. package/src/participant/participantAudioCapture.test.ts +15 -0
  53. package/src/participant/participantAudioCapture.ts +85 -0
  54. package/src/participant/participantAudioCoordinator.ts +13 -0
  55. package/src/participant/participantAudioFormat.test.ts +93 -0
  56. package/src/participant/participantAudioFormat.ts +72 -0
  57. package/src/participant/participantAudioGolden.test.tsx +168 -0
  58. package/src/participant/participantAudioLabels.test.ts +22 -0
  59. package/src/participant/participantAudioLabels.ts +76 -0
  60. package/src/participant/participantAudioPlayerControlsDom.test.tsx +123 -0
  61. package/src/participant/participantAudioPlayerCss.test.ts +63 -0
  62. package/src/participant/participantAudioPlayerDom.test.tsx +154 -0
  63. package/src/participant/participantAudioPlayerDomKit.test-helper.tsx +84 -0
  64. package/src/participant/participantAudioPlayerState.ts +35 -0
  65. package/src/participant/participantAudioRecorderDom.test.tsx +127 -0
  66. package/src/participant/participantAudioRecorderInterruptionDom.test.tsx +135 -0
  67. package/src/participant/participantAudioRecorderSpecDom.test.tsx +110 -0
  68. package/src/participant/participantAudioRecorderState.test.ts +144 -0
  69. package/src/participant/participantAudioRecorderState.ts +93 -0
  70. package/src/participant/participantAudioRecorderView.test.tsx +98 -0
  71. package/src/participant/participantAudioSupport.ts +36 -0
  72. package/src/participant/participantAudioTranscriptionDom.test.tsx +198 -0
  73. package/src/participant/participantComposerAudioDom.test.tsx +185 -0
  74. package/src/participant/participantComposerAudioGolden.test.tsx +68 -0
  75. package/src/participant/participantComposerAudioSsr.test.tsx +74 -0
  76. package/src/participant/participantFailedMenuDom.test.tsx +154 -0
  77. package/src/participant/participantFailedMenuState.test.ts +61 -0
  78. package/src/participant/participantFailedMenuState.ts +57 -0
  79. package/src/participant/participantFailedMessage.test.tsx +83 -0
  80. package/src/participant/participantFileValidation.ts +1 -1
  81. package/src/participant/participantGolden.test.tsx +2 -0
  82. package/src/participant/participantGoldenScenarios.test-helper.tsx +14 -1
  83. package/src/participant/participantLabels.ts +10 -1
  84. package/src/participant/participantMediaFakes.test-helper.ts +82 -0
  85. package/src/participant/participantNoMicrophoneDom.test.tsx +44 -0
  86. package/src/participant/participantPublicTypes.test.ts +11 -0
  87. package/src/participant/participantRecorderDomKit.test-helper.tsx +131 -0
  88. package/src/participant/participantRecorderFakes.test-helper.ts +47 -0
  89. package/src/participant/participantSpecRecorderFake.test-helper.ts +52 -0
  90. package/src/participant/participantThemeContrast.test.ts +17 -0
  91. package/src/participant/participantTooltips.test.tsx +74 -0
  92. package/src/participant/test-fixtures/composer-audio-denied.golden.html +1 -0
  93. package/src/participant/test-fixtures/composer-audio-idle.golden.html +1 -0
  94. package/src/participant/test-fixtures/composer-audio-recording.golden.html +1 -0
  95. package/src/participant/test-fixtures/composer-audio-review.golden.html +1 -0
  96. package/src/participant/test-fixtures/thread-audio-native.golden.html +1 -0
  97. package/src/participant/test-fixtures/thread-audio-player.golden.html +1 -0
  98. package/src/participant/test-fixtures/thread-audio-transcription-empty.golden.html +1 -0
  99. package/src/participant/test-fixtures/thread-audio-transcription-pending.golden.html +1 -0
  100. package/src/participant/test-fixtures/thread-audio-transcription-ready.golden.html +1 -0
  101. package/src/participant/test-fixtures/thread-audio-transcription-too_long.golden.html +1 -0
  102. package/src/participant/test-fixtures/thread-avatars-initials.golden.html +1 -1
  103. package/src/participant/test-fixtures/thread-closed.golden.html +1 -1
  104. package/src/participant/test-fixtures/thread-composer-microphone-idle.golden.html +1 -0
  105. package/src/participant/test-fixtures/thread-default.golden.html +1 -1
  106. package/src/participant/test-fixtures/thread-empty-draft.golden.html +1 -0
  107. package/src/participant/test-fixtures/thread-error.golden.html +1 -1
  108. package/src/participant/test-fixtures/thread-loading.golden.html +1 -1
  109. package/src/participant/test-fixtures/thread-plain.golden.html +1 -1
  110. package/src/participant/test-fixtures/thread-tail-off.golden.html +1 -1
  111. package/src/participant/useFailedMenuEffects.ts +59 -0
  112. package/src/participant/useParticipantAudioMediaEvents.ts +46 -0
  113. package/src/participant/useParticipantAudioPlayer.ts +52 -0
  114. package/src/participant/useParticipantAudioPlayerActions.ts +47 -0
  115. package/src/participant/useParticipantAudioRecorder.ts +153 -0
  116. package/src/participant/useParticipantAudioRecorderCore.ts +55 -0
  117. package/src/participant/useParticipantAudioRecorderEffects.ts +70 -0
  118. package/src/participant/useParticipantAudioSourceRetry.ts +42 -0
  119. package/src/participant/useParticipantFailedMenu.ts +60 -0
  120. package/src/participant/useParticipantSendAudio.test.tsx +85 -0
  121. package/src/participant/useParticipantSendAudio.ts +28 -0
  122. package/src/participant/useParticipantThreadController.ts +20 -7
  123. package/src/participant/useStableAttachmentUrl.ts +71 -0
  124. package/src/participant/useSuppressTouchTooltips.ts +38 -0
  125. package/src/participant/useTranscriptionOverflow.ts +25 -0
  126. package/src/participant/useTranscriptionReadyAnnouncement.ts +24 -0
  127. package/src/styles.css +48 -5
  128. package/src/tooltipLayerRegistry.test.ts +56 -0
  129. package/src/tooltipLayerRegistry.ts +34 -0
@@ -1,6 +1,6 @@
1
1
  import { M as MessagePayload } from '../quickReply.types-DzWH3NYV.js';
2
- import { C as ConversationSummary, a as ConversationEventSource, b as ConversationsApi, L as ListConversationsParams, c as ConversationPage, S as SSEProvider, d as ConversationDocument, e as SendPreviewMediaParams, P as PreviewUploadedMedia, f as PreviewWebhookClient, R as ResolveMediaUrl } from '../ConversationSimulatorPanel-q-wVgBCN.js';
3
- export { A as AudioRecorderButton, g as AudioRecorderButtonLabels, h as AudioRecorderButtonProps, i as ConversationPreview, j as ConversationPreviewProps, k as ConversationSimulatorClient, l as ConversationSimulatorPanel, m as ConversationSimulatorPanelLabels, n as ConversationSimulatorPanelProps, o as CreatePreviewMediaUploaderParams, p as CreatePreviewWebhookClientParams, D as DEFAULT_AUDIO_RECORDER_BUTTON_LABELS, q as DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS, r as DEFAULT_MEDIA_UPLOAD_PATH, s as PreviewInProductionError, t as PreviewMediaUploadRejectedError, u as PreviewMediaUploadRequest, v as PreviewUploadedMedia, w as PreviewWebhookRejectedError, x as SIMULATOR_FILE_MEDIA_KINDS, y as SendSimulatorMediaParams, z as SimulatorMediaKind, T as ToSimulatorClientParams, B as acceptsMediaKind, E as assertPreviewEnvironment, F as createPreviewMediaPoster, G as createPreviewMediaUploader, H as createPreviewWebhookClient, I as isConversationSimulatorClient, J as mediaKindOf, K as mediaTypeOf, M as signPreviewPayload, N as simulatorPanelLabelsOf, O as toConversationSimulatorClient } from '../ConversationSimulatorPanel-q-wVgBCN.js';
2
+ import { C as ConversationSummary, a as ConversationEventSource, b as ConversationsApi, L as ListConversationsParams, c as ConversationPage, S as SSEProvider, d as ConversationDocument, e as SendPreviewMediaParams, P as PreviewUploadedMedia, f as PreviewWebhookClient, R as ResolveMediaUrl } from '../ConversationSimulatorPanel-P6eqTSAO.js';
3
+ export { A as AudioRecorderButton, g as AudioRecorderButtonLabels, h as AudioRecorderButtonProps, i as ConversationPreview, j as ConversationPreviewProps, k as ConversationSimulatorClient, l as ConversationSimulatorPanel, m as ConversationSimulatorPanelLabels, n as ConversationSimulatorPanelProps, o as CreatePreviewMediaUploaderParams, p as CreatePreviewWebhookClientParams, D as DEFAULT_AUDIO_RECORDER_BUTTON_LABELS, q as DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS, r as DEFAULT_MEDIA_UPLOAD_PATH, s as PreviewInProductionError, t as PreviewMediaUploadRejectedError, u as PreviewMediaUploadRequest, v as PreviewUploadedMedia, w as PreviewWebhookRejectedError, x as SIMULATOR_FILE_MEDIA_KINDS, y as SendSimulatorMediaParams, z as SimulatorMediaKind, T as ToSimulatorClientParams, B as acceptsMediaKind, E as assertPreviewEnvironment, F as createPreviewMediaPoster, G as createPreviewMediaUploader, H as createPreviewWebhookClient, I as isConversationSimulatorClient, J as mediaKindOf, K as mediaTypeOf, M as signPreviewPayload, N as simulatorPanelLabelsOf, O as toConversationSimulatorClient } from '../ConversationSimulatorPanel-P6eqTSAO.js';
4
4
  import { InteractiveReplyOption } from '@adatechnology/meta-whatsapp-contracts/testing';
5
5
  import * as react from 'react';
6
6
  export { PREVIEW_MEDIA_ID_PREFIX } from '@adatechnology/meta-whatsapp-contracts';
@@ -16,9 +16,9 @@ import {
16
16
  mediaTypeOf,
17
17
  simulatorPanelLabelsOf,
18
18
  toConversationSimulatorClient
19
- } from "../chunk-L3OFAQPS.js";
19
+ } from "../chunk-Y326LLEP.js";
20
20
  import "../chunk-PJNR6UMN.js";
21
- import "../chunk-O6457XHK.js";
21
+ import "../chunk-WCZNIJ3M.js";
22
22
 
23
23
  // src/preview/previewStore.ts
24
24
  var GLOBAL_CHANNEL = "global";
package/dist/styles.css CHANGED
@@ -1382,8 +1382,8 @@
1382
1382
  .cv-p-chip,
1383
1383
  .cv-p-row,
1384
1384
  .cv-p-thread__older,
1385
- .cv-p-bubble__retry,
1386
- .cv-p-bubble__action {
1385
+ .cv-p-failed-menu__trigger,
1386
+ .cv-p-failed-menu__item {
1387
1387
  font: inherit;
1388
1388
  color: inherit;
1389
1389
  cursor: pointer;
@@ -1970,8 +1970,7 @@
1970
1970
  min-width: 0;
1971
1971
  margin-inline-end: auto;
1972
1972
  }
1973
- .cv-p-bubble > .cv-p-attachment,
1974
- .cv-p-bubble > .cv-p-bubble__actions {
1973
+ .cv-p-bubble > .cv-p-attachment {
1975
1974
  flex: 1 0 100%;
1976
1975
  }
1977
1976
  .cv-p-bubble__meta {
@@ -2004,33 +2003,103 @@
2004
2003
  .cv-p-bubble .cv-status-ticks--failed {
2005
2004
  color: var(--cv-p-i-danger);
2006
2005
  }
2007
- .cv-p-bubble__retry {
2008
- min-height: var(--cv-p-i-touch);
2009
- display: inline-flex;
2010
- align-items: center;
2011
- gap: 0.25rem;
2012
- padding: 0 0.5rem;
2006
+ @supports (color: color-mix(in srgb, red, blue)) {
2007
+ .cv-p-bubble--failed {
2008
+ border-color: color-mix(in srgb, var(--cv-p-i-danger) 55%, var(--cv-p-i-border));
2009
+ --cv-p-tail-edge: color-mix(in srgb, var(--cv-p-i-danger) 55%, var(--cv-p-i-border));
2010
+ }
2011
+ .cv-p-bubble--mine.cv-p-bubble--failed {
2012
+ border-color: color-mix(in srgb, var(--cv-p-i-danger) 55%, var(--cv-p-i-accent));
2013
+ --cv-p-tail-edge: color-mix(in srgb, var(--cv-p-i-danger) 55%, var(--cv-p-i-accent));
2014
+ }
2015
+ }
2016
+ .cv-p-failed-retry {
2017
+ position: absolute;
2018
+ top: 50%;
2019
+ right: calc(100% + 0.125rem);
2020
+ transform: translateY(-50%);
2021
+ display: grid;
2022
+ place-items: center;
2023
+ width: var(--cv-p-i-touch);
2024
+ height: var(--cv-p-i-touch);
2025
+ padding: 0;
2013
2026
  background: transparent;
2014
- border: 1px solid var(--cv-p-i-danger);
2027
+ border: 0;
2015
2028
  color: var(--cv-p-i-danger);
2016
- border-radius: var(--cv-p-i-radius);
2017
- font-size: 0.7rem;
2029
+ font: inherit;
2030
+ cursor: pointer;
2018
2031
  }
2019
- .cv-p-bubble__actions {
2020
- display: inline-flex;
2021
- gap: 0.5rem;
2022
- margin-top: 0.25rem;
2032
+ .cv-p-failed-retry__icon {
2033
+ display: grid;
2034
+ place-items: center;
2035
+ width: 2rem;
2036
+ height: 2rem;
2037
+ border: 1px solid var(--cv-p-i-danger);
2038
+ border-radius: 50%;
2023
2039
  }
2024
- .cv-p-bubble__action {
2025
- min-height: var(--cv-p-i-touch);
2040
+ .cv-p-failed-retry:hover .cv-p-failed-retry__icon,
2041
+ .cv-p-failed-retry:focus-visible .cv-p-failed-retry__icon {
2042
+ background: var(--cv-p-i-surface-raised);
2043
+ }
2044
+ .cv-p-failed-menu {
2026
2045
  display: inline-flex;
2027
- align-items: center;
2028
- padding: 0 0.75rem;
2046
+ }
2047
+ .cv-p-failed-menu__trigger {
2048
+ position: relative;
2049
+ display: inline-grid;
2050
+ place-items: center;
2051
+ min-width: 1.25rem;
2052
+ height: 0.75rem;
2053
+ padding: 0;
2029
2054
  background: transparent;
2055
+ border: 0;
2056
+ color: var(--cv-p-i-text-muted);
2057
+ font: inherit;
2058
+ line-height: 0.75rem;
2059
+ cursor: pointer;
2060
+ }
2061
+ .cv-p-failed-menu__trigger::after {
2062
+ content: "";
2063
+ position: absolute;
2064
+ inset: -0.5rem -0.4rem;
2065
+ }
2066
+ .cv-p-failed-menu__list {
2067
+ position: absolute;
2068
+ right: 0;
2069
+ bottom: calc(100% + 0.25rem);
2070
+ z-index: 2;
2071
+ display: flex;
2072
+ flex-direction: column;
2073
+ min-width: 8rem;
2074
+ background: var(--cv-p-i-surface-raised);
2030
2075
  border: 1px solid var(--cv-p-i-border);
2031
- color: inherit;
2032
2076
  border-radius: var(--cv-p-i-radius);
2033
- font-size: 0.7rem;
2077
+ }
2078
+ .cv-p-failed-menu__item {
2079
+ min-height: var(--cv-p-i-touch);
2080
+ padding: 0 0.75rem;
2081
+ background: transparent;
2082
+ border: 0;
2083
+ color: var(--cv-p-i-text);
2084
+ font:
2085
+ 0.7rem "SFMono-Regular",
2086
+ Consolas,
2087
+ monospace;
2088
+ text-align: start;
2089
+ text-transform: uppercase;
2090
+ letter-spacing: 0.04em;
2091
+ cursor: pointer;
2092
+ }
2093
+ .cv-p-failed-menu__list--below {
2094
+ bottom: auto;
2095
+ top: calc(100% + 0.25rem);
2096
+ }
2097
+ .cv-p-failed-menu__item--danger {
2098
+ color: var(--cv-p-i-danger);
2099
+ }
2100
+ .cv-p-failed-menu__item:hover,
2101
+ .cv-p-failed-menu__item:focus-visible {
2102
+ background: var(--cv-p-i-highlight);
2034
2103
  }
2035
2104
  .cv-p-attachment {
2036
2105
  display: flex;
@@ -2056,11 +2125,129 @@
2056
2125
  width: 100%;
2057
2126
  min-height: var(--cv-p-i-touch);
2058
2127
  }
2128
+ .cv-p-audio {
2129
+ display: flex;
2130
+ flex-wrap: wrap;
2131
+ align-items: center;
2132
+ gap: 0.25rem 0.5rem;
2133
+ width: 100%;
2134
+ min-width: min(15rem, 100%);
2135
+ box-sizing: border-box;
2136
+ }
2137
+ .cv-p-audio__toggle,
2138
+ .cv-p-audio__rate {
2139
+ flex: 0 0 auto;
2140
+ display: inline-grid;
2141
+ place-items: center;
2142
+ min-width: var(--cv-p-i-touch);
2143
+ min-height: var(--cv-p-i-touch);
2144
+ padding: 0;
2145
+ background: transparent;
2146
+ border: 1px solid var(--cv-p-i-border);
2147
+ border-radius: var(--cv-p-i-radius);
2148
+ color: inherit;
2149
+ font: inherit;
2150
+ font-size: 0.75rem;
2151
+ cursor: pointer;
2152
+ }
2153
+ .cv-p-audio__toggle:disabled {
2154
+ opacity: 0.45;
2155
+ cursor: not-allowed;
2156
+ }
2157
+ .cv-p-audio__toggle svg {
2158
+ display: block;
2159
+ }
2160
+ .cv-p-audio__seek {
2161
+ flex: 1 1 5rem;
2162
+ min-width: 5rem;
2163
+ min-height: var(--cv-p-i-touch);
2164
+ margin: 0;
2165
+ accent-color: var(--cv-p-i-accent);
2166
+ cursor: pointer;
2167
+ }
2168
+ .cv-p-audio__time {
2169
+ flex: 0 0 auto;
2170
+ font-size: 0.75rem;
2171
+ font-variant-numeric: tabular-nums;
2172
+ color: var(--cv-p-i-text-muted);
2173
+ }
2174
+ .cv-p-audio__media {
2175
+ display: none;
2176
+ }
2177
+ .cv-p-audio__error {
2178
+ flex: 1 0 100%;
2179
+ font-size: 0.75rem;
2180
+ }
2181
+ .cv-p-recorder .cv-p-audio {
2182
+ flex: 1 1 8rem;
2183
+ width: auto;
2184
+ min-width: 0;
2185
+ }
2059
2186
  .cv-p-attachment__name,
2060
2187
  .cv-p-attachment__size {
2061
2188
  font-size: 0.75rem;
2062
2189
  overflow-wrap: anywhere;
2063
2190
  }
2191
+ .cv-p-transcription {
2192
+ display: flex;
2193
+ flex-direction: column;
2194
+ gap: 0.25rem;
2195
+ margin: 0;
2196
+ font-size: 0.8rem;
2197
+ line-height: 1.4;
2198
+ color: var(--cv-p-i-text);
2199
+ }
2200
+ .cv-p-transcription--pending,
2201
+ .cv-p-transcription--too-long {
2202
+ font-size: 0.75rem;
2203
+ color: var(--cv-p-i-text-muted);
2204
+ }
2205
+ .cv-p-transcription__text,
2206
+ .cv-p-transcription__empty,
2207
+ .cv-p-transcription__notice {
2208
+ margin: 0;
2209
+ overflow-wrap: anywhere;
2210
+ }
2211
+ .cv-p-transcription__text {
2212
+ display: -webkit-box;
2213
+ -webkit-box-orient: vertical;
2214
+ -webkit-line-clamp: 6;
2215
+ line-clamp: 6;
2216
+ overflow: hidden;
2217
+ -webkit-user-select: text;
2218
+ user-select: text;
2219
+ }
2220
+ .cv-p-transcription__text[data-expanded=true] {
2221
+ display: block;
2222
+ overflow: visible;
2223
+ }
2224
+ .cv-p-transcription__empty {
2225
+ color: var(--cv-p-i-text-muted);
2226
+ }
2227
+ .cv-p-transcription__notice {
2228
+ order: 1;
2229
+ font-size: 0.7rem;
2230
+ color: var(--cv-p-i-text-muted);
2231
+ }
2232
+ .cv-p-transcription__toggle {
2233
+ order: 2;
2234
+ align-self: flex-start;
2235
+ min-width: var(--cv-p-i-touch);
2236
+ min-height: var(--cv-p-i-touch);
2237
+ padding: 0 0.5rem;
2238
+ background: transparent;
2239
+ border: 0;
2240
+ border-radius: var(--cv-p-i-radius);
2241
+ color: var(--cv-p-i-accent);
2242
+ font: inherit;
2243
+ font-size: 0.75rem;
2244
+ text-decoration: underline;
2245
+ cursor: pointer;
2246
+ }
2247
+ .cv-p-transcription__toggle:focus-visible {
2248
+ outline: 2px solid var(--cv-p-i-accent);
2249
+ outline-offset: 2px;
2250
+ }
2064
2251
  .cv-p-composer {
2065
2252
  display: flex;
2066
2253
  flex-direction: column;
@@ -2121,6 +2308,66 @@
2121
2308
  font-size: 0.8rem;
2122
2309
  color: var(--cv-p-i-danger);
2123
2310
  }
2311
+ .cv-p-composer__record {
2312
+ flex: 0 0 auto;
2313
+ padding: 0;
2314
+ margin-inline-start: 0.25rem;
2315
+ }
2316
+ .cv-p-composer__record svg,
2317
+ .cv-p-recorder svg {
2318
+ display: block;
2319
+ }
2320
+ .cv-p-recorder {
2321
+ display: flex;
2322
+ flex: 1 1 auto;
2323
+ flex-wrap: wrap;
2324
+ align-items: center;
2325
+ gap: 0.5rem;
2326
+ min-width: 0;
2327
+ }
2328
+ .cv-p-recorder__hint {
2329
+ flex: 1 1 100%;
2330
+ margin: 0;
2331
+ font-size: 0.8rem;
2332
+ color: var(--cv-p-i-text-muted);
2333
+ }
2334
+ .cv-p-recorder__discard {
2335
+ flex: 0 0 auto;
2336
+ padding: 0;
2337
+ color: var(--cv-p-i-text-muted);
2338
+ }
2339
+ .cv-p-recorder__dot {
2340
+ flex: 0 0 auto;
2341
+ width: 0.625rem;
2342
+ height: 0.625rem;
2343
+ border-radius: 50%;
2344
+ background: var(--cv-p-i-danger);
2345
+ animation: cv-p-recorder-pulse 1.2s ease-in-out infinite;
2346
+ }
2347
+ .cv-p-recorder__elapsed {
2348
+ flex: 1 1 auto;
2349
+ font:
2350
+ 0.85rem "SFMono-Regular",
2351
+ Consolas,
2352
+ monospace;
2353
+ font-variant-numeric: tabular-nums;
2354
+ color: var(--cv-p-i-text);
2355
+ }
2356
+ .cv-p-recorder__stop,
2357
+ .cv-p-recorder__send {
2358
+ flex: 0 0 auto;
2359
+ padding: 0;
2360
+ }
2361
+ @keyframes cv-p-recorder-pulse {
2362
+ 50% {
2363
+ opacity: 0.3;
2364
+ }
2365
+ }
2366
+ @media (prefers-reduced-motion: reduce) {
2367
+ .cv-p-recorder__dot {
2368
+ animation: none;
2369
+ }
2370
+ }
2124
2371
  .cv-p-quick {
2125
2372
  --cv-p-i-fade: 0.75rem;
2126
2373
  display: flex;
@@ -2525,10 +2772,21 @@
2525
2772
  .cv-p-thread__latest-badge {
2526
2773
  border: 1px solid ButtonText;
2527
2774
  }
2775
+ .cv-p-transcription__toggle {
2776
+ color: LinkText;
2777
+ border: 1px solid ButtonText;
2778
+ }
2528
2779
  .cv-p-bubble .cv-status-ticks,
2529
2780
  .cv-p-copyable__done {
2530
2781
  color: CanvasText;
2531
2782
  }
2783
+ .cv-p-failed-menu__list {
2784
+ border-color: CanvasText;
2785
+ }
2786
+ .cv-p-failed-menu__item:focus-visible,
2787
+ .cv-p-failed-menu__item:hover {
2788
+ outline: 2px solid Highlight;
2789
+ }
2532
2790
  }
2533
2791
  @media (forced-colors: active) {
2534
2792
  .cv-p-text__code,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adatechnology/conversations-ui",
3
- "version": "0.8.0",
3
+ "version": "0.10.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.5.0",
38
+ "@adatechnology/conversation-contracts": "0.6.0",
39
39
  "@adatechnology/meta-whatsapp-contracts": "0.6.0"
40
40
  },
41
41
  "peerDependencies": {
@@ -49,6 +49,7 @@
49
49
  }
50
50
  },
51
51
  "devDependencies": {
52
+ "@happy-dom/global-registrator": "^18.0.1",
52
53
  "@types/bun": "1.3.14",
53
54
  "@types/react": "^18 || ^19",
54
55
  "@types/react-dom": "^18 || ^19",
@@ -11,6 +11,10 @@
11
11
  import { useCallback, useEffect, useId, useRef, useState } from 'react'
12
12
 
13
13
  import { AudioPlayer } from './AudioPlayer'
14
+ import { DEFAULT_MAX_RECORDING_MILLISECONDS, RECORDING_FORMATS, resolveRecordingFormat } from './audioRecorderFormat'
15
+
16
+ export { DEFAULT_MAX_RECORDING_MILLISECONDS, RECORDING_FORMATS, resolveRecordingFormat }
17
+ export type { RecordingFormat } from './audioRecorderFormat'
14
18
 
15
19
  export interface AudioRecorderButtonLabels {
16
20
  start: string
@@ -64,34 +68,6 @@ export interface AudioRecorderButtonProps {
64
68
  disabledHint?: string
65
69
  }
66
70
 
67
- /**
68
- * Cinco minutos: com o codec de voz do WhatsApp isso dá menos de 3MB, folgado dentro do teto de
69
- * 16MB que a Meta impõe a áudio, e é mais do que qualquer recado de cliente. O corte automático
70
- * existe porque gravação esquecida aberta só se descobre no envio, com o arquivo inteiro perdido.
71
- */
72
- export const DEFAULT_MAX_RECORDING_MILLISECONDS = 5 * 60 * 1000
73
-
74
- /**
75
- * Ordem de preferência de formato: os dois primeiros o WhatsApp aceita como áudio; `webm` é só
76
- * saída de emergência para navegador que não grava mais nada — gravar em webm e descobrir na hora
77
- * do envio que o formato é inválido é pior do que gravar já no formato certo.
78
- */
79
- const RECORDING_FORMATS = [
80
- { mimeType: 'audio/ogg;codecs=opus', uploadMimeType: 'audio/ogg', extension: 'ogg' },
81
- { mimeType: 'audio/mp4', uploadMimeType: 'audio/mp4', extension: 'm4a' },
82
- { mimeType: 'audio/webm', uploadMimeType: 'audio/webm', extension: 'webm' },
83
- ] as const
84
-
85
- export type RecordingFormat = (typeof RECORDING_FORMATS)[number]
86
-
87
- /** Primeiro formato que o navegador sabe gravar, ou `undefined` se não souber gravar nenhum. */
88
- export function resolveRecordingFormat(): RecordingFormat | undefined {
89
- if (typeof MediaRecorder === 'undefined') return undefined
90
- // `isTypeSupported` não existe em toda implementação; onde falta, o primeiro da lista é o palpite.
91
- if (typeof MediaRecorder.isTypeSupported !== 'function') return RECORDING_FORMATS[0]
92
- return RECORDING_FORMATS.find((format) => MediaRecorder.isTypeSupported(format.mimeType))
93
- }
94
-
95
71
  type PendingRecording = { file: File; objectURL: string }
96
72
 
97
73
  export function AudioRecorderButton({
package/src/Tooltip.tsx CHANGED
@@ -14,6 +14,8 @@
14
14
  import { useEffect, useRef, useState } from 'react'
15
15
  import { createPortal } from 'react-dom'
16
16
 
17
+ import { createTooltipLayerRegistry } from './tooltipLayerRegistry'
18
+
17
19
  /** Atributo que marca um elemento como portador de dica. */
18
20
  export const TOOLTIP_ATTRIBUTE = 'data-cv-tooltip'
19
21
 
@@ -64,25 +66,16 @@ function tooltipStateFor(target: Element, text: string): TooltipState {
64
66
  /**
65
67
  * Só uma camada desenha por vez. A camada é montada por cada superfície do pacote (inbox, fluxos,
66
68
  * documentos) para o host não precisar montá-la à mão; duas superfícies na mesma tela renderizariam
67
- * dois balões sobrepostos sem essa trava.
69
+ * dois balões sobrepostos sem essa trava. Quando a dona desmonta, a posse passa à próxima montada.
68
70
  */
69
- let layerOwner: object | undefined
71
+ const layerRegistry = createTooltipLayerRegistry()
70
72
 
71
73
  export function TooltipLayer() {
72
74
  const identity = useRef({})
73
75
  const [isOwner, setIsOwner] = useState(false)
74
76
  const [tooltip, setTooltip] = useState<TooltipState | undefined>(undefined)
75
77
 
76
- useEffect(() => {
77
- const owned = identity.current
78
- if (layerOwner && layerOwner !== owned) return undefined
79
- layerOwner = owned
80
- setIsOwner(true)
81
-
82
- return () => {
83
- if (layerOwner === owned) layerOwner = undefined
84
- }
85
- }, [])
78
+ useEffect(() => layerRegistry.register(identity.current, setIsOwner), [])
86
79
 
87
80
  useEffect(() => {
88
81
  if (!isOwner) return undefined
@@ -0,0 +1,78 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from 'bun:test'
2
+ import { act, useRef } from 'react'
3
+
4
+ import { setupDom, wait, type DomHarness } from './domHarness.test-helper'
5
+ import { TooltipLayer } from './Tooltip'
6
+ import { useSuppressTouchTooltips } from './participant/useSuppressTouchTooltips'
7
+
8
+ let harness: DomHarness
9
+
10
+ beforeAll(async () => {
11
+ harness = await setupDom()
12
+ })
13
+ afterAll(() => harness.teardown())
14
+
15
+ function hover(target: Element): void {
16
+ act(() => void target.dispatchEvent(new Event('pointerover', { bubbles: true })))
17
+ }
18
+
19
+ function balloons(): number {
20
+ return document.body.querySelectorAll('[role="tooltip"]').length
21
+ }
22
+
23
+ function Target() {
24
+ return <button type="button" data-cv-tooltip="Hint">x</button>
25
+ }
26
+
27
+ describe('TooltipLayer ownership', () => {
28
+ it('draws one balloon with two layers, and keeps drawing after the owner unmounts', async () => {
29
+ const first = harness.mount(<TooltipLayer />)
30
+ harness.mount(<TooltipLayer />)
31
+ const target = harness.mount(<Target />).container.querySelector('button')!
32
+
33
+ hover(target)
34
+ await wait(200)
35
+ expect(balloons()).toBe(1)
36
+
37
+ first.unmount()
38
+ act(() => void target.dispatchEvent(new Event('pointerdown', { bubbles: true })))
39
+ hover(target)
40
+ await wait(200)
41
+ expect(balloons()).toBe(1)
42
+ })
43
+ })
44
+
45
+ function Thread() {
46
+ const rootRef = useRef<HTMLDivElement>(null)
47
+ useSuppressTouchTooltips(rootRef)
48
+ return (
49
+ <div ref={rootRef}>
50
+ <Target />
51
+ </div>
52
+ )
53
+ }
54
+
55
+ describe('touch on the participant thread', () => {
56
+ it('does not light the balloon when a tap focuses the button', async () => {
57
+ harness.mount(<TooltipLayer />)
58
+ const button = harness.mount(<Thread />).container.querySelector('button')!
59
+
60
+ act(() => void button.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, pointerType: 'touch' })))
61
+ act(() => void button.dispatchEvent(new Event('focusin', { bubbles: true })))
62
+ await wait(200)
63
+
64
+ expect(balloons()).toBe(0)
65
+ })
66
+
67
+ it('still shows it for keyboard focus after a touch', async () => {
68
+ harness.mount(<TooltipLayer />)
69
+ const button = harness.mount(<Thread />).container.querySelector('button')!
70
+
71
+ act(() => void button.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, pointerType: 'touch' })))
72
+ act(() => void button.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true })))
73
+ act(() => void button.dispatchEvent(new Event('focusin', { bubbles: true })))
74
+ await wait(200)
75
+
76
+ expect(balloons()).toBe(1)
77
+ })
78
+ })
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Cinco minutos: com o codec de voz do WhatsApp isso dá menos de 3MB, folgado dentro do teto de
3
+ * 16MB que a Meta impõe a áudio, e é mais do que qualquer recado de cliente. O corte automático
4
+ * existe porque gravação esquecida aberta só se descobre no envio, com o arquivo inteiro perdido.
5
+ */
6
+ export const DEFAULT_MAX_RECORDING_MILLISECONDS = 5 * 60 * 1000
7
+
8
+ /**
9
+ * Ordem de preferência de formato: os dois primeiros o WhatsApp aceita como áudio; `webm` é só
10
+ * saída de emergência para navegador que não grava mais nada — gravar em webm e descobrir na hora
11
+ * do envio que o formato é inválido é pior do que gravar já no formato certo.
12
+ */
13
+ export const RECORDING_FORMATS = [
14
+ { mimeType: 'audio/ogg;codecs=opus', uploadMimeType: 'audio/ogg', extension: 'ogg' },
15
+ { mimeType: 'audio/mp4', uploadMimeType: 'audio/mp4', extension: 'm4a' },
16
+ { mimeType: 'audio/webm', uploadMimeType: 'audio/webm', extension: 'webm' },
17
+ ] as const
18
+
19
+ export type RecordingFormat = (typeof RECORDING_FORMATS)[number]
20
+
21
+ /** Primeiro formato que o navegador sabe gravar, ou `undefined` se não souber gravar nenhum. */
22
+ export function resolveRecordingFormat(): RecordingFormat | undefined {
23
+ if (typeof MediaRecorder === 'undefined') return undefined
24
+ // `isTypeSupported` não existe em toda implementação; onde falta, o primeiro da lista é o palpite.
25
+ if (typeof MediaRecorder.isTypeSupported !== 'function') return RECORDING_FORMATS[0]
26
+ return RECORDING_FORMATS.find((format) => MediaRecorder.isTypeSupported(format.mimeType))
27
+ }
@@ -0,0 +1,53 @@
1
+ import { GlobalRegistrator } from '@happy-dom/global-registrator'
2
+ import { act, type ReactElement } from 'react'
3
+ import type { Root } from 'react-dom/client'
4
+
5
+ export type MountedTree = {
6
+ readonly container: HTMLElement
7
+ readonly unmount: () => void
8
+ }
9
+
10
+ export type DomHarness = {
11
+ readonly mount: (element: ReactElement) => MountedTree
12
+ readonly teardown: () => void
13
+ }
14
+
15
+ /**
16
+ * Registers happy-dom for one test file only and mounts React trees into it. react-dom/client is
17
+ * imported after the registration because it reads the DOM globals when it loads.
18
+ */
19
+ export async function setupDom(): Promise<DomHarness> {
20
+ GlobalRegistrator.register()
21
+ ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
22
+ const { createRoot } = await import('react-dom/client')
23
+ const roots: Root[] = []
24
+
25
+ function mount(element: ReactElement): MountedTree {
26
+ const container = document.createElement('div')
27
+ document.body.appendChild(container)
28
+ const root = createRoot(container)
29
+ roots.push(root)
30
+ act(() => root.render(element))
31
+ return { container, unmount: () => act(() => root.unmount()) }
32
+ }
33
+
34
+ function teardown(): void {
35
+ for (const root of roots) act(() => root.unmount())
36
+ document.body.innerHTML = ''
37
+ GlobalRegistrator.unregister()
38
+ }
39
+
40
+ return { mount, teardown }
41
+ }
42
+
43
+ export function pressKey(target: Element, key: string): void {
44
+ act(() => {
45
+ target.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true, cancelable: true }))
46
+ })
47
+ }
48
+
49
+ export function wait(milliseconds: number): Promise<void> {
50
+ return act(async () => {
51
+ await new Promise((resolve) => setTimeout(resolve, milliseconds))
52
+ })
53
+ }
@@ -10,6 +10,7 @@ import { ParticipantThread } from './ParticipantThread'
10
10
  import { toParticipantApi, type ConversationThreadApi } from './conversationThreadApi'
11
11
  import { buildThreadSummary } from './conversationThreadSummary'
12
12
  import type { ParticipantSubjectGroup, ParticipantSubjectIconRenderer } from './participant.types'
13
+ import type { ParticipantAudioRecordingOptions } from './participantAudio.types'
13
14
  import type { ParticipantPendingMessage } from './participantApi.types'
14
15
  import { draftKey, type ParticipantDrafts } from './participantDrafts'
15
16
  import { DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS, type ParticipantConversationsLabels } from './participantLabels'
@@ -50,6 +51,8 @@ export type ConversationThreadProps = {
50
51
  readonly tail?: boolean
51
52
  readonly avatars?: 'initials'
52
53
  readonly renderAuthorAvatar?: ParticipantAuthorAvatarRenderer
54
+ /** Absent draws no microphone. Present lets the user record and send an audio as a message of its own. */
55
+ readonly audioRecording?: ParticipantAudioRecordingOptions
53
56
  }
54
57
 
55
58
  function useConversationThreadCore(props: ConversationThreadProps): ParticipantThreadCoreProps {
@@ -105,6 +108,7 @@ function ConversationThreadBody(props: ConversationThreadProps) {
105
108
  tail={props.tail}
106
109
  counterpartLabel={props.counterpartLabel}
107
110
  headerActions={props.headerActions}
111
+ audioRecording={props.audioRecording}
108
112
  />
109
113
  </div>
110
114
  </div>