@nyaruka/temba-components 0.157.0 → 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 (38) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/temba-components.js +1450 -1370
  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 +60 -9
  7. package/src/display/Dropdown.ts +11 -0
  8. package/src/display/Label.ts +1 -3
  9. package/src/display/LeafletMap.ts +4 -3
  10. package/src/display/TembaUser.ts +9 -3
  11. package/src/events/eventRenderers.ts +152 -67
  12. package/src/flow/AutoTranslate.ts +2 -2
  13. package/src/flow/Editor.ts +4 -4
  14. package/src/flow/NodeEditor.ts +2 -2
  15. package/src/flow/NodeTypeSelector.ts +0 -5
  16. package/src/flow/actions/set_contact_language.ts +5 -4
  17. package/src/flow/nodes/split_by_llm_categorize.ts +1 -6
  18. package/src/flow/utils.ts +1 -20
  19. package/src/form/ColorPicker.ts +5 -3
  20. package/src/form/select/Omnibox.ts +1 -3
  21. package/src/form/select/Select.ts +5 -4
  22. package/src/interfaces.ts +1 -0
  23. package/src/languages.ts +56 -0
  24. package/src/layout/Dialog.ts +1 -3
  25. package/src/list/ContentMenu.ts +1 -2
  26. package/src/list/SortableList.ts +1 -4
  27. package/src/list/TembaMenu.ts +159 -113
  28. package/src/live/ContactBadges.ts +2 -1
  29. package/src/live/ContactChat.ts +16 -1
  30. package/src/live/ContactDetails.ts +2 -1
  31. package/src/live/ContactFieldEditor.ts +0 -2
  32. package/src/store/AppState.ts +3 -21
  33. package/src/store/Store.ts +0 -29
  34. package/src/styles/designTokens.ts +31 -18
  35. package/src/styles/pillVariants.ts +24 -13
  36. package/static/css/temba-components.css +82 -55
  37. package/web-dev-server.config.mjs +0 -1
  38. package/web-test-runner.config.mjs +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nyaruka/temba-components",
3
- "version": "0.157.0",
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;
@@ -314,11 +315,18 @@ export class Chat extends RapidElement {
314
315
 
315
316
  .note .bubble {
316
317
  background: #fffac3;
317
- color: rgba(0, 0, 0, 0.7);
318
+ border: 1px solid #e8d169;
319
+ color: #5d4e1e;
318
320
  }
319
321
 
320
322
  .note .bubble .name {
321
- 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);
322
330
  }
323
331
 
324
332
  .warning .bubble {
@@ -632,6 +640,10 @@ export class Chat extends RapidElement {
632
640
  padding: 0;
633
641
  }
634
642
 
643
+ .event strong {
644
+ font-weight: 500;
645
+ }
646
+
635
647
  .collapse {
636
648
  }
637
649
 
@@ -953,15 +965,19 @@ export class Chat extends RapidElement {
953
965
  return { same: true };
954
966
  }
955
967
 
956
- // 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).
957
971
  const isMsg1 =
958
972
  msg1.type === 'msg_created' ||
959
973
  msg1.type === 'msg_received' ||
960
- msg1.type === 'ivr_created';
974
+ msg1.type === 'ivr_created' ||
975
+ msg1.type === 'ticket_note_added';
961
976
  const isMsg2 =
962
977
  msg2.type === 'msg_created' ||
963
978
  msg2.type === 'msg_received' ||
964
- msg2.type === 'ivr_created';
979
+ msg2.type === 'ivr_created' ||
980
+ msg2.type === 'ticket_note_added';
965
981
  const typeMatch =
966
982
  isMsg1 && isMsg2 ? msg1.type === msg2.type : isMsg1 === isMsg2;
967
983
 
@@ -1192,7 +1208,8 @@ export class Chat extends RapidElement {
1192
1208
  const isMessageType =
1193
1209
  currentMsg.type === 'msg_received' ||
1194
1210
  currentMsg.type === 'msg_created' ||
1195
- currentMsg.type === 'ivr_created';
1211
+ currentMsg.type === 'ivr_created' ||
1212
+ currentMsg.type === 'ticket_note_added';
1196
1213
  const showAvatar =
1197
1214
  this.avatars && ((isMessageType && this.agent) || !incoming);
1198
1215
 
@@ -1258,13 +1275,18 @@ export class Chat extends RapidElement {
1258
1275
  const deletedClass = msgEvent._deleted ? 'deleted' : '';
1259
1276
  const latestClass = index === msgIds.length - 1 ? 'latest' : '';
1260
1277
  const eventClass = msg._rendered ? 'is-event' : '';
1278
+ const noteClass = msg.type === 'ticket_note_added' ? 'note' : '';
1261
1279
  const matchClass =
1262
1280
  this.highlightMessageUuid === msg.uuid ? 'search-match' : '';
1263
1281
  return html`<div
1264
- class="row message ${statusClass} ${unsendableClass} ${deletedClass} ${latestClass} ${eventClass} ${matchClass}"
1282
+ class="row message ${statusClass} ${unsendableClass} ${deletedClass} ${latestClass} ${eventClass} ${noteClass} ${matchClass}"
1265
1283
  data-uuid=${msg.uuid || nothing}
1266
1284
  >
1267
- ${this.renderMessage(msg, index == 0 ? name : null)}
1285
+ ${this.renderMessage(
1286
+ msg,
1287
+ index == 0 ? name : null,
1288
+ index === msgIds.length - 1
1289
+ )}
1268
1290
  </div>`;
1269
1291
  }
1270
1292
  )}
@@ -1292,11 +1314,40 @@ export class Chat extends RapidElement {
1292
1314
  return { html: resultHtml, timestamp: newLastShownTimestamp };
1293
1315
  }
1294
1316
 
1295
- 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 {
1296
1343
  if (event._rendered) {
1297
1344
  return html`<div class="event">${event._rendered.html}</div>`;
1298
1345
  }
1299
1346
 
1347
+ if (event.type === 'ticket_note_added') {
1348
+ return this.renderNote(event as TicketEvent, name, isLast);
1349
+ }
1350
+
1300
1351
  const message = event as MsgEvent;
1301
1352
 
1302
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',
@@ -290,9 +290,7 @@ export default class Label extends LitElement {
290
290
  <temba-icon name="x" size="0.85"></temba-icon>
291
291
  </button>`
292
292
  : null}
293
- ${resolvedIcon
294
- ? html`<temba-icon name=${resolvedIcon} />`
295
- : null}
293
+ ${resolvedIcon ? html`<temba-icon name=${resolvedIcon} />` : null}
296
294
  <slot></slot>
297
295
  </div>
298
296
  </div>
@@ -1,13 +1,14 @@
1
1
  import { Feature, Geometry } from 'geojson';
2
- import {
2
+ import type {
3
3
  GeoJSON,
4
- geoJSON,
5
4
  LeafletEvent,
6
5
  LeafletMouseEvent,
7
6
  Map as RenderedMap,
8
- map as createMap,
9
7
  Path
10
8
  } from 'leaflet';
9
+ import * as L from 'leaflet';
10
+
11
+ const { geoJSON, map: createMap } = L;
11
12
  import { css, html, LitElement } from 'lit';
12
13
  import { property } from 'lit/decorators.js';
13
14
 
@@ -21,7 +21,6 @@ export class TembaUser extends RapidElement {
21
21
  public static styles = css`
22
22
  :host {
23
23
  display: flex;
24
- transform: scale(var(--temba-scale, 1));
25
24
  box-sizing: border-box;
26
25
  }
27
26
 
@@ -32,6 +31,10 @@ export class TembaUser extends RapidElement {
32
31
  flex-grow: 1;
33
32
  }
34
33
 
34
+ .avatar-circle {
35
+ transform-origin: left center;
36
+ }
37
+
35
38
  .name {
36
39
  flex-grow: 1;
37
40
  display: -webkit-box;
@@ -115,7 +118,7 @@ export class TembaUser extends RapidElement {
115
118
  <div
116
119
  class="avatar-circle"
117
120
  style="
118
- transform:scale(${this.scale || 1});
121
+ transform:scale(calc(var(--temba-scale, 1) * ${this.scale || 1}));
119
122
  display: flex;
120
123
  min-height: 26px;
121
124
  min-width: 26px;
@@ -126,7 +129,10 @@ export class TembaUser extends RapidElement {
126
129
  font-weight: 400;
127
130
  overflow: hidden;
128
131
  font-size: 0.8em;
129
- margin-right: 0.75em;
132
+ margin-right: max(
133
+ 0px,
134
+ calc(0.75em - (1 - var(--temba-scale, 1)) * 26px)
135
+ );
130
136
  box-shadow: inset 0 0 0 3px rgba(0, 0, 0, 0.1);
131
137
  background:${this.bgimage || this.bgcolor};"
132
138
  >