@nyaruka/temba-components 0.169.0 → 0.170.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.
- package/CHANGELOG.md +19 -0
- package/dist/locales/es.js +1 -0
- package/dist/locales/es.js.map +1 -1
- package/dist/locales/fr.js +1 -0
- package/dist/locales/fr.js.map +1 -1
- package/dist/locales/pt.js +1 -0
- package/dist/locales/pt.js.map +1 -1
- package/dist/temba-components.js +501 -173
- package/dist/temba-components.js.map +1 -1
- package/package.json +1 -1
- package/src/display/Button.ts +14 -2
- package/src/display/Chat.ts +43 -24
- package/src/display/Dropdown.ts +11 -7
- package/src/display/Label.ts +18 -3
- package/src/display/Options.ts +27 -14
- package/src/display/TembaDate.ts +9 -2
- package/src/form/Compose.ts +37 -28
- package/src/form/select/Select.ts +15 -2
- package/src/interfaces.ts +13 -3
- package/src/list/BroadcastList.ts +20 -7
- package/src/list/ContactList.ts +113 -9
- package/src/list/ContentList.ts +380 -21
- package/src/list/ContentMenu.ts +12 -6
- package/src/list/FieldList.ts +8 -1
- package/src/list/TembaList.ts +99 -29
- package/src/list/TicketList.ts +87 -18
- package/src/live/CampaignEvents.ts +31 -11
- package/src/live/ContactChat.ts +36 -7
- package/src/live/ContactTimeline.ts +46 -24
- package/src/locales/es.ts +1 -0
- package/src/locales/fr.ts +1 -0
- package/src/locales/pt.ts +1 -0
- package/src/simulator/Simulator.ts +1 -0
- package/src/styles/designTokens.ts +57 -17
- package/src/styles/pillVariants.ts +42 -10
- package/static/css/design-system.css +47 -12
- package/static/css/temba-components.css +31 -9
- package/xliff/es.xlf +3 -0
- package/xliff/fr.xlf +3 -0
- package/xliff/pt.xlf +3 -0
package/src/list/ContentMenu.ts
CHANGED
|
@@ -34,15 +34,23 @@ export enum ContentMenuItemType {
|
|
|
34
34
|
export class ContentMenu extends RapidElement {
|
|
35
35
|
static get styles() {
|
|
36
36
|
return css`
|
|
37
|
-
:host {
|
|
38
|
-
tabindex: 0;
|
|
39
|
-
z-index: 5000;
|
|
40
|
-
}
|
|
41
37
|
.container {
|
|
42
38
|
display: flex;
|
|
43
39
|
align-items: center;
|
|
44
40
|
}
|
|
45
41
|
|
|
42
|
+
temba-dropdown {
|
|
43
|
+
/* as a flex item of .container this z-index takes effect and
|
|
44
|
+
creates a stacking context — keep it in sync with the popup's
|
|
45
|
+
z-index inside temba-dropdown (9000) so the open menu clears
|
|
46
|
+
floating windows like the simulator (5000). This also keeps
|
|
47
|
+
the closed toggle above those windows, intentionally: the
|
|
48
|
+
menu has to stay clickable when a floating window overlaps
|
|
49
|
+
the header. Scoped here rather than on :host so the action
|
|
50
|
+
buttons rendered alongside keep their normal stacking. */
|
|
51
|
+
z-index: 9000;
|
|
52
|
+
}
|
|
53
|
+
|
|
46
54
|
.button_item,
|
|
47
55
|
.primary_button_item {
|
|
48
56
|
margin-left: 1rem;
|
|
@@ -68,7 +76,6 @@ export class ContentMenu extends RapidElement {
|
|
|
68
76
|
color: rgb(45, 45, 45);
|
|
69
77
|
z-index: 50;
|
|
70
78
|
min-width: 200px;
|
|
71
|
-
tabindex: 0;
|
|
72
79
|
}
|
|
73
80
|
|
|
74
81
|
.divider {
|
|
@@ -82,7 +89,6 @@ export class ContentMenu extends RapidElement {
|
|
|
82
89
|
font-size: 1.1rem;
|
|
83
90
|
cursor: pointer;
|
|
84
91
|
font-weight: 400;
|
|
85
|
-
tabindex: 0;
|
|
86
92
|
}
|
|
87
93
|
|
|
88
94
|
.item:hover {
|
package/src/list/FieldList.ts
CHANGED
|
@@ -377,7 +377,14 @@ export class FieldList extends EndpointMonitorElement {
|
|
|
377
377
|
|
|
378
378
|
.menu-button.destructive:hover {
|
|
379
379
|
border-color: #dc2626;
|
|
380
|
-
|
|
380
|
+
/* pre-mixed fallback for browsers without color-mix() */
|
|
381
|
+
background: #fdf2f2;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
@supports (color: color-mix(in srgb, red, red)) {
|
|
385
|
+
.menu-button.destructive:hover {
|
|
386
|
+
background: color-mix(in srgb, #dc2626 6%, var(--surface, #fff));
|
|
387
|
+
}
|
|
381
388
|
}
|
|
382
389
|
|
|
383
390
|
.detail-body {
|
package/src/list/TembaList.ts
CHANGED
|
@@ -56,6 +56,9 @@ export class TembaList extends RapidElement {
|
|
|
56
56
|
@property({ attribute: false })
|
|
57
57
|
renderOption: (option: any, selected: boolean) => TemplateResult;
|
|
58
58
|
|
|
59
|
+
@property({ attribute: false })
|
|
60
|
+
renderDivider: (prev: any, option: any) => TemplateResult | null;
|
|
61
|
+
|
|
59
62
|
@property({ attribute: false })
|
|
60
63
|
renderOptionDetail: (option: any, selected: boolean) => TemplateResult;
|
|
61
64
|
|
|
@@ -68,6 +71,10 @@ export class TembaList extends RapidElement {
|
|
|
68
71
|
|
|
69
72
|
reverseRefresh = true;
|
|
70
73
|
|
|
74
|
+
// subclasses can enforce a display order when refreshed items are merged in,
|
|
75
|
+
// otherwise new items simply land at the top of the list
|
|
76
|
+
protected compareItems: (a: any, b: any) => number = null;
|
|
77
|
+
|
|
71
78
|
// subclasses that get realtime updates can opt out of interval polling
|
|
72
79
|
protected pollingEnabled = true;
|
|
73
80
|
|
|
@@ -251,6 +258,24 @@ export class TembaList extends RapidElement {
|
|
|
251
258
|
return Promise.resolve(results);
|
|
252
259
|
}
|
|
253
260
|
|
|
261
|
+
/**
|
|
262
|
+
* Finds where the item our cursor was pinned to ended up after a merge
|
|
263
|
+
* re-ordered our list. Returns null if the cursor is still on the right item,
|
|
264
|
+
* otherwise the index it moved to (-1 if it is no longer in the list).
|
|
265
|
+
*/
|
|
266
|
+
private findRepinnedCursorIndex(prevItem: any, newItems: any[]): number {
|
|
267
|
+
if (!prevItem) {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const prevValue = this.getValue(prevItem);
|
|
272
|
+
if (prevValue === this.getValue(newItems[this.cursorIndex])) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return newItems.findIndex((option) => this.getValue(option) === prevValue);
|
|
277
|
+
}
|
|
278
|
+
|
|
254
279
|
/**
|
|
255
280
|
* Refreshes the first page, updating any found items in our list
|
|
256
281
|
*/
|
|
@@ -303,7 +328,15 @@ export class TembaList extends RapidElement {
|
|
|
303
328
|
}
|
|
304
329
|
const newItems = [...results, ...items];
|
|
305
330
|
|
|
331
|
+
// capture the top item before any display sort - it means "the newest
|
|
332
|
+
// item we just fetched, or the previous top if we fetched nothing" and
|
|
333
|
+
// drives the Refreshed event
|
|
306
334
|
const topItem = newItems[0];
|
|
335
|
+
|
|
336
|
+
if (this.compareItems) {
|
|
337
|
+
newItems.sort(this.compareItems);
|
|
338
|
+
}
|
|
339
|
+
|
|
307
340
|
if (
|
|
308
341
|
!this.mostRecentItem ||
|
|
309
342
|
JSON.stringify(this.mostRecentItem) !== JSON.stringify(topItem)
|
|
@@ -311,27 +344,21 @@ export class TembaList extends RapidElement {
|
|
|
311
344
|
this.mostRecentItem = topItem;
|
|
312
345
|
}
|
|
313
346
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
);
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
if (options) {
|
|
327
|
-
const option =
|
|
328
|
-
options.shadowRoot.querySelector('.option.focused');
|
|
329
|
-
if (option) {
|
|
330
|
-
option.scrollIntoView({ block: 'end', inline: 'nearest' });
|
|
331
|
-
}
|
|
347
|
+
const newIndex = this.findRepinnedCursorIndex(prevItem, newItems);
|
|
348
|
+
if (newIndex !== null && newIndex > -1) {
|
|
349
|
+
this.cursorIndex = newIndex;
|
|
350
|
+
|
|
351
|
+
// make sure our focused item is visible
|
|
352
|
+
window.setTimeout(() => {
|
|
353
|
+
const options = this.shadowRoot.querySelector('temba-options');
|
|
354
|
+
if (options) {
|
|
355
|
+
const option =
|
|
356
|
+
options.shadowRoot.querySelector('.option.focused');
|
|
357
|
+
if (option) {
|
|
358
|
+
option.scrollIntoView({ block: 'end', inline: 'nearest' });
|
|
332
359
|
}
|
|
333
|
-
}
|
|
334
|
-
}
|
|
360
|
+
}
|
|
361
|
+
}, 0);
|
|
335
362
|
}
|
|
336
363
|
|
|
337
364
|
this.items = newItems;
|
|
@@ -457,18 +484,60 @@ export class TembaList extends RapidElement {
|
|
|
457
484
|
private handleScrollThreshold() {
|
|
458
485
|
if (this.nextPage && !this.loading) {
|
|
459
486
|
this.loading = true;
|
|
460
|
-
fetchResultsPage(this.nextPage)
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
sanitizedResults
|
|
464
|
-
|
|
487
|
+
fetchResultsPage(this.nextPage)
|
|
488
|
+
.then((page: ResultsPage) => {
|
|
489
|
+
return this.sanitizeResults(page.results).then(
|
|
490
|
+
(sanitizedResults: any[]) => {
|
|
491
|
+
if (this.sanitizeOption) {
|
|
492
|
+
sanitizedResults.forEach(this.sanitizeOption);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// the item our cursor is pinned to, sorting can move it
|
|
496
|
+
const prevItem = this.items[this.cursorIndex];
|
|
497
|
+
|
|
498
|
+
// drop anything we already have, a poll can pull an item from the
|
|
499
|
+
// next page up into our loaded window before we fetch it
|
|
500
|
+
const seen = new Set(
|
|
501
|
+
this.items.map((option) => this.getValue(option))
|
|
502
|
+
);
|
|
503
|
+
const appended = (sanitizedResults || []).filter(
|
|
504
|
+
(option: any) => {
|
|
505
|
+
const value = this.getValue(option);
|
|
506
|
+
if (seen.has(value)) {
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
seen.add(value);
|
|
510
|
+
return true;
|
|
511
|
+
}
|
|
512
|
+
);
|
|
465
513
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
514
|
+
const items = [...this.items, ...appended];
|
|
515
|
+
|
|
516
|
+
// the server pages in the same total order we display in, so this is
|
|
517
|
+
// normally a no-op, but a poll can re-sort an item (say a ticket that
|
|
518
|
+
// just closed) into the loaded window - without this the appended
|
|
519
|
+
// page would land below it
|
|
520
|
+
if (this.compareItems) {
|
|
521
|
+
items.sort(this.compareItems);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// the sort can shift our selection, keep the cursor on it
|
|
525
|
+
const newIndex = this.findRepinnedCursorIndex(prevItem, items);
|
|
526
|
+
if (newIndex !== null && newIndex > -1) {
|
|
527
|
+
this.cursorIndex = newIndex;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
this.items = items;
|
|
531
|
+
this.nextPage = page.next;
|
|
532
|
+
this.pages++;
|
|
533
|
+
this.loading = false;
|
|
534
|
+
}
|
|
535
|
+
);
|
|
536
|
+
})
|
|
537
|
+
.catch(() => {
|
|
538
|
+
// let the next scroll try again instead of wedging on loading
|
|
469
539
|
this.loading = false;
|
|
470
540
|
});
|
|
471
|
-
});
|
|
472
541
|
}
|
|
473
542
|
}
|
|
474
543
|
|
|
@@ -512,6 +581,7 @@ export class TembaList extends RapidElement {
|
|
|
512
581
|
?loading=${this.loading}
|
|
513
582
|
?internalFocusDisabled=${this.internalFocusDisabled}
|
|
514
583
|
.renderOption=${this.renderOption}
|
|
584
|
+
.renderDivider=${this.renderDivider}
|
|
515
585
|
.renderOptionDetail=${this.renderOptionDetail}
|
|
516
586
|
@temba-scroll-threshold=${this.handleScrollThreshold}
|
|
517
587
|
@temba-selection=${this.handleSelection.bind(this)}
|
package/src/list/TicketList.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { html, TemplateResult } from 'lit';
|
|
2
2
|
import { property } from 'lit/decorators.js';
|
|
3
|
+
import { msg } from '@lit/localize';
|
|
3
4
|
import { TembaList } from './TembaList';
|
|
4
5
|
import { Contact } from '../interfaces';
|
|
5
6
|
import { Icon } from '../Icons';
|
|
@@ -9,15 +10,17 @@ export class TicketList extends TembaList {
|
|
|
9
10
|
agent = '';
|
|
10
11
|
|
|
11
12
|
public getRefreshEndpoint() {
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
// open tickets sort above closed ones, so the newest activity can be
|
|
14
|
+
// anywhere in the list, not just the top. Skip anything unparseable so a
|
|
15
|
+
// single bad timestamp can't poison the cursor with NaN
|
|
16
|
+
const lastActivity = this.items.reduce((newest, item) => {
|
|
17
|
+
const activity = new Date(item.ticket.last_activity_on).getTime();
|
|
18
|
+
return isNaN(activity) ? newest : Math.max(newest, activity);
|
|
19
|
+
}, 0);
|
|
20
|
+
|
|
21
|
+
if (lastActivity > 0) {
|
|
14
22
|
const separator = this.endpoint.includes('?') ? '&' : '?';
|
|
15
|
-
return
|
|
16
|
-
this.endpoint +
|
|
17
|
-
separator +
|
|
18
|
-
'after=' +
|
|
19
|
-
new Date(lastActivity).getTime() * 1000
|
|
20
|
-
);
|
|
23
|
+
return this.endpoint + separator + 'after=' + lastActivity * 1000;
|
|
21
24
|
}
|
|
22
25
|
return this.endpoint;
|
|
23
26
|
}
|
|
@@ -35,10 +38,79 @@ export class TicketList extends TembaList {
|
|
|
35
38
|
super();
|
|
36
39
|
|
|
37
40
|
this.valueKey = 'ticket.uuid';
|
|
38
|
-
|
|
41
|
+
// the refresh feed is served oldest first - the inherited reverseRefresh
|
|
42
|
+
// keeps mostRecentItem meaning the newest fetched item (compareItems
|
|
43
|
+
// re-sorts the merged list immediately anyway)
|
|
44
|
+
|
|
45
|
+
this.compareItems = (a: Contact, b: Contact): number => {
|
|
46
|
+
const aClosed = !!a.ticket.closed_on;
|
|
47
|
+
const bClosed = !!b.ticket.closed_on;
|
|
48
|
+
if (aClosed !== bClosed) {
|
|
49
|
+
return aClosed ? 1 : -1;
|
|
50
|
+
}
|
|
51
|
+
return (
|
|
52
|
+
new Date(b.ticket.last_activity_on).getTime() -
|
|
53
|
+
new Date(a.ticket.last_activity_on).getTime()
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
// a quiet label marks where the list crosses from open into closed
|
|
57
|
+
// tickets - the rows themselves then only need gentle muting
|
|
58
|
+
this.renderDivider = (
|
|
59
|
+
prev: Contact,
|
|
60
|
+
contact: Contact
|
|
61
|
+
): TemplateResult | null => {
|
|
62
|
+
if (!contact.ticket.closed_on || (prev && prev.ticket.closed_on)) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
const closed = msg('Closed');
|
|
66
|
+
return html`
|
|
67
|
+
<div
|
|
68
|
+
role="separator"
|
|
69
|
+
aria-label=${closed}
|
|
70
|
+
style="display:flex; align-items:center; gap:0.6em; padding:0.9em var(--pad) 0.2em var(--pad);"
|
|
71
|
+
>
|
|
72
|
+
<div
|
|
73
|
+
style="font-size:0.7em; font-weight:500; letter-spacing:0.08em; text-transform:uppercase; color:var(--text-4);"
|
|
74
|
+
>
|
|
75
|
+
${closed}
|
|
76
|
+
</div>
|
|
77
|
+
<div
|
|
78
|
+
style="flex-grow:1; height:1px; background:var(--color-widget-border);"
|
|
79
|
+
></div>
|
|
80
|
+
</div>
|
|
81
|
+
`;
|
|
82
|
+
};
|
|
83
|
+
this.renderOption = (
|
|
84
|
+
contact: Contact,
|
|
85
|
+
selected: boolean
|
|
86
|
+
): TemplateResult => {
|
|
87
|
+
// closed tickets are settled history - a compact single line that
|
|
88
|
+
// recedes behind the open work above it
|
|
89
|
+
if (contact.ticket.closed_on) {
|
|
90
|
+
return html`
|
|
91
|
+
<div
|
|
92
|
+
style="display:flex; align-items:baseline; margin-top:0.1em; margin-bottom:0.1em; ${selected
|
|
93
|
+
? ''
|
|
94
|
+
: 'color:var(--text-3);'}"
|
|
95
|
+
>
|
|
96
|
+
<div
|
|
97
|
+
style="flex:1; min-width:0; font-weight:400; line-height:1.6; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;"
|
|
98
|
+
>
|
|
99
|
+
${contact.name}
|
|
100
|
+
</div>
|
|
101
|
+
<div style="font-size:0.8em; margin-left:0.75em;">
|
|
102
|
+
<temba-date
|
|
103
|
+
value=${contact.ticket.closed_on}
|
|
104
|
+
display="duration"
|
|
105
|
+
></temba-date>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
`;
|
|
109
|
+
}
|
|
110
|
+
|
|
39
111
|
return html`
|
|
40
112
|
<div
|
|
41
|
-
style="align-items:center; margin-top: 0.1em; margin-bottom: 0.1em"
|
|
113
|
+
style="align-items:center; margin-top: 0.1em; margin-bottom: 0.1em;"
|
|
42
114
|
>
|
|
43
115
|
<div
|
|
44
116
|
style="display:flex; align-items: flex-start;border:0px solid red;"
|
|
@@ -49,10 +121,8 @@ export class TicketList extends TembaList {
|
|
|
49
121
|
>
|
|
50
122
|
${contact.name}
|
|
51
123
|
</div>
|
|
52
|
-
${contact.
|
|
53
|
-
?
|
|
54
|
-
: contact.last_msg
|
|
55
|
-
? html`
|
|
124
|
+
${contact.last_msg
|
|
125
|
+
? html`
|
|
56
126
|
<div
|
|
57
127
|
style="font-size: 0.9em; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;"
|
|
58
128
|
>
|
|
@@ -81,13 +151,13 @@ export class TicketList extends TembaList {
|
|
|
81
151
|
}
|
|
82
152
|
</div></div>
|
|
83
153
|
`
|
|
84
|
-
|
|
154
|
+
: null}
|
|
85
155
|
</div>
|
|
86
156
|
<div
|
|
87
157
|
style="margin-right: -5px; margin-top: 0px;display:flex;flex-direction:column;align-items:flex-end;max-width:60px;min-width:30px;border:0px solid green;text-align:right"
|
|
88
158
|
>
|
|
89
159
|
<div>
|
|
90
|
-
${
|
|
160
|
+
${contact.ticket.assignee
|
|
91
161
|
? html`<temba-user
|
|
92
162
|
name=${contact.ticket.assignee.name}
|
|
93
163
|
email=${contact.ticket.assignee.email}
|
|
@@ -101,8 +171,7 @@ export class TicketList extends TembaList {
|
|
|
101
171
|
|
|
102
172
|
<div style="font-size:0.8em;text-align:right;border:0px solid red;">
|
|
103
173
|
<temba-date
|
|
104
|
-
value=${contact.ticket.
|
|
105
|
-
contact.ticket.last_activity_on}
|
|
174
|
+
value=${contact.ticket.last_activity_on}
|
|
106
175
|
display="duration"
|
|
107
176
|
></temba-date>
|
|
108
177
|
</div>
|
|
@@ -273,11 +273,20 @@ export class CampaignEvents extends EndpointMonitorElement {
|
|
|
273
273
|
justify-content: center;
|
|
274
274
|
/* icons inside the dot take the dot's hue */
|
|
275
275
|
--icon-color: var(--dot-color);
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
276
|
+
/* plain widget bg for browsers without color-mix(); the tinted
|
|
277
|
+
fill below needs the @supports gate since var() keeps an
|
|
278
|
+
invalid declaration alive until computed-value time */
|
|
279
|
+
background: var(--color-widget-bg, #fff);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
@supports (color: color-mix(in srgb, red, red)) {
|
|
283
|
+
.dot {
|
|
284
|
+
background: color-mix(
|
|
285
|
+
in srgb,
|
|
286
|
+
var(--dot-color) 12%,
|
|
287
|
+
var(--color-widget-bg, #fff)
|
|
288
|
+
);
|
|
289
|
+
}
|
|
281
290
|
}
|
|
282
291
|
|
|
283
292
|
/* the anchor marks the field's date itself - painted with the widget
|
|
@@ -470,7 +479,8 @@ export class CampaignEvents extends EndpointMonitorElement {
|
|
|
470
479
|
|
|
471
480
|
.menu-button.destructive:hover {
|
|
472
481
|
border-color: #dc2626;
|
|
473
|
-
|
|
482
|
+
/* pre-mixed fallback for browsers without color-mix() */
|
|
483
|
+
background: #fdf2f2;
|
|
474
484
|
}
|
|
475
485
|
|
|
476
486
|
/* the event's action, contained with a leading type label. Children
|
|
@@ -481,16 +491,26 @@ export class CampaignEvents extends EndpointMonitorElement {
|
|
|
481
491
|
align-items: flex-start;
|
|
482
492
|
gap: 0.6em;
|
|
483
493
|
padding: 0.9em 1em 1em;
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
var(--sunken, #f1f3f5) 45%,
|
|
487
|
-
var(--surface, #fff)
|
|
488
|
-
);
|
|
494
|
+
/* pre-mixed fallback for browsers without color-mix() */
|
|
495
|
+
background: #f9fafa;
|
|
489
496
|
border: 1px solid var(--border, #e4e7ec);
|
|
490
497
|
border-radius: var(--r);
|
|
491
498
|
line-height: 1.5;
|
|
492
499
|
}
|
|
493
500
|
|
|
501
|
+
@supports (color: color-mix(in srgb, red, red)) {
|
|
502
|
+
.menu-button.destructive:hover {
|
|
503
|
+
background: color-mix(in srgb, #dc2626 6%, var(--surface, #fff));
|
|
504
|
+
}
|
|
505
|
+
.detail-action {
|
|
506
|
+
background: color-mix(
|
|
507
|
+
in srgb,
|
|
508
|
+
var(--sunken, #f1f3f5) 45%,
|
|
509
|
+
var(--surface, #fff)
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
494
514
|
/* the close (✕) in the header's upper right - the square
|
|
495
515
|
hover-wash button treatment (the pager buttons'), sitting
|
|
496
516
|
just outboard of the action buttons */
|
package/src/live/ContactChat.ts
CHANGED
|
@@ -140,10 +140,18 @@ export class ContactChat extends ContactStoreElement {
|
|
|
140
140
|
|
|
141
141
|
/* While the contact is in a flow the compose outline carries the
|
|
142
142
|
flow hue: resting border matches the flow pill border, focus
|
|
143
|
-
goes solid flow green
|
|
143
|
+
goes solid flow green. The pre-mixed static comes first — a
|
|
144
|
+
custom property can't fall back per-declaration, so the
|
|
145
|
+
derived version needs the @supports gate. */
|
|
144
146
|
.in-flow .compose {
|
|
145
|
-
--compose-border: 1px solid
|
|
146
|
-
|
|
147
|
+
--compose-border: 1px solid #c5e8d2;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
@supports (color: color-mix(in srgb, red, red)) {
|
|
151
|
+
.in-flow .compose {
|
|
152
|
+
--compose-border: 1px solid
|
|
153
|
+
color-mix(in srgb, var(--flow, #16a34a) 25%, white);
|
|
154
|
+
}
|
|
147
155
|
}
|
|
148
156
|
|
|
149
157
|
.in-flow .compose temba-compose {
|
|
@@ -459,10 +467,18 @@ export class ContactChat extends ContactStoreElement {
|
|
|
459
467
|
status-area grey. Fallbacks match the --flow design token for
|
|
460
468
|
pages without tokens. */
|
|
461
469
|
.in-flow .contact-status {
|
|
462
|
-
|
|
470
|
+
/* pre-mixed statics first for browsers without color-mix() */
|
|
471
|
+
background: #e3f4e9;
|
|
463
472
|
color: var(--flow, #16a34a);
|
|
464
473
|
--icon-color: var(--flow, #16a34a);
|
|
465
|
-
border-top-color:
|
|
474
|
+
border-top-color: #c5e8d2;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
@supports (color: color-mix(in srgb, red, red)) {
|
|
478
|
+
.in-flow .contact-status {
|
|
479
|
+
background: color-mix(in srgb, var(--flow, #16a34a) 12%, white);
|
|
480
|
+
border-top-color: color-mix(in srgb, var(--flow, #16a34a) 25%, white);
|
|
481
|
+
}
|
|
466
482
|
}
|
|
467
483
|
|
|
468
484
|
/* compact the interrupt button to the row's caption scale */
|
|
@@ -530,7 +546,14 @@ export class ContactChat extends ContactStoreElement {
|
|
|
530
546
|
}
|
|
531
547
|
|
|
532
548
|
.in-flow .chat-box {
|
|
533
|
-
|
|
549
|
+
/* pre-mixed static first for browsers without color-mix() */
|
|
550
|
+
background: #e3f4e9;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
@supports (color: color-mix(in srgb, red, red)) {
|
|
554
|
+
.in-flow .chat-box {
|
|
555
|
+
background: color-mix(in srgb, var(--flow, #16a34a) 12%, white);
|
|
556
|
+
}
|
|
534
557
|
}
|
|
535
558
|
|
|
536
559
|
.ticket-controls {
|
|
@@ -883,9 +906,15 @@ export class ContactChat extends ContactStoreElement {
|
|
|
883
906
|
return null;
|
|
884
907
|
}
|
|
885
908
|
|
|
909
|
+
// unparseable values and zero-value times (e.g. go's zero time for a
|
|
910
|
+
// contact that has never been seen) mean there's no last seen to show
|
|
911
|
+
const lastSeen = DateTime.fromISO(lastSeenOn);
|
|
912
|
+
if (!lastSeen.isValid || lastSeen.year <= 1) {
|
|
913
|
+
return null;
|
|
914
|
+
}
|
|
915
|
+
|
|
886
916
|
// recent activity speaks for itself in the chat - only surface last
|
|
887
917
|
// seen once the contact has been quiet for at least an hour
|
|
888
|
-
const lastSeen = DateTime.fromISO(lastSeenOn);
|
|
889
918
|
const minutes = DateTime.now().diff(lastSeen, 'minutes').minutes;
|
|
890
919
|
if (minutes < 60) {
|
|
891
920
|
return null;
|
|
@@ -189,24 +189,36 @@ export class ContactTimeline extends EndpointMonitorElement {
|
|
|
189
189
|
padding: 0.05em 0.65em;
|
|
190
190
|
border-radius: 999px;
|
|
191
191
|
white-space: nowrap;
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
);
|
|
197
|
-
border: 1px solid
|
|
198
|
-
color-mix(in srgb, var(--pill-hue) 25%, var(--color-widget-bg, #fff));
|
|
192
|
+
/* plain bg / neutral border for browsers without color-mix();
|
|
193
|
+
the hue-tinted versions live in the @supports block below */
|
|
194
|
+
background: var(--color-widget-bg, #fff);
|
|
195
|
+
border: 1px solid var(--pill-hue);
|
|
199
196
|
color: var(--pill-hue);
|
|
200
197
|
cursor: pointer;
|
|
201
198
|
transition: background 100ms ease-in-out;
|
|
202
199
|
}
|
|
203
200
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
201
|
+
@supports (color: color-mix(in srgb, red, red)) {
|
|
202
|
+
.campaign-pill {
|
|
203
|
+
background: color-mix(
|
|
204
|
+
in srgb,
|
|
205
|
+
var(--pill-hue) 12%,
|
|
206
|
+
var(--color-widget-bg, #fff)
|
|
207
|
+
);
|
|
208
|
+
border: 1px solid
|
|
209
|
+
color-mix(
|
|
210
|
+
in srgb,
|
|
211
|
+
var(--pill-hue) 25%,
|
|
212
|
+
var(--color-widget-bg, #fff)
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
.campaign-pill:hover {
|
|
216
|
+
background: color-mix(
|
|
217
|
+
in srgb,
|
|
218
|
+
var(--pill-hue) 22%,
|
|
219
|
+
var(--color-widget-bg, #fff)
|
|
220
|
+
);
|
|
221
|
+
}
|
|
210
222
|
}
|
|
211
223
|
|
|
212
224
|
.campaign-pill:focus-visible {
|
|
@@ -289,24 +301,34 @@ export class ContactTimeline extends EndpointMonitorElement {
|
|
|
289
301
|
}
|
|
290
302
|
|
|
291
303
|
/* past events are filled with a lighter tint of the same hue and keep
|
|
292
|
-
the solid border - they're settled, whether real or projected
|
|
304
|
+
the solid border - they're settled, whether real or projected.
|
|
305
|
+
Plain widget bg first for browsers without color-mix(). */
|
|
293
306
|
.dot.past {
|
|
294
|
-
background: color-
|
|
295
|
-
in srgb,
|
|
296
|
-
var(--dot-color) 25%,
|
|
297
|
-
var(--color-widget-bg, #fff)
|
|
298
|
-
);
|
|
307
|
+
background: var(--color-widget-bg, #fff);
|
|
299
308
|
}
|
|
300
309
|
|
|
301
310
|
/* future events haven't occurred yet - signal that with a dotted
|
|
302
311
|
border and the faintest tint of the hue inside */
|
|
303
312
|
.dot.future {
|
|
304
313
|
border-style: dotted;
|
|
305
|
-
background: color-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
314
|
+
background: var(--color-widget-bg, #fff);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
@supports (color: color-mix(in srgb, red, red)) {
|
|
318
|
+
.dot.past {
|
|
319
|
+
background: color-mix(
|
|
320
|
+
in srgb,
|
|
321
|
+
var(--dot-color) 25%,
|
|
322
|
+
var(--color-widget-bg, #fff)
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
.dot.future {
|
|
326
|
+
background: color-mix(
|
|
327
|
+
in srgb,
|
|
328
|
+
var(--dot-color) 8%,
|
|
329
|
+
var(--color-widget-bg, #fff)
|
|
330
|
+
);
|
|
331
|
+
}
|
|
310
332
|
}
|
|
311
333
|
|
|
312
334
|
/* the now dot and label both paint with the SPA content background
|
package/src/locales/es.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
'sbc913d7dc0f33877': `to add`,
|
|
15
15
|
's7722a91d3a512442': str`Last seen ${0}`,
|
|
16
16
|
's22965bb9808befb0': `Interrupt`,
|
|
17
|
+
's6dbbe2646b239ca5': `Closed`,
|
|
17
18
|
's122d4de68bcfcdf4': `It's okay to restart`,
|
|
18
19
|
's3eb2567092b4d7c1': `from the beginning`,
|
|
19
20
|
's28f37776b3901438': `It's okay to interrupt`,
|
package/src/locales/fr.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
'sbc913d7dc0f33877': `to add`,
|
|
15
15
|
's7722a91d3a512442': str`Last seen ${0}`,
|
|
16
16
|
's22965bb9808befb0': `Interrupt`,
|
|
17
|
+
's6dbbe2646b239ca5': `Closed`,
|
|
17
18
|
's122d4de68bcfcdf4': `It's okay to restart`,
|
|
18
19
|
's3eb2567092b4d7c1': `from the beginning`,
|
|
19
20
|
's28f37776b3901438': `It's okay to interrupt`,
|
package/src/locales/pt.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
'sbc913d7dc0f33877': `to add`,
|
|
15
15
|
's7722a91d3a512442': str`Last seen ${0}`,
|
|
16
16
|
's22965bb9808befb0': `Interrupt`,
|
|
17
|
+
's6dbbe2646b239ca5': `Closed`,
|
|
17
18
|
's122d4de68bcfcdf4': `It's okay to restart`,
|
|
18
19
|
's3eb2567092b4d7c1': `from the beginning`,
|
|
19
20
|
's28f37776b3901438': `It's okay to interrupt`,
|