@nyaruka/temba-components 0.156.18 → 0.157.1

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 (56) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/temba-components.js +2119 -1617
  3. package/dist/temba-components.js.map +1 -1
  4. package/package.json +1 -1
  5. package/src/display/Button.ts +102 -121
  6. package/src/display/Chat.ts +74 -9
  7. package/src/display/Dropdown.ts +11 -0
  8. package/src/display/Label.ts +154 -2
  9. package/src/display/LeafletMap.ts +4 -3
  10. package/src/display/Options.ts +71 -16
  11. package/src/display/TembaUser.ts +32 -8
  12. package/src/events/eventRenderers.ts +243 -95
  13. package/src/excellent/caret-utils.ts +0 -1
  14. package/src/flow/AutoTranslate.ts +2 -2
  15. package/src/flow/Editor.ts +4 -4
  16. package/src/flow/NodeEditor.ts +2 -2
  17. package/src/flow/NodeTypeSelector.ts +0 -5
  18. package/src/flow/RevisionsWindow.ts +1 -3
  19. package/src/flow/actions/set_contact_language.ts +5 -4
  20. package/src/flow/nodes/shared.ts +14 -0
  21. package/src/flow/nodes/split_by_llm_categorize.ts +28 -8
  22. package/src/flow/utils.ts +39 -60
  23. package/src/form/ArrayEditor.ts +9 -11
  24. package/src/form/Checkbox.ts +2 -2
  25. package/src/form/ColorPicker.ts +5 -3
  26. package/src/form/Compose.ts +1 -1
  27. package/src/form/FieldElement.ts +8 -8
  28. package/src/form/KeyValueEditor.ts +4 -4
  29. package/src/form/MessageEditor.ts +2 -3
  30. package/src/form/RangePicker.ts +17 -17
  31. package/src/form/TembaSlider.ts +10 -10
  32. package/src/form/TemplateEditor.ts +4 -4
  33. package/src/form/TextInput.ts +19 -1
  34. package/src/form/select/Omnibox.ts +21 -20
  35. package/src/form/select/Select.ts +382 -173
  36. package/src/form/select/WorkspaceSelect.ts +7 -1
  37. package/src/interfaces.ts +1 -0
  38. package/src/languages.ts +56 -0
  39. package/src/layout/Accordion.ts +2 -2
  40. package/src/layout/Dialog.ts +1 -3
  41. package/src/layout/Modax.ts +1 -1
  42. package/src/list/ContentMenu.ts +1 -2
  43. package/src/list/SortableList.ts +156 -0
  44. package/src/list/TembaMenu.ts +159 -113
  45. package/src/live/ContactBadges.ts +2 -1
  46. package/src/live/ContactChat.ts +62 -45
  47. package/src/live/ContactDetails.ts +3 -1
  48. package/src/live/ContactFieldEditor.ts +36 -31
  49. package/src/live/FieldManager.ts +4 -4
  50. package/src/store/AppState.ts +3 -21
  51. package/src/store/Store.ts +0 -29
  52. package/src/styles/designTokens.ts +158 -0
  53. package/src/styles/pillVariants.ts +147 -0
  54. package/static/css/temba-components.css +141 -36
  55. package/web-dev-server.config.mjs +0 -1
  56. package/web-test-runner.config.mjs +98 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nyaruka/temba-components",
3
- "version": "0.156.18",
3
+ "version": "0.157.1",
4
4
  "description": "Web components to support rapidpro and related projects",
5
5
  "author": "Nyaruka <code@nyaruka.coim>",
6
6
  "main": "dist/index.js",
@@ -7,170 +7,148 @@ export class Button extends LitElement {
7
7
  static get styles() {
8
8
  return css`
9
9
  :host {
10
- display: flex;
10
+ display: inline-flex;
11
11
  align-self: stretch;
12
- font-family: var(--font-family);
13
- font-weight: 400;
14
- }
15
-
16
- .small {
17
- font-size: 0.8em;
18
- --button-y: 0px;
19
- --button-x: 0.5em;
20
- }
21
-
22
- .v-2.button-container {
23
- background: var(--button-bg);
24
- background-image: var(--button-bg-img);
25
- color: var(--button-text);
26
- box-shadow: var(--button-shadow);
27
- transition: all calc(var(--transition-speed) / 2) ease-in;
12
+ font-family: var(--font);
28
13
  }
29
14
 
15
+ /* DS .btn-sm — sizing, type, transition, shape */
30
16
  .button-container {
31
- color: #fff;
32
- cursor: pointer;
33
- display: flex;
17
+ display: inline-flex;
18
+ align-items: center;
19
+ justify-content: var(--button-justify, center);
20
+ gap: 6px;
34
21
  flex-grow: 1;
35
- border-radius: var(--curvature);
36
- outline: none;
37
- transition: background ease-in var(--transition-speed);
22
+ height: 28px;
23
+ padding: 0 10px;
24
+ border: 1px solid transparent;
25
+ border-radius: var(--r-sm);
26
+ font-size: 12.5px;
27
+ font-weight: var(--w-regular);
28
+ letter-spacing: -0.005em;
29
+ line-height: 1;
30
+ white-space: nowrap;
31
+ cursor: pointer;
38
32
  user-select: none;
39
33
  -webkit-user-select: none;
40
- text-align: center;
41
- border: var(--button-border, none);
42
- }
43
-
44
- .button-name {
45
- white-space: nowrap;
46
- }
47
-
48
- .secondary-button:hover .button-mask {
49
- border: 1px solid rgba(0, 0, 0, 0.15);
50
- }
51
-
52
- .button-mask:hover {
53
- background: rgba(0, 0, 0, 0.05);
54
- }
55
-
56
- .button-mask:active {
57
- background: rgba(0, 0, 0, 0.12);
58
- }
59
-
60
- .button-container:focus {
61
34
  outline: none;
62
- }
63
-
64
- /* Only show the focus ring for keyboard navigation, never on click. */
65
- .button-container:focus-visible {
66
- box-shadow: var(--widget-box-shadow-focused);
67
- }
68
-
69
- .button-container.secondary-button:focus-visible .button-mask {
70
- background: transparent;
35
+ box-sizing: border-box;
36
+ transition:
37
+ background 120ms,
38
+ border-color 120ms,
39
+ color 120ms,
40
+ box-shadow 120ms;
71
41
  }
72
42
 
73
43
  .button-mask {
74
- padding: var(--button-y) var(--button-x);
75
- border-radius: var(--curvature);
76
- border: 1px solid transparent;
77
- transition: var(--transition-speed);
78
- background: var(--button-mask);
79
- display: flex;
44
+ display: inline-flex;
80
45
  align-items: center;
46
+ gap: 6px;
47
+ line-height: 1;
81
48
  }
82
49
 
83
- .button-container.disabled-button {
84
- background: rgba(0, 0, 0, 0.05);
85
- color: rgba(255, 255, 255, 0.45);
86
- cursor: default;
50
+ .button-name {
51
+ white-space: nowrap;
87
52
  }
88
53
 
89
- .button-container.disabled-button .button-mask {
90
- box-shadow: 0 0 0px 1px var(--color-button-disabled);
54
+ /* even smaller variant (compact lists, list-row actions, etc.) */
55
+ .small {
56
+ height: 24px;
57
+ padding: 0 8px;
58
+ font-size: 12px;
91
59
  }
92
60
 
93
- .button-container.disabled-button:hover .button-mask {
94
- box-shadow: 0 0 0px 1px var(--color-button-disabled);
95
- background: rgba(0, 0, 0, 0.05);
61
+ /* DS .btn-primary — solid accent fill with a slightly darker
62
+ 1px border so the visible box matches the secondary's
63
+ 1px-bordered box. */
64
+ .primary-button {
65
+ background: var(--accent-600);
66
+ border-color: var(--accent-700);
67
+ color: #fff;
96
68
  }
97
-
98
- .button-container.active-button .button-mask {
69
+ .primary-button:hover {
70
+ background: var(--accent-700);
99
71
  }
100
72
 
101
- .secondary-button.active-button {
102
- background: transparent;
103
- color: var(--color-text);
73
+ /* DS .btn-secondary — surface bg with a 1px gray outline. */
74
+ .secondary-button {
75
+ background: var(--surface);
76
+ border-color: var(--border-strong);
77
+ color: var(--text-1);
104
78
  }
105
-
106
- .secondary-button.active-button .button-mask {
107
- border: 1px solid transparent;
79
+ .secondary-button:hover {
80
+ background: var(--sunken);
108
81
  }
109
82
 
110
- .button-container.secondary-button.active-button:focus-visible
111
- .button-mask {
112
- background: transparent;
113
- box-shadow: none;
83
+ /* affirmative + attention share DS .btn-primary chrome but tint
84
+ green; treat them as solid CTAs. */
85
+ .attention-button,
86
+ .affirmative {
87
+ background: var(--success, #16a34a);
88
+ border-color: color-mix(in srgb, var(--success, #16a34a) 80%, black);
89
+ color: #fff;
114
90
  }
115
-
116
- .primary-button {
117
- background: var(--color-button-primary);
118
- color: var(--color-button-primary-text);
91
+ .attention-button:hover,
92
+ .affirmative:hover {
93
+ background: color-mix(in srgb, var(--success, #16a34a) 88%, black);
119
94
  }
120
95
 
121
- .affirmative {
122
- background: var(--color-button-attention);
96
+ /* DS .btn-danger */
97
+ .destructive-button {
98
+ background: var(--danger, #d03f3f);
99
+ border-color: color-mix(in srgb, var(--danger, #d03f3f) 80%, black);
100
+ color: #fff;
123
101
  }
124
-
125
- .light-button {
126
- background: var(--color-button-light);
127
- color: var(--color-button-light-text);
102
+ .destructive-button:hover {
103
+ background: color-mix(in srgb, var(--danger, #d03f3f) 88%, black);
128
104
  }
129
105
 
106
+ /* DS .btn-ghost — text-only, hover fills with sunken */
107
+ .light-button,
130
108
  .lined-button {
131
- border: 1px solid rgba(0, 0, 0, 0.1);
132
- color: rgba(0, 0, 0, 0.7);
133
- background: transparent;
109
+ background: var(--surface);
110
+ border-color: var(--border-strong);
111
+ color: var(--text-1);
134
112
  }
135
-
136
- .lined-button .button-mask {
137
- flex-grow: 1;
138
- }
139
-
140
- .lined-button .button-mask:hover {
141
- background: rgba(0, 0, 0, 0.03);
113
+ .light-button:hover,
114
+ .lined-button:hover {
115
+ background: var(--sunken);
142
116
  }
143
117
 
118
+ /* icon-only button — square footprint, ghost chrome */
144
119
  .icon-button {
145
- --button-y: 0.2em;
146
- --button-x: 0em;
120
+ width: 28px;
121
+ padding: 0;
122
+ background: transparent;
123
+ border-color: transparent;
124
+ color: var(--text-2);
147
125
  }
148
-
149
- .icon-button temba-icon {
150
- padding: 0 0.5em;
126
+ .icon-button:hover {
127
+ background: var(--sunken);
128
+ color: var(--text-1);
151
129
  }
152
-
153
- .attention-button {
154
- background: var(--color-button-attention);
155
- color: var(--color-button-primary-text);
130
+ .icon-button.small {
131
+ width: 24px;
156
132
  }
157
133
 
158
- .secondary-button {
159
- background: transparent;
160
- color: var(--color-text);
134
+ /* active = pressed-down look — slightly inset */
135
+ .button-container.active-button {
136
+ box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.12);
161
137
  }
162
-
163
- .destructive-button {
164
- background: var(--color-button-destructive);
165
- color: var(--color-button-destructive-text);
138
+ .secondary-button.active-button {
139
+ background: var(--sunken);
166
140
  }
167
141
 
168
- .button-mask.disabled-button {
169
- background: rgba(0, 0, 0, 0.1);
142
+ /* disabled */
143
+ .button-container.disabled-button {
144
+ opacity: 0.45;
145
+ cursor: not-allowed;
146
+ pointer-events: none;
170
147
  }
171
148
 
172
- .secondary-button .button-mask:hover {
173
- background: transparent;
149
+ /* focus ring — keyboard nav only */
150
+ .button-container:focus-visible {
151
+ box-shadow: var(--widget-box-shadow-focused);
174
152
  }
175
153
 
176
154
  .submit-animation {
@@ -278,6 +256,9 @@ export class Button extends LitElement {
278
256
  (!this.primary &&
279
257
  !this.secondary &&
280
258
  !this.attention &&
259
+ !this.destructive &&
260
+ !this.lined &&
261
+ !this.light &&
281
262
  this.v == 1),
282
263
  'secondary-button': this.secondary,
283
264
  'disabled-button': this.disabled,
@@ -286,7 +267,7 @@ export class Button extends LitElement {
286
267
  'destructive-button': this.destructive,
287
268
  'light-button': this.light,
288
269
  'lined-button': this.lined,
289
- 'icon-button': !!this.icon,
270
+ 'icon-button': !!this.icon && !this.name,
290
271
  small: this.small
291
272
  })}"
292
273
  tabindex="0"
@@ -3,6 +3,7 @@ import { property } from 'lit/decorators.js';
3
3
  import { repeat } from 'lit/directives/repeat.js';
4
4
  import { RapidElement } from '../RapidElement';
5
5
  import { CustomEventType } from '../interfaces';
6
+ import { TicketEvent } from '../events';
6
7
  import { DEFAULT_AVATAR } from '../webchat/assets';
7
8
 
8
9
  const BATCH_TIME_WINDOW = 60 * 60 * 1000;
@@ -67,6 +68,8 @@ export interface ObjectReference {
67
68
  interface User extends ObjectReference {
68
69
  avatar?: string;
69
70
  email: string;
71
+ first_name?: string;
72
+ last_name?: string;
70
73
  }
71
74
 
72
75
  export interface Msg {
@@ -153,6 +156,11 @@ export class Chat extends RapidElement {
153
156
  left: 0;
154
157
  right: 0;
155
158
  display: block;
159
+ /* The slot overlays the bottom of the chat history, so clicks
160
+ on the chat scrollbar or messages behind it must pass
161
+ through. Slotted footer content can opt back in with
162
+ pointer-events: auto on its interactive bits. */
163
+ pointer-events: none;
156
164
  }
157
165
 
158
166
  .block {
@@ -307,11 +315,18 @@ export class Chat extends RapidElement {
307
315
 
308
316
  .note .bubble {
309
317
  background: #fffac3;
310
- color: rgba(0, 0, 0, 0.7);
318
+ border: 1px solid #e8d169;
319
+ color: #5d4e1e;
311
320
  }
312
321
 
313
322
  .note .bubble .name {
314
- color: rgba(0, 0, 0, 0.5);
323
+ color: rgba(93, 78, 30, 0.65);
324
+ }
325
+
326
+ .note-time {
327
+ margin-top: 0.35em;
328
+ font-size: 0.9em;
329
+ color: rgba(93, 78, 30, 0.65);
315
330
  }
316
331
 
317
332
  .warning .bubble {
@@ -447,6 +462,9 @@ export class Chat extends RapidElement {
447
462
  display: none;
448
463
  }
449
464
 
465
+ /* Top/bottom scroll-shadow indicators. Decorative only — they
466
+ must not intercept clicks (would otherwise block the chat
467
+ scrollbar and the bottom edge of the messages area). */
450
468
  .messages:before {
451
469
  content: '';
452
470
  background: radial-gradient(
@@ -461,6 +479,7 @@ export class Chat extends RapidElement {
461
479
  width: 100%;
462
480
  transition: opacity var(--toggle-speed, 200ms) ease-out;
463
481
  z-index: 1;
482
+ pointer-events: none;
464
483
  }
465
484
 
466
485
  .messages:after {
@@ -480,6 +499,7 @@ export class Chat extends RapidElement {
480
499
  margin-right: 5em;
481
500
  transition: opacity var(--toggle-speed, 200ms) ease-out;
482
501
  z-index: 1;
502
+ pointer-events: none;
483
503
  }
484
504
 
485
505
  .bubble-wrap {
@@ -620,6 +640,10 @@ export class Chat extends RapidElement {
620
640
  padding: 0;
621
641
  }
622
642
 
643
+ .event strong {
644
+ font-weight: 500;
645
+ }
646
+
623
647
  .collapse {
624
648
  }
625
649
 
@@ -941,15 +965,19 @@ export class Chat extends RapidElement {
941
965
  return { same: true };
942
966
  }
943
967
 
944
- // for type equivalence, treat all non-message types as the same
968
+ // for type equivalence, treat all non-message types as the same.
969
+ // Notes are treated like messages so they group with other notes
970
+ // only when type + author match (and split from non-note events).
945
971
  const isMsg1 =
946
972
  msg1.type === 'msg_created' ||
947
973
  msg1.type === 'msg_received' ||
948
- msg1.type === 'ivr_created';
974
+ msg1.type === 'ivr_created' ||
975
+ msg1.type === 'ticket_note_added';
949
976
  const isMsg2 =
950
977
  msg2.type === 'msg_created' ||
951
978
  msg2.type === 'msg_received' ||
952
- msg2.type === 'ivr_created';
979
+ msg2.type === 'ivr_created' ||
980
+ msg2.type === 'ticket_note_added';
953
981
  const typeMatch =
954
982
  isMsg1 && isMsg2 ? msg1.type === msg2.type : isMsg1 === isMsg2;
955
983
 
@@ -1180,7 +1208,8 @@ export class Chat extends RapidElement {
1180
1208
  const isMessageType =
1181
1209
  currentMsg.type === 'msg_received' ||
1182
1210
  currentMsg.type === 'msg_created' ||
1183
- currentMsg.type === 'ivr_created';
1211
+ currentMsg.type === 'ivr_created' ||
1212
+ currentMsg.type === 'ticket_note_added';
1184
1213
  const showAvatar =
1185
1214
  this.avatars && ((isMessageType && this.agent) || !incoming);
1186
1215
 
@@ -1246,13 +1275,18 @@ export class Chat extends RapidElement {
1246
1275
  const deletedClass = msgEvent._deleted ? 'deleted' : '';
1247
1276
  const latestClass = index === msgIds.length - 1 ? 'latest' : '';
1248
1277
  const eventClass = msg._rendered ? 'is-event' : '';
1278
+ const noteClass = msg.type === 'ticket_note_added' ? 'note' : '';
1249
1279
  const matchClass =
1250
1280
  this.highlightMessageUuid === msg.uuid ? 'search-match' : '';
1251
1281
  return html`<div
1252
- class="row message ${statusClass} ${unsendableClass} ${deletedClass} ${latestClass} ${eventClass} ${matchClass}"
1282
+ class="row message ${statusClass} ${unsendableClass} ${deletedClass} ${latestClass} ${eventClass} ${noteClass} ${matchClass}"
1253
1283
  data-uuid=${msg.uuid || nothing}
1254
1284
  >
1255
- ${this.renderMessage(msg, index == 0 ? name : null)}
1285
+ ${this.renderMessage(
1286
+ msg,
1287
+ index == 0 ? name : null,
1288
+ index === msgIds.length - 1
1289
+ )}
1256
1290
  </div>`;
1257
1291
  }
1258
1292
  )}
@@ -1262,6 +1296,8 @@ export class Chat extends RapidElement {
1262
1296
  <temba-user
1263
1297
  uuid=${currentMsg._user?.uuid}
1264
1298
  name=${name}
1299
+ first_name=${currentMsg._user?.first_name}
1300
+ last_name=${currentMsg._user?.last_name}
1265
1301
  avatar=${currentMsg._user?.avatar}
1266
1302
  ?system=${isSystem}
1267
1303
  >
@@ -1278,11 +1314,40 @@ export class Chat extends RapidElement {
1278
1314
  return { html: resultHtml, timestamp: newLastShownTimestamp };
1279
1315
  }
1280
1316
 
1281
- private renderMessage(event: ContactEvent, name = null): TemplateResult {
1317
+ private renderNote(
1318
+ event: TicketEvent,
1319
+ name: string | null,
1320
+ isLast: boolean
1321
+ ): TemplateResult {
1322
+ return html`<div class="bubble-wrap">
1323
+ <div class="bubble">
1324
+ ${name ? html`<div class="name">${name}</div>` : null}
1325
+ <div style="white-space: pre-wrap;">${event.note}</div>
1326
+ </div>
1327
+ ${isLast
1328
+ ? html`<div class="note-time">
1329
+ <temba-date
1330
+ value=${event.created_on.toISOString()}
1331
+ display="relative"
1332
+ ></temba-date>
1333
+ </div>`
1334
+ : null}
1335
+ </div>`;
1336
+ }
1337
+
1338
+ private renderMessage(
1339
+ event: ContactEvent,
1340
+ name = null,
1341
+ isLast = false
1342
+ ): TemplateResult {
1282
1343
  if (event._rendered) {
1283
1344
  return html`<div class="event">${event._rendered.html}</div>`;
1284
1345
  }
1285
1346
 
1347
+ if (event.type === 'ticket_note_added') {
1348
+ return this.renderNote(event as TicketEvent, name, isLast);
1349
+ }
1350
+
1286
1351
  const message = event as MsgEvent;
1287
1352
 
1288
1353
  // safety check: if msg doesn't exist, return nothing
@@ -233,6 +233,17 @@ export class Dropdown extends RapidElement {
233
233
  arrowLeft = 10;
234
234
  }
235
235
 
236
+ // safety: keep dropdown off the viewport's left edge so popups
237
+ // anchored to far-left toggles (e.g. rail items) don't rub against
238
+ // the window edge. Shift the dropdown right and slide the arrow
239
+ // back the same amount so it still points at the toggle.
240
+ const MIN_LEFT = 8;
241
+ if (dropdownBounds.left < MIN_LEFT && !bumpedLeft) {
242
+ const shift = MIN_LEFT - dropdownBounds.left;
243
+ dropdownStyle['left'] = MIN_LEFT + 'px';
244
+ arrowLeft -= shift;
245
+ }
246
+
236
247
  const arrowStyle = {
237
248
  left: arrowLeft + 'px',
238
249
  borderWidth: this.arrowSize + 'px',