@nyaruka/temba-components 0.131.2 → 0.131.3

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 (223) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/demo/components/floating-tabs/example.html +400 -0
  3. package/demo/components/flow/index.html +1 -1
  4. package/demo/data/flows/sample-flow.json +41 -2
  5. package/demo/data/flows/voicemail.json +613 -0
  6. package/demo/index.html +6 -0
  7. package/dist/locales/es.js +5 -5
  8. package/dist/locales/es.js.map +1 -1
  9. package/dist/locales/fr.js +5 -5
  10. package/dist/locales/fr.js.map +1 -1
  11. package/dist/locales/locale-codes.js +11 -2
  12. package/dist/locales/locale-codes.js.map +1 -1
  13. package/dist/locales/pt.js +5 -5
  14. package/dist/locales/pt.js.map +1 -1
  15. package/dist/temba-components.js +1109 -535
  16. package/dist/temba-components.js.map +1 -1
  17. package/out-tsc/src/display/FloatingTab.js +167 -0
  18. package/out-tsc/src/display/FloatingTab.js.map +1 -0
  19. package/out-tsc/src/display/ProgressBar.js +22 -2
  20. package/out-tsc/src/display/ProgressBar.js.map +1 -1
  21. package/out-tsc/src/events.js.map +1 -1
  22. package/out-tsc/src/flow/CanvasNode.js +165 -31
  23. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  24. package/out-tsc/src/flow/Editor.js +857 -3
  25. package/out-tsc/src/flow/Editor.js.map +1 -1
  26. package/out-tsc/src/flow/NodeEditor.js +239 -19
  27. package/out-tsc/src/flow/NodeEditor.js.map +1 -1
  28. package/out-tsc/src/flow/NodeTypeSelector.js +44 -3
  29. package/out-tsc/src/flow/NodeTypeSelector.js.map +1 -1
  30. package/out-tsc/src/flow/StickyNote.js +12 -3
  31. package/out-tsc/src/flow/StickyNote.js.map +1 -1
  32. package/out-tsc/src/flow/actions/add_contact_groups.js +2 -1
  33. package/out-tsc/src/flow/actions/add_contact_groups.js.map +1 -1
  34. package/out-tsc/src/flow/actions/add_contact_urn.js +2 -1
  35. package/out-tsc/src/flow/actions/add_contact_urn.js.map +1 -1
  36. package/out-tsc/src/flow/actions/add_input_labels.js +2 -1
  37. package/out-tsc/src/flow/actions/add_input_labels.js.map +1 -1
  38. package/out-tsc/src/flow/actions/play_audio.js +2 -1
  39. package/out-tsc/src/flow/actions/play_audio.js.map +1 -1
  40. package/out-tsc/src/flow/actions/remove_contact_groups.js +2 -1
  41. package/out-tsc/src/flow/actions/remove_contact_groups.js.map +1 -1
  42. package/out-tsc/src/flow/actions/request_optin.js +1 -0
  43. package/out-tsc/src/flow/actions/request_optin.js.map +1 -1
  44. package/out-tsc/src/flow/actions/say_msg.js +2 -1
  45. package/out-tsc/src/flow/actions/say_msg.js.map +1 -1
  46. package/out-tsc/src/flow/actions/send_broadcast.js +2 -1
  47. package/out-tsc/src/flow/actions/send_broadcast.js.map +1 -1
  48. package/out-tsc/src/flow/actions/send_email.js +2 -1
  49. package/out-tsc/src/flow/actions/send_email.js.map +1 -1
  50. package/out-tsc/src/flow/actions/send_msg.js +93 -3
  51. package/out-tsc/src/flow/actions/send_msg.js.map +1 -1
  52. package/out-tsc/src/flow/actions/set_contact_channel.js +2 -1
  53. package/out-tsc/src/flow/actions/set_contact_channel.js.map +1 -1
  54. package/out-tsc/src/flow/actions/set_contact_field.js +2 -1
  55. package/out-tsc/src/flow/actions/set_contact_field.js.map +1 -1
  56. package/out-tsc/src/flow/actions/set_contact_language.js +2 -1
  57. package/out-tsc/src/flow/actions/set_contact_language.js.map +1 -1
  58. package/out-tsc/src/flow/actions/set_contact_name.js +2 -1
  59. package/out-tsc/src/flow/actions/set_contact_name.js.map +1 -1
  60. package/out-tsc/src/flow/actions/set_contact_status.js +2 -1
  61. package/out-tsc/src/flow/actions/set_contact_status.js.map +1 -1
  62. package/out-tsc/src/flow/actions/set_run_result.js +2 -1
  63. package/out-tsc/src/flow/actions/set_run_result.js.map +1 -1
  64. package/out-tsc/src/flow/actions/start_session.js +2 -1
  65. package/out-tsc/src/flow/actions/start_session.js.map +1 -1
  66. package/out-tsc/src/flow/config.js +2 -10
  67. package/out-tsc/src/flow/config.js.map +1 -1
  68. package/out-tsc/src/flow/nodes/shared.js +54 -0
  69. package/out-tsc/src/flow/nodes/shared.js.map +1 -1
  70. package/out-tsc/src/flow/nodes/split_by_airtime.js +9 -3
  71. package/out-tsc/src/flow/nodes/split_by_airtime.js.map +1 -1
  72. package/out-tsc/src/flow/nodes/split_by_contact_field.js +8 -3
  73. package/out-tsc/src/flow/nodes/split_by_contact_field.js.map +1 -1
  74. package/out-tsc/src/flow/nodes/split_by_expression.js +8 -3
  75. package/out-tsc/src/flow/nodes/split_by_expression.js.map +1 -1
  76. package/out-tsc/src/flow/nodes/split_by_groups.js +8 -3
  77. package/out-tsc/src/flow/nodes/split_by_groups.js.map +1 -1
  78. package/out-tsc/src/flow/nodes/split_by_intent.js +3 -2
  79. package/out-tsc/src/flow/nodes/split_by_intent.js.map +1 -1
  80. package/out-tsc/src/flow/nodes/split_by_llm.js +9 -2
  81. package/out-tsc/src/flow/nodes/split_by_llm.js.map +1 -1
  82. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js +9 -2
  83. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js.map +1 -1
  84. package/out-tsc/src/flow/nodes/split_by_random.js +8 -2
  85. package/out-tsc/src/flow/nodes/split_by_random.js.map +1 -1
  86. package/out-tsc/src/flow/nodes/split_by_resthook.js +8 -3
  87. package/out-tsc/src/flow/nodes/split_by_resthook.js.map +1 -1
  88. package/out-tsc/src/flow/nodes/split_by_run_result.js +8 -3
  89. package/out-tsc/src/flow/nodes/split_by_run_result.js.map +1 -1
  90. package/out-tsc/src/flow/nodes/split_by_scheme.js +8 -3
  91. package/out-tsc/src/flow/nodes/split_by_scheme.js.map +1 -1
  92. package/out-tsc/src/flow/nodes/split_by_subflow.js +8 -2
  93. package/out-tsc/src/flow/nodes/split_by_subflow.js.map +1 -1
  94. package/out-tsc/src/flow/nodes/split_by_ticket.js +8 -2
  95. package/out-tsc/src/flow/nodes/split_by_ticket.js.map +1 -1
  96. package/out-tsc/src/flow/nodes/split_by_webhook.js +8 -2
  97. package/out-tsc/src/flow/nodes/split_by_webhook.js.map +1 -1
  98. package/out-tsc/src/flow/nodes/wait_for_digits.js +3 -2
  99. package/out-tsc/src/flow/nodes/wait_for_digits.js.map +1 -1
  100. package/out-tsc/src/flow/nodes/wait_for_menu.js +3 -2
  101. package/out-tsc/src/flow/nodes/wait_for_menu.js.map +1 -1
  102. package/out-tsc/src/flow/nodes/wait_for_response.js +8 -3
  103. package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -1
  104. package/out-tsc/src/flow/types.js +15 -0
  105. package/out-tsc/src/flow/types.js.map +1 -1
  106. package/out-tsc/src/layout/FloatingWindow.js +346 -0
  107. package/out-tsc/src/layout/FloatingWindow.js.map +1 -0
  108. package/out-tsc/src/live/ContactChat.js +3 -19
  109. package/out-tsc/src/live/ContactChat.js.map +1 -1
  110. package/out-tsc/src/locales/es.js +5 -5
  111. package/out-tsc/src/locales/es.js.map +1 -1
  112. package/out-tsc/src/locales/fr.js +5 -5
  113. package/out-tsc/src/locales/fr.js.map +1 -1
  114. package/out-tsc/src/locales/locale-codes.js +11 -2
  115. package/out-tsc/src/locales/locale-codes.js.map +1 -1
  116. package/out-tsc/src/locales/pt.js +5 -5
  117. package/out-tsc/src/locales/pt.js.map +1 -1
  118. package/out-tsc/src/store/AppState.js +67 -0
  119. package/out-tsc/src/store/AppState.js.map +1 -1
  120. package/out-tsc/temba-modules.js +4 -0
  121. package/out-tsc/temba-modules.js.map +1 -1
  122. package/out-tsc/test/temba-floating-tab.test.js +91 -0
  123. package/out-tsc/test/temba-floating-tab.test.js.map +1 -0
  124. package/out-tsc/test/temba-floating-window.test.js +301 -0
  125. package/out-tsc/test/temba-floating-window.test.js.map +1 -0
  126. package/out-tsc/test/temba-flow-editor-node.test.js +117 -0
  127. package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
  128. package/out-tsc/test/temba-localization.test.js +471 -0
  129. package/out-tsc/test/temba-localization.test.js.map +1 -0
  130. package/out-tsc/test/temba-node-type-selector.test.js +150 -0
  131. package/out-tsc/test/temba-node-type-selector.test.js.map +1 -1
  132. package/out-tsc/test/utils.test.js +18 -0
  133. package/out-tsc/test/utils.test.js.map +1 -1
  134. package/package.json +1 -1
  135. package/screenshots/truth/floating-tab/default.png +0 -0
  136. package/screenshots/truth/floating-tab/gray.png +0 -0
  137. package/screenshots/truth/floating-tab/green.png +0 -0
  138. package/screenshots/truth/floating-tab/hidden.png +0 -0
  139. package/screenshots/truth/floating-tab/hover.png +0 -0
  140. package/screenshots/truth/floating-tab/purple.png +0 -0
  141. package/screenshots/truth/floating-window/chromeless.png +0 -0
  142. package/screenshots/truth/floating-window/custom-size.png +0 -0
  143. package/screenshots/truth/floating-window/default.png +0 -0
  144. package/screenshots/truth/floating-window/with-header.png +0 -0
  145. package/screenshots/truth/node-type-selector/action-mode.png +0 -0
  146. package/screenshots/truth/node-type-selector/split-mode.png +0 -0
  147. package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
  148. package/src/display/FloatingTab.ts +174 -0
  149. package/src/display/ProgressBar.ts +22 -2
  150. package/src/events.ts +2 -4
  151. package/src/flow/CanvasNode.ts +190 -32
  152. package/src/flow/Editor.ts +1040 -3
  153. package/src/flow/NodeEditor.ts +317 -19
  154. package/src/flow/NodeTypeSelector.ts +47 -3
  155. package/src/flow/StickyNote.ts +12 -3
  156. package/src/flow/actions/add_contact_groups.ts +2 -1
  157. package/src/flow/actions/add_contact_urn.ts +3 -1
  158. package/src/flow/actions/add_input_labels.ts +2 -1
  159. package/src/flow/actions/play_audio.ts +2 -1
  160. package/src/flow/actions/remove_contact_groups.ts +3 -1
  161. package/src/flow/actions/request_optin.ts +1 -0
  162. package/src/flow/actions/say_msg.ts +2 -1
  163. package/src/flow/actions/send_broadcast.ts +2 -1
  164. package/src/flow/actions/send_email.ts +3 -1
  165. package/src/flow/actions/send_msg.ts +134 -3
  166. package/src/flow/actions/set_contact_channel.ts +2 -1
  167. package/src/flow/actions/set_contact_field.ts +2 -1
  168. package/src/flow/actions/set_contact_language.ts +3 -1
  169. package/src/flow/actions/set_contact_name.ts +2 -1
  170. package/src/flow/actions/set_contact_status.ts +2 -1
  171. package/src/flow/actions/set_run_result.ts +2 -1
  172. package/src/flow/actions/start_session.ts +3 -1
  173. package/src/flow/config.ts +2 -12
  174. package/src/flow/nodes/shared.ts +70 -1
  175. package/src/flow/nodes/split_by_airtime.ts +20 -3
  176. package/src/flow/nodes/split_by_contact_field.ts +13 -3
  177. package/src/flow/nodes/split_by_expression.ts +13 -3
  178. package/src/flow/nodes/split_by_groups.ts +13 -3
  179. package/src/flow/nodes/split_by_intent.ts +3 -2
  180. package/src/flow/nodes/split_by_llm.ts +19 -2
  181. package/src/flow/nodes/split_by_llm_categorize.ts +19 -2
  182. package/src/flow/nodes/split_by_random.ts +12 -2
  183. package/src/flow/nodes/split_by_resthook.ts +13 -3
  184. package/src/flow/nodes/split_by_run_result.ts +13 -3
  185. package/src/flow/nodes/split_by_scheme.ts +13 -3
  186. package/src/flow/nodes/split_by_subflow.ts +12 -2
  187. package/src/flow/nodes/split_by_ticket.ts +12 -2
  188. package/src/flow/nodes/split_by_webhook.ts +12 -2
  189. package/src/flow/nodes/wait_for_digits.ts +3 -2
  190. package/src/flow/nodes/wait_for_menu.ts +3 -2
  191. package/src/flow/nodes/wait_for_response.ts +13 -3
  192. package/src/flow/types.ts +47 -0
  193. package/src/layout/FloatingWindow.ts +386 -0
  194. package/src/live/ContactChat.ts +4 -19
  195. package/src/locales/es.ts +18 -13
  196. package/src/locales/fr.ts +18 -13
  197. package/src/locales/locale-codes.ts +11 -2
  198. package/src/locales/pt.ts +18 -13
  199. package/src/store/AppState.ts +104 -0
  200. package/static/api/llms.json +18 -0
  201. package/temba-modules.ts +4 -0
  202. package/test/temba-floating-tab.test.ts +110 -0
  203. package/test/temba-floating-window.test.ts +477 -0
  204. package/test/temba-flow-editor-node.test.ts +144 -0
  205. package/test/temba-localization.test.ts +611 -0
  206. package/test/temba-node-type-selector.test.ts +203 -0
  207. package/test/utils.test.ts +20 -0
  208. package/test-assets/contacts/history.json +5 -6
  209. package/test-assets/select/llms.json +2 -2
  210. package/web-dev-server.config.mjs +47 -1
  211. package/web-test-runner.config.mjs +0 -1
  212. package/out-tsc/src/flow/nodes/wait_for_audio.js +0 -7
  213. package/out-tsc/src/flow/nodes/wait_for_audio.js.map +0 -1
  214. package/out-tsc/src/flow/nodes/wait_for_image.js +0 -7
  215. package/out-tsc/src/flow/nodes/wait_for_image.js.map +0 -1
  216. package/out-tsc/src/flow/nodes/wait_for_location.js +0 -7
  217. package/out-tsc/src/flow/nodes/wait_for_location.js.map +0 -1
  218. package/out-tsc/src/flow/nodes/wait_for_video.js +0 -7
  219. package/out-tsc/src/flow/nodes/wait_for_video.js.map +0 -1
  220. package/src/flow/nodes/wait_for_audio.ts +0 -7
  221. package/src/flow/nodes/wait_for_image.ts +0 -7
  222. package/src/flow/nodes/wait_for_location.ts +0 -7
  223. package/src/flow/nodes/wait_for_video.ts +0 -7
@@ -0,0 +1,386 @@
1
+ import { css, html, PropertyValueMap, TemplateResult } from 'lit';
2
+ import { property } from 'lit/decorators.js';
3
+ import { RapidElement } from '../RapidElement';
4
+ import { CustomEventType } from '../interfaces';
5
+ import { getClasses } from '../utils';
6
+ import { FloatingTab } from '../display/FloatingTab';
7
+
8
+ export class FloatingWindow extends RapidElement {
9
+ static get styles() {
10
+ return css`
11
+ .window.hidden {
12
+ transform: translateX(100%);
13
+ opacity: 0;
14
+ pointer-events: none;
15
+ }
16
+
17
+ .window {
18
+ transition: transform var(--transition-duration, 300ms) ease-in-out,
19
+ opacity var(--transition-duration, 300ms) ease-in-out;
20
+ position: fixed;
21
+ z-index: 9999;
22
+ top: 100px;
23
+ background: white;
24
+ border-radius: 8px;
25
+ box-shadow: -4px 4px 20px rgba(0, 0, 0, 0.3);
26
+ display: flex;
27
+ flex-direction: column;
28
+ overflow: hidden;
29
+ }
30
+
31
+ .window.chromeless {
32
+ background: transparent;
33
+ border-radius: 0;
34
+ box-shadow: none;
35
+ }
36
+
37
+ .window.dragging {
38
+ user-select: none;
39
+ cursor: move;
40
+ }
41
+
42
+ .header {
43
+ display: flex;
44
+ align-items: center;
45
+ justify-content: space-between;
46
+ padding: 6px 6px;
47
+ background: var(--header-color, var(--color-primary-light, #f3f4f6));
48
+ border-bottom: 1px solid rgba(0, 0, 0, 0.1);
49
+ cursor: move;
50
+ user-select: none;
51
+ }
52
+
53
+ .title {
54
+ font-weight: 600;
55
+ font-size: 16px;
56
+ color: white;
57
+ padding-left: 8px;
58
+ }
59
+
60
+ .close-button {
61
+ background: none;
62
+ border: none;
63
+ cursor: pointer;
64
+ padding: 4px;
65
+ display: flex;
66
+ align-items: center;
67
+ justify-content: center;
68
+ border-radius: 4px;
69
+ transition: background-color calc(var(--transition-duration, 150ms) / 2)
70
+ ease-in-out;
71
+ }
72
+
73
+ .close-button:hover {
74
+ background-color: rgba(255, 255, 255, 0.2);
75
+ }
76
+
77
+ .close-button temba-icon {
78
+ --icon-color: white;
79
+ }
80
+
81
+ .body {
82
+ flex: 1;
83
+ overflow-y: auto;
84
+ padding: 16px;
85
+ }
86
+
87
+ .window.chromeless .body {
88
+ padding: 0;
89
+ }
90
+
91
+ ::slotted(.drag-handle) {
92
+ cursor: move;
93
+ user-select: none;
94
+ border: 1px solid red;
95
+ }
96
+ `;
97
+ }
98
+
99
+ @property({ type: String })
100
+ header = '';
101
+
102
+ @property({ type: Number })
103
+ width = 500;
104
+
105
+ @property({ type: Number })
106
+ minHeight = 200;
107
+
108
+ @property({ type: Number })
109
+ maxHeight = 800;
110
+
111
+ @property({ type: Number })
112
+ top = 100;
113
+
114
+ @property({ type: Number })
115
+ left = -1; // -1 means calculate from right side
116
+
117
+ @property({ type: Boolean })
118
+ hidden = true;
119
+
120
+ @property({ type: Boolean })
121
+ dragging = false;
122
+
123
+ @property({ type: Boolean })
124
+ chromeless = false;
125
+
126
+ @property({ type: String })
127
+ color = '#6B7280';
128
+
129
+ private dragStartX = 0;
130
+ private dragStartY = 0;
131
+ private dragOffsetX = 0;
132
+ private dragOffsetY = 0;
133
+ private positionFromRight = false;
134
+ private defaultTop = 100;
135
+ private defaultLeft = -1;
136
+
137
+ public firstUpdated(
138
+ changes: PropertyValueMap<any> | Map<PropertyKey, unknown>
139
+ ): void {
140
+ super.firstUpdated(changes);
141
+
142
+ // store the default position from properties
143
+ this.defaultTop = this.top;
144
+ this.defaultLeft = this.left;
145
+
146
+ // determine if we should position from right side
147
+ if (this.left === -1) {
148
+ this.positionFromRight = true;
149
+ }
150
+
151
+ // set up drag handle listeners for chromeless windows
152
+ if (this.chromeless) {
153
+ this.setupDragHandles();
154
+ }
155
+
156
+ // listen for window resize to keep window in bounds
157
+ window.addEventListener('resize', this.handleResize);
158
+ }
159
+
160
+ disconnectedCallback(): void {
161
+ super.disconnectedCallback();
162
+ window.removeEventListener('resize', this.handleResize);
163
+ }
164
+
165
+ private setupDragHandles() {
166
+ // listen for mousedown on slotted content
167
+ this.addEventListener('mousedown', this.handleSlotMouseDown);
168
+ }
169
+
170
+ private handleSlotMouseDown = (event: MouseEvent) => {
171
+ // check if the target or any parent has the drag-handle class
172
+ const target = event.target as HTMLElement;
173
+ const dragHandle = target.closest('.drag-handle');
174
+
175
+ if (!dragHandle) {
176
+ return;
177
+ }
178
+
179
+ this.dragging = true;
180
+ this.dragStartX = event.clientX;
181
+ this.dragStartY = event.clientY;
182
+ this.dragOffsetX = this.left;
183
+ this.dragOffsetY = this.top;
184
+
185
+ document.addEventListener('mousemove', this.handleMouseMove);
186
+ document.addEventListener('mouseup', this.handleMouseUp);
187
+
188
+ event.preventDefault();
189
+ };
190
+
191
+ public updated(
192
+ changes: PropertyValueMap<any> | Map<PropertyKey, unknown>
193
+ ): void {
194
+ super.updated(changes);
195
+ if (changes.has('hidden')) {
196
+ this.classList.toggle('hidden', this.hidden);
197
+
198
+ // when hiding, reset positioning behavior to original
199
+ if (this.hidden && !changes.get('hidden')) {
200
+ if (this.defaultLeft === -1) {
201
+ this.positionFromRight = true;
202
+ }
203
+ }
204
+
205
+ // reset to default position when showing
206
+ if (!this.hidden && changes.get('hidden')) {
207
+ // reset top to default
208
+ this.top = this.defaultTop;
209
+
210
+ // if positioned from right, recalculate based on current viewport
211
+ if (this.positionFromRight) {
212
+ this.left = window.innerWidth - this.width - 20;
213
+ } else {
214
+ // reset left to default
215
+ this.left = this.defaultLeft;
216
+ }
217
+ }
218
+ }
219
+
220
+ // setup drag handles if chromeless changed to true
221
+ if (changes.has('chromeless') && this.chromeless) {
222
+ this.setupDragHandles();
223
+ }
224
+ }
225
+
226
+ private handleClose() {
227
+ this.hidden = true;
228
+ // show all tabs when window is closed
229
+ FloatingTab.showAllTabs();
230
+ this.fireCustomEvent(CustomEventType.DialogHidden);
231
+ }
232
+
233
+ private handleHeaderMouseDown(event: MouseEvent) {
234
+ // don't start drag if clicking on close button
235
+ if ((event.target as HTMLElement).closest('.close-button')) {
236
+ return;
237
+ }
238
+
239
+ this.dragging = true;
240
+ this.dragStartX = event.clientX;
241
+ this.dragStartY = event.clientY;
242
+ this.dragOffsetX = this.left;
243
+ this.dragOffsetY = this.top;
244
+
245
+ document.addEventListener('mousemove', this.handleMouseMove);
246
+ document.addEventListener('mouseup', this.handleMouseUp);
247
+
248
+ event.preventDefault();
249
+ }
250
+
251
+ private handleMouseMove = (event: MouseEvent) => {
252
+ if (!this.dragging) return;
253
+
254
+ const deltaX = event.clientX - this.dragStartX;
255
+ const deltaY = event.clientY - this.dragStartY;
256
+
257
+ this.left = this.dragOffsetX + deltaX;
258
+ this.top = this.dragOffsetY + deltaY;
259
+
260
+ // keep window within viewport bounds with 20px padding
261
+ const padding = 20;
262
+ this.left = Math.max(
263
+ padding,
264
+ Math.min(this.left, window.innerWidth - this.width - padding)
265
+ );
266
+
267
+ // get the actual rendered height of the window element
268
+ const windowElement = this.shadowRoot?.querySelector(
269
+ '.window'
270
+ ) as HTMLElement;
271
+ const currentHeight =
272
+ windowElement?.offsetHeight || this.maxHeight || window.innerHeight;
273
+ const maxTop = Math.max(
274
+ padding,
275
+ window.innerHeight - currentHeight - padding
276
+ );
277
+ this.top = Math.max(padding, Math.min(this.top, maxTop));
278
+ };
279
+
280
+ private handleMouseUp = () => {
281
+ this.dragging = false;
282
+ document.removeEventListener('mousemove', this.handleMouseMove);
283
+ document.removeEventListener('mouseup', this.handleMouseUp);
284
+
285
+ // once user drags the window, stop auto-positioning from right
286
+ this.positionFromRight = false;
287
+ };
288
+
289
+ private handleResize = () => {
290
+ // only constrain position if window is visible
291
+ if (this.hidden) return;
292
+
293
+ const padding = 20;
294
+ const windowElement = this.shadowRoot?.querySelector(
295
+ '.window'
296
+ ) as HTMLElement;
297
+ const currentHeight =
298
+ windowElement?.offsetHeight || this.maxHeight || window.innerHeight;
299
+
300
+ // if positioned from right, always recalculate from right edge
301
+ if (this.positionFromRight) {
302
+ this.left = window.innerWidth - this.width - padding;
303
+ } else {
304
+ // only adjust left if out of bounds
305
+ const minLeft = padding;
306
+ const maxLeft = window.innerWidth - this.width - padding;
307
+
308
+ if (this.left < minLeft) {
309
+ this.left = minLeft;
310
+ } else if (this.left > maxLeft) {
311
+ this.left = maxLeft;
312
+ }
313
+ }
314
+
315
+ // only adjust top if out of bounds
316
+ const minTop = padding;
317
+ const maxTop = Math.max(
318
+ padding,
319
+ window.innerHeight - currentHeight - padding
320
+ );
321
+
322
+ if (this.top < minTop) {
323
+ this.top = minTop;
324
+ } else if (this.top > maxTop) {
325
+ this.top = maxTop;
326
+ }
327
+ };
328
+
329
+ public show() {
330
+ this.hidden = false;
331
+ }
332
+
333
+ public hide() {
334
+ this.hidden = true;
335
+ }
336
+
337
+ public close() {
338
+ this.hidden = true;
339
+ // show all tabs when window is closed
340
+ FloatingTab.showAllTabs();
341
+ this.fireCustomEvent(CustomEventType.DialogHidden);
342
+ }
343
+
344
+ public render(): TemplateResult {
345
+ const minHeightStyle = this.minHeight
346
+ ? `min-height: ${this.minHeight}px;`
347
+ : '';
348
+ const maxHeightStyle = this.maxHeight
349
+ ? `max-height: ${this.maxHeight}px;`
350
+ : '';
351
+
352
+ const windowStyle = `
353
+ width: ${this.width}px;
354
+ ${minHeightStyle}
355
+ ${maxHeightStyle}
356
+ top: ${this.top}px;
357
+ left: ${this.left}px;
358
+ --header-color: ${this.color};
359
+ `;
360
+
361
+ const windowClasses = getClasses({
362
+ window: true,
363
+ dragging: this.dragging,
364
+ hidden: this.hidden,
365
+ chromeless: this.chromeless
366
+ });
367
+
368
+ return html`
369
+ <div class="${windowClasses}" style="${windowStyle}">
370
+ ${!this.chromeless
371
+ ? html`
372
+ <div class="header" @mousedown=${this.handleHeaderMouseDown}>
373
+ <div class="title">${this.header}</div>
374
+ <button class="close-button" @click=${this.handleClose}>
375
+ <temba-icon name="close" size="1.5"></temba-icon>
376
+ </button>
377
+ </div>
378
+ `
379
+ : ''}
380
+ <div class="body">
381
+ <slot></slot>
382
+ </div>
383
+ </div>
384
+ `;
385
+ }
386
+ }
@@ -812,15 +812,6 @@ export class ContactChat extends ContactStoreElement {
812
812
  return null;
813
813
  }
814
814
 
815
- private isMessageError(status: string | { status: string }): boolean {
816
- if (typeof status === 'string') {
817
- return status === 'E' || status === 'F';
818
- } else if (status && typeof status === 'object' && 'status' in status) {
819
- return status.status === 'errored' || status.status === 'failed';
820
- }
821
- return false;
822
- }
823
-
824
815
  private createMessages(page: ContactHistoryPage): ChatEvent[] {
825
816
  if (page.events) {
826
817
  let messages = [];
@@ -866,7 +857,10 @@ export class ContactChat extends ContactStoreElement {
866
857
  date: new Date(msgEvent.created_on),
867
858
  attachments: msgEvent.msg.attachments,
868
859
  text: msgEvent.msg.text,
869
- sendError: this.isMessageError(msgEvent._status),
860
+ sendError:
861
+ msgEvent._status &&
862
+ (msgEvent._status.status === 'errored' ||
863
+ msgEvent._status.status === 'failed'),
870
864
  popup: html`<div
871
865
  style="display: flex; flex-direction: row; align-items:center; justify-content: space-between;font-size:0.9em;line-height:1em;min-width:10em"
872
866
  >
@@ -881,15 +875,6 @@ export class ContactChat extends ContactStoreElement {
881
875
  ${msgEvent.optin.name}
882
876
  </div>`
883
877
  : null}
884
- ${msgEvent._failed_reason
885
- ? html`
886
- <div
887
- style="margin-top:0.2em;margin-right: 0.5em;min-width:10em;max-width:15em;color:var(--color-error);font-size:0.9em"
888
- >
889
- ${msgEvent._failed_reason}
890
- </div>
891
- `
892
- : null}
893
878
  </div>
894
879
  ${msgEvent._logs_url
895
880
  ? html`<a style="margin-left:0.5em" href="${msgEvent._logs_url}"
package/src/locales/es.ts CHANGED
@@ -1,13 +1,18 @@
1
- // Do not modify this file by hand!
2
- // Re-generate this file by running lit-localize
3
-
4
- /* eslint-disable no-irregular-whitespace */
5
- /* eslint-disable @typescript-eslint/no-explicit-any */
6
-
7
- export const templates = {
8
- scf1453991c986b25: `Tab para completar, enter para seleccionar`,
9
- s73b4d70c02f4b4e0: `No options`,
10
- s8f02e3a18ffc083a: `Are not currently in a flow`,
11
- s638236250662c6b3: `Have sent a message in the last`,
12
- s4788ee206c4570c7: `Have not started this flow in the last 90 days`
13
- };
1
+
2
+ // Do not modify this file by hand!
3
+ // Re-generate this file by running lit-localize
4
+
5
+
6
+
7
+
8
+ /* eslint-disable no-irregular-whitespace */
9
+ /* eslint-disable @typescript-eslint/no-explicit-any */
10
+
11
+ export const templates = {
12
+ 'scf1453991c986b25': `Tab para completar, enter para seleccionar`,
13
+ 's73b4d70c02f4b4e0': `No options`,
14
+ 's8f02e3a18ffc083a': `Are not currently in a flow`,
15
+ 's638236250662c6b3': `Have sent a message in the last`,
16
+ 's4788ee206c4570c7': `Have not started this flow in the last 90 days`,
17
+ };
18
+
package/src/locales/fr.ts CHANGED
@@ -1,13 +1,18 @@
1
- // Do not modify this file by hand!
2
- // Re-generate this file by running lit-localize
3
-
4
- /* eslint-disable no-irregular-whitespace */
5
- /* eslint-disable @typescript-eslint/no-explicit-any */
6
-
7
- export const templates = {
8
- s73b4d70c02f4b4e0: `No options`,
9
- scf1453991c986b25: `Tab to complete, enter to select`,
10
- s8f02e3a18ffc083a: `Are not currently in a flow`,
11
- s638236250662c6b3: `Have sent a message in the last`,
12
- s4788ee206c4570c7: `Have not started this flow in the last 90 days`
13
- };
1
+
2
+ // Do not modify this file by hand!
3
+ // Re-generate this file by running lit-localize
4
+
5
+
6
+
7
+
8
+ /* eslint-disable no-irregular-whitespace */
9
+ /* eslint-disable @typescript-eslint/no-explicit-any */
10
+
11
+ export const templates = {
12
+ 's73b4d70c02f4b4e0': `No options`,
13
+ 'scf1453991c986b25': `Tab to complete, enter to select`,
14
+ 's8f02e3a18ffc083a': `Are not currently in a flow`,
15
+ 's638236250662c6b3': `Have sent a message in the last`,
16
+ 's4788ee206c4570c7': `Have not started this flow in the last 90 days`,
17
+ };
18
+
@@ -10,9 +10,18 @@ export const sourceLocale = `en`;
10
10
  * The other locale codes that this application is localized into. Sorted
11
11
  * lexicographically.
12
12
  */
13
- export const targetLocales = [`es`, `fr`, `pt`] as const;
13
+ export const targetLocales = [
14
+ `es`,
15
+ `fr`,
16
+ `pt`,
17
+ ] as const;
14
18
 
15
19
  /**
16
20
  * All valid project locale codes. Sorted lexicographically.
17
21
  */
18
- export const allLocales = [`en`, `es`, `fr`, `pt`] as const;
22
+ export const allLocales = [
23
+ `en`,
24
+ `es`,
25
+ `fr`,
26
+ `pt`,
27
+ ] as const;
package/src/locales/pt.ts CHANGED
@@ -1,13 +1,18 @@
1
- // Do not modify this file by hand!
2
- // Re-generate this file by running lit-localize
3
-
4
- /* eslint-disable no-irregular-whitespace */
5
- /* eslint-disable @typescript-eslint/no-explicit-any */
6
-
7
- export const templates = {
8
- s73b4d70c02f4b4e0: `No options`,
9
- scf1453991c986b25: `Tab to complete, enter to select`,
10
- s8f02e3a18ffc083a: `Are not currently in a flow`,
11
- s638236250662c6b3: `Have sent a message in the last`,
12
- s4788ee206c4570c7: `Have not started this flow in the last 90 days`
13
- };
1
+
2
+ // Do not modify this file by hand!
3
+ // Re-generate this file by running lit-localize
4
+
5
+
6
+
7
+
8
+ /* eslint-disable no-irregular-whitespace */
9
+ /* eslint-disable @typescript-eslint/no-explicit-any */
10
+
11
+ export const templates = {
12
+ 's73b4d70c02f4b4e0': `No options`,
13
+ 'scf1453991c986b25': `Tab to complete, enter to select`,
14
+ 's8f02e3a18ffc083a': `Are not currently in a flow`,
15
+ 's638236250662c6b3': `Have sent a message in the last`,
16
+ 's4788ee206c4570c7': `Have not started this flow in the last 90 days`,
17
+ };
18
+
@@ -109,6 +109,17 @@ export interface AppState {
109
109
  createStickyNote(position: FlowPosition): string;
110
110
  createNode(nodeType: string, position: FlowPosition): string;
111
111
  addNode(node: Node, nodeUI: NodeUI): void;
112
+ updateLocalization(
113
+ languageCode: string,
114
+ actionUuid: string,
115
+ localizationData: Record<string, any>
116
+ ): void;
117
+ setTranslationFilters: (filters: { categories: boolean }) => void;
118
+ markAutoTranslated: (
119
+ languageCode: string,
120
+ uuid: string,
121
+ attributes: string[]
122
+ ) => void;
112
123
  }
113
124
 
114
125
  export const zustand = createStore<AppState>()(
@@ -407,6 +418,99 @@ export const zustand = createStore<AppState>()(
407
418
 
408
419
  state.dirtyDate = new Date();
409
420
  });
421
+ },
422
+
423
+ updateLocalization: (
424
+ languageCode: string,
425
+ actionUuid: string,
426
+ localizationData: Record<string, any>
427
+ ) => {
428
+ set((state: AppState) => {
429
+ // Initialize localization structure if it doesn't exist
430
+ if (!state.flowDefinition.localization) {
431
+ state.flowDefinition.localization = {};
432
+ }
433
+
434
+ if (!state.flowDefinition.localization[languageCode]) {
435
+ state.flowDefinition.localization[languageCode] = {};
436
+ }
437
+
438
+ // Update or remove the localization for this action
439
+ if (Object.keys(localizationData).length > 0) {
440
+ state.flowDefinition.localization[languageCode][actionUuid] =
441
+ localizationData;
442
+ } else {
443
+ // If no localized values, remove the entry
444
+ delete state.flowDefinition.localization[languageCode][actionUuid];
445
+ }
446
+
447
+ // Clean up empty language sections
448
+ if (
449
+ Object.keys(state.flowDefinition.localization[languageCode])
450
+ .length === 0
451
+ ) {
452
+ delete state.flowDefinition.localization[languageCode];
453
+ }
454
+
455
+ // Clean up empty localization object
456
+ if (Object.keys(state.flowDefinition.localization).length === 0) {
457
+ delete state.flowDefinition.localization;
458
+ }
459
+
460
+ state.dirtyDate = new Date();
461
+ });
462
+ },
463
+
464
+ setTranslationFilters: (filters: { categories: boolean }) => {
465
+ set((state: AppState) => {
466
+ if (!state.flowDefinition?._ui) {
467
+ return;
468
+ }
469
+
470
+ const currentFilters = state.flowDefinition._ui
471
+ .translation_filters || {
472
+ categories: false
473
+ };
474
+
475
+ state.flowDefinition._ui.translation_filters = {
476
+ ...currentFilters,
477
+ categories: !!filters.categories
478
+ };
479
+
480
+ state.dirtyDate = new Date();
481
+ });
482
+ },
483
+
484
+ markAutoTranslated: (
485
+ languageCode: string,
486
+ uuid: string,
487
+ attributes: string[]
488
+ ) => {
489
+ set((state: AppState) => {
490
+ if (!state.flowDefinition?._ui) {
491
+ return;
492
+ }
493
+
494
+ if (!state.flowDefinition._ui.auto_translations) {
495
+ state.flowDefinition._ui.auto_translations = {};
496
+ }
497
+
498
+ if (!state.flowDefinition._ui.auto_translations[languageCode]) {
499
+ state.flowDefinition._ui.auto_translations[languageCode] = {};
500
+ }
501
+
502
+ const existing =
503
+ state.flowDefinition._ui.auto_translations[languageCode][uuid] ||
504
+ [];
505
+
506
+ const merged = Array.from(
507
+ new Set([...existing, ...(attributes || [])])
508
+ );
509
+
510
+ state.flowDefinition._ui.auto_translations[languageCode][uuid] =
511
+ merged;
512
+ state.dirtyDate = new Date();
513
+ });
410
514
  }
411
515
  }))
412
516
  )
@@ -0,0 +1,18 @@
1
+ {
2
+ "next": null,
3
+ "previous": null,
4
+ "results": [
5
+ {
6
+ "uuid": "2399e7d6-fcdf-4e47-a835-f3bdb7f80938",
7
+ "name": "GPT 4.1",
8
+ "type": "openai",
9
+ "description": "General-purpose reasoning model"
10
+ },
11
+ {
12
+ "uuid": "4399e7d6-fcdf-4e47-a835-f3bdb7f80938",
13
+ "name": "GPT 5",
14
+ "type": "openai",
15
+ "description": "Latest experimental model"
16
+ }
17
+ ]
18
+ }
package/temba-modules.ts CHANGED
@@ -74,6 +74,8 @@ import { KeyValueEditor } from './src/form/KeyValueEditor';
74
74
  import { TembaArrayEditor } from './src/form/ArrayEditor';
75
75
  import { MessageEditor } from './src/form/MessageEditor';
76
76
  import './src/form/BaseListEditor'; // Import base class
77
+ import { FloatingTab } from './src/display/FloatingTab';
78
+ import { FloatingWindow } from './src/layout/FloatingWindow';
77
79
 
78
80
  export function addCustomElement(name: string, comp: any) {
79
81
  if (!window.customElements.get(name)) {
@@ -158,3 +160,5 @@ addCustomElement('temba-chart', TembaChart);
158
160
  addCustomElement('temba-key-value-editor', KeyValueEditor);
159
161
  addCustomElement('temba-array-editor', TembaArrayEditor);
160
162
  addCustomElement('temba-message-editor', MessageEditor);
163
+ addCustomElement('temba-floating-tab', FloatingTab);
164
+ addCustomElement('temba-floating-window', FloatingWindow);